getServerSideProps
메소드에서 react-query를 이용해 서버사이드에서 데이터를 패치하고 페이지에 전달하는데 이 컴포넌트를 테스트하다가 발생한 문제를 해결했던 과정을 기록했다.
특정 페이지 컴포넌트를 테스트하는데 이 페이지는 getServerSideProps
메소드를 통해 서버사이드에서 목록을 조회하고 이를 클라이언트로 넘기는 구조이다.
const CategoryById = ({ categoryId, isMine }: MyCategoryByIdPageProps) => {
const { cardList, hasNextPage, fetchNextPage } = useCardList(categoryId, isMine);
return (
<ListSection>
...와꾸
</ListSection>
);
};
export const getServerSideProps: GetServerSideProps = ssrAspect(async (context, queryClient, memberId) => {
const { categoryId } = context.query;
if (!categoryId || typeof categoryId !== 'string') {
throw { url: '/' };
}
const isMine: boolean = context.params?.memberId === memberId ?? false;
await queryClient.fetchInfiniteQuery(
[queryKey.cards, categoryId],
() => getCardList(categoryId, isMine, queryClient),
{
getNextPageParam: (lastPage) => lastPage.hasNext,
},
);
return { categoryId, isMine };
}, true);
서버사이드 처리에서 요청에 특정 query가 없거나 본인 데이터가 아니라던가 등에 따라 동작이 많이 달라져서
ssr동작을 포함하여 페이지를 렌더링할 때에 대한 테스트가 필요해보였다.
jest.mock('next/router', () => ({
useRouter: jest.fn(),
}));
jest.mock('nookies', () => ({
get: jest.fn(),
}));
describe('CardListPage', () => {
afterEach(() => {
jest.clearAllMocks();
});
it('회원 본인의 카드 목록 요청에 대해 카드 목록이 보여진다.', async () => {
(nookies.get as jest.Mock).mockReturnValue({
accessToken: 'token',
});
const mockedContext = {
req: {
url: '/abc01/categories/' + MOCK_CATEGORY_ID,
},
query: {
categoryId: MOCK_CATEGORY_ID,
},
params: {
memberId: 'abc01',
},
} as unknown as GetServerSidePropsContext;
const { props } = (await getServerSideProps(mockedContext)) as any;
expect(props).toHaveProperty('categoryId');
expect(props).toHaveProperty('isMine', true);
renderQuery(<CategoryById categoryId={props.categoryId} isMine={props.isMine} />);
const addButton = screen.getByRole('button', {
name: /추가하기/i,
});
expect(addButton).toBeInTheDocument();
const noListText = screen.queryByText(/목록이 존재하지 않습니다/i);
expect(noListText).not.toBeInTheDocument();
});
});
정상적으로 본인의 목록을 요청하는 상황에 대한 테스트 케이스이다.
테스트코드를 요약하면 해당 페이지의 ssr 메소드에 대한 context를 mocking해 수행하고 그 다음 해당 페이지 컴포넌트를 렌더링 시켜 목록이 존재하는지 assertion 하는 것이다.
서버사이드에서 정상적으로 mock handler api를 호출해 데이터를 패치했고 콘솔 찍고 확인도 해봤을 때 데이터가 있었는데 계속 목록이 없다고 나오는 것입니다!
서버사이드에서 패치한 후 렌더링하기 때문에 즉시 목록이 가져와져야 되는데...
mock api 핸들러로 api 인터셉트도 잘 했고 데이터도 잘 가져왔는데 왜 안되는거지 싶다가
api데이터를 무엇으로 관리하는지 잊고있었다는 것을 알게되었다.
react-query
// _app.tsx
<QueryClientProvider client={queryClient}>
<Hydrate state={pageProps.dehydratedState}>
<Layout>
<Component {...pageProps} />
</Layout>
<ReactQueryDevtools initialIsOpen={false} />
</Hydrate>
</QueryClientProvider>
// _ssrAspect
return {
props: {
...propsAspect,
hasAuth: !!user,
dehydratedState: dehydrate(queryClient),
},
};
SSR 또는 SSG에서 데이터를 prefetch 또는 fetch하고 이를 클라이언트에서 사용할 수 있게 해주는 작업을 한다.
dehydrate
캐시된 데이터를 직렬화하여 캐시 저장소에 JSON으로 직렬화하여 저장한다.
hydrate
캐시 저장소에 직렬화된 캐시 데이터를 읽어와 react-query의 캐시를 복원한다.
즉 서버사이드에서 dehydrate
로 fetch한 api 데이터를 직렬화해 저장하고 앱(클라이언트사이드)에서 hydrate
로 복원해서 데이터를 읽어와 캐시로 구성하는 형태이다.
이런 방법으로 서버사이드에서 사전에 조회한 데이터를 클라이언트에서 useQuery
를 사용해도 다시 패치하지 않고 동기화해서 그대로 사용하는 것이다.
그래서 문제는
jest
환경에서 react-query
를 사용하는 컴포넌트를 테스트할 수 있는 render
함수를 만들어 사용하고 있었다.
function renderWithQueryClient(ui, options, client) {
const queryClient = client ?? generateQueryClient();
return render(<QueryClientProvider client={queryClient}>{ui}</QueryClientProvider>, { wrapper: Wrapper, ...options });
}
_app.tsx
에서 했던 것 처럼 캐시 저장소에 직렬화된 데이터를 가지고 동기화 해주는 부분이 없기 때문에 컴포넌트를 렌더링 했을 때 데이터가 전달되지 않았던 것이다!
설정 한 번 해두고 그냥 구현만 했기 때문에 원리를 간과하고 있었다.
hydrate로 캐시 데이터 동기화 설정하기
const { props } = (await getServerSideProps(mockedContext)) as any;
expect(props).toHaveProperty('categoryId', 'cid24');
expect(props).toHaveProperty('isMine', true);
expect(props).toHaveProperty('dehydratedState');
const queryClient = new QueryClient();
hydrate(queryClient, props.dehydratedState);
renderQuery(<CategoryById categoryId={props.categoryId} isMine={props.isMine} />, undefined, queryClient);
const addButton = screen.getByRole('button', {
name: /추가하기/i,
});
expect(addButton).toBeInTheDocument();
const noListText = screen.queryByText(/목록이 존재하지 않습니다/i);
expect(noListText).not.toBeInTheDocument();
getServerSideProps
의 결과(props
)로 dehydrated
된 캐시 상태를 얻게 된다.
이 것을 hydrate
하여 렌더링 할 컴포넌트로 넘겨주면 된다.
renderQuery
함수에서 queryClient
를 생성해서 넘길 수 있게 구성해서 저렇게 했지만
계속해서 이렇게 사용해야하는 상황이 있다면 renderQuery
함수에서 옵션으로 dehydratedState
를 받을 수 있게 설정을 해주는 것이 재사용성 면에서 좋을 것 같다!
function renderWithQueryClient(ui, options, client, dehydratedState) {
const queryClient = client ?? generateQueryClient();
if (dehydratedState) {
hydrate(queryClient, dehydratedState);
}
return render(<QueryClientProvider client={queryClient}>{ui}</QueryClientProvider>,
{ wrapper: Wrapper, ...options });
}