배경
개발을 하다 보면 항상 에러를 마주하기 마련이다. 그리고 터미널에 띄워주는 오류는 문제해결을 할 수 있는 아주 큰 도움을 주는 건 사실이다. 하지만 위에 보여주는 에러는 내가 믿고 거르는 에러이다. 보통은 오류의 권고는 아주 유용하지만 이 케이스는 유용하지 않은 경우이다.
오류 메세지를 보면 act에서 코드를 감싸라고 권고하는데 여기서는 그렇게 해선 안된다.
비슷한 에러중 하나는
Warning: Cant' perform a React state update on an unmounted component.
라고 하는 에러다. 이 두 가지 오류 중 하나가 나타났다면 아마도 원인은 테스트가 끝난 뒤에 컴포넌트가 바뀌기 때문이다.
이게 무슨 소리냐면, 일부 비동기 상태 업데이트가 완료되기 전에 테스트 함수가 종료된다는 이야기이다.
예시
예시를 들면,
// 네트워크 통신
const getMyProfile = async () => {
const response = await API.getMyProfile();
return response;
};
const setMyProfile = async () => {
const data = await getMyProfile();
setProfile({
nickname: { value: data.nickname, initialValue: data.nickname },
introduce: { value: data.introduce, initialValue: data.introduce },
});
setImageUrl(data.profileImageFileName);
};
useEffect(() => {
setMyProfile();
}, []);
현재 위의 코드를 테스트하는 코드인
describe('프로필 페이지에서', () => {
test('화면에 텍스트, 이미지, 버튼이 렌더링 되는지', async () => {
renderWithProviders(<Profile accessToken='testToken' />);
// 텍스트가 렌더링 되는지 확인
expect(screen.getByText('아이디')).toBeInTheDocument();
expect(screen.getByText('자기소개')).toBeInTheDocument();
expect(screen.getByText('수정하기')).toBeInTheDocument();
// 이미지가 렌더링되는지 확인
const imageElement = screen.getByAltText('cat');
expect(imageElement).toBeInTheDocument();
// 초기에는 버튼이 비활성화되어 있어야 함
const submitButton = screen.getByText('수정하기') as HTMLButtonElement;
expect(submitButton).toBeDisabled();
});
이 코드는 아까 처음에 나왔던 에러가 발생한다.
문제 해결
왜 에러가 발생할까?
동작을 천천히 뜯어보면,
1. 테스트 코드 동작
2. render함수에 의해 Profile 컴포넌트 렌더링
3. mount 되면서 useEffect 실행
4. setProfile 실행 (비동기 통신)
5. 테스트 코드 동작완료
6. Profile 컴포넌트 unMount
7. 네트워크 통신이 완료되지 않은 상태에서 unMount 됨으로 에러 발생!
이러한 플로우로 흘러간다.
테스트 코드가 끝나고 종료될 때 아직 비동기적인 통신이 마무리되지 않아서 위와 같은 에러가 발생하는 것이다.
어떻게 해결할까?
문제는 6번, 7번이다.
앞의 동작은 그대로 두고 unMount 되는 시점을 컨트롤 해야한다.
컴포넌트가 unMount 될 때 클린업 과정을 추가하고 테스트에서 명시적으로 컴포넌트를 언마운트 시켜버린다.
우선 네트워크 통신을 컨트롤하기 위해 cancelToken.js를 추가한다.
import axios from 'axios';
let source;
export const createSource = () => {
source = axios.CancelToken.source();
return source;
};
export const getSource = () => source;
export const cancelSource = () => {
if (source) {
source.cancel('Component unmounted');
}
};
그 이후에 API 함수인 이 코드를
수정 전
getMyProfile: async () => {
const response = await clientInstance.get(`user`);
return response.data;
},
수정 후
getMyProfile: async () => {
const source = createSource();
try {
const response = await clientInstance.get('user', {
cancelToken: source.token,
});
return response.data;
} catch (error) {
if (axios.isCancel(error)) {
return null;
}
throw error;
}
},
로 바꿔줌으로써 네트워크 통신을 컨트롤할 수 있게 세팅해 놓는다.
그리고 테스트할 컴포넌트의 클린업 코드를 추가한다.
useEffect(() => {
setMyProfile();
dispatch(routerSlice.actions.loadProfileDetailPage());
return () => {
// axios 요청 취소
cancelSource();
};
}, []);
마지막으로 테스트 코드에서 renderWithProviders() 함수에 unmount()를 분리해서 테스트 코드 동작 이후 마지막에 unmount()시킨다.
test('화면에 텍스트, 이미지, 버튼이 렌더링 되는지', async () => {
const { unmount } = renderWithProviders(<Profile accessToken='testToken' />);
// 텍스트가 렌더링 되는지 확인
expect(screen.getByText('아이디')).toBeInTheDocument();
expect(screen.getByText('자기소개')).toBeInTheDocument();
expect(screen.getByText('수정하기')).toBeInTheDocument();
// 이미지가 렌더링되는지 확인
const imageElement = screen.getByAltText('cat');
expect(imageElement).toBeInTheDocument();
// 초기에는 버튼이 비활성화되어 있어야 함
const submitButton = screen.getByText('수정하기') as HTMLButtonElement;
expect(submitButton).toBeDisabled();
unmount();
});
결과
'토이프로젝트 > LinkArchive' 카테고리의 다른 글
횡단 관심사 분리를 위한 Axios 인스턴스와 인터셉터 설정 (0) | 2023.07.27 |
---|---|
Redux Provider로 감싸진 컴포넌트 테스트하기 (0) | 2023.07.15 |
Next.js에서 토큰 재발급 구현하기 (3) | 2023.06.14 |