트러블슈팅

[MeranGo] 무한스크롤 구현 중 queryKey로 인한 데이터 누락

stopbrother 2025. 12. 11. 17:21

증상

TanStack Query의 Infinite Queries사용하여 커서 기반 무한스크롤을 적용하고
목록 페이지에서 데이터를 불러오지 못하는 현상이 발생했다.
이전에는 useQuery로 목록 조회를 구현했을때는 정상적으로 동작했지만 useInfiniteQuery적용 후 문제가 발생했다.

원인

API함수 내에 콘솔을 찍어보고 ReactDevTools로 쿼리키가 어떻게 동작하는지 비교해보았다.

// api/party-api.ts
export const getInfiniteParties = async ({
  client,
  partyType,
  keyword,
  cursor,
  limit = 15,
}: getPartiesParams) => {
  console.log('query', { partyType, keyword, cursor, limit });
  ...

API 내에 콘솔은 keywordundefined로 찍히고 있었고 ReactDevTools에서는 null이 들어오고 있었다.

내가 의도한건 keyword가 없을 경우 undefined로 캐싱하는것이였다.
하지만 TanStack Query는 dehydrate를 통해 서버 쿼리 캐시를 JSON으로 직렬화해 클리언트로 넘겨주는데 undefined는 JSON으로 표현할 수 없기 때문에 undefinednull로 바뀌게 되면서 서버에서 저장한 keywordundefined가 되고 클라이언트에서는 null이 사용이 되어 서로 다른 값이 사용이 되기 때문에 문제가 생겼던것이다.
특히 useInfiniteQuery는 같은 키 아래 pages 스택을 유지하기 때문에 키가 불일치하면 다른 쿼리로 인식하게 된다.

즉 서버에서 prefetch한 데이터가 클라이언트에서 재사용되지 못해 무한스크롤의 첫 페이지 데이터가 비워져있는 문제가 발생한것이다.

해결

원인은 결국 검색어가 없을때 표현이 서버와 클라이언트에서 일관되지 않았다는 것이었고 이를 해결하기 위해

처음부터 searchParams.keyword를 사용할때 항상 문자열로 고정하도록 하여 해결하였다.

const RecruitPage = async ({ searchParams }: RecruitPageProps) => {
  const partyType = searchParams.partyType ?? 'all';
  const keyword = searchParams.keyword ?? '';

  const serverClient = createClient();
  const queryClient = new QueryClient();

  await queryClient.prefetchInfiniteQuery({
  queryKey: ['recruits', partyType, keyword],
  ...

    return (
    <HydrationBoundary state={dehydrate(queryClient)}>
      <PartyRecruitList partyType={partyType} keyword={keyword} />
    </HydrationBoundary>
  );

이렇게 하여 서버 prefetch시 keyword는 ''로 캐싱되고 직렬화된 캐시에서도 ''가 사용되기에 동일한 queryKey가 사용이 된다.
이후 콘솔과 ReactQueryDevtools에서 확인한 queryKey는 항상 일치하게 되면서 무한스크롤 기능도 동작하며 데이터가 정상적으로 노출되었다.

회고

이번 경험으로 "없음" 상태를 모두 동일한 값으로 표현하는것이 중요하다는 사실을 깨달았다. undefined는 json 직렬화시 null로 바뀐다는 사실을 알았고 ReactDevTools와 API 콘솔 로그를 같이 확인하면 디버깅에 유용하다는 것을 체감할 수 있었다.