[nextjs] ssr에서 react-query로 가져온 데이터로 페이지 컴포넌트 테스트하기

pds·2023년 4월 17일
0

TIL

목록 보기
50/60

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 });
}
profile
강해지고 싶은 주니어 프론트엔드 개발자

0개의 댓글