고민의 발단

gloddy는 다음과 같이 페이지에서 사용하는 쿼리를 모아두고 HydrationProvider를 통해서 일괄 처리를 하는 방식이다.

interface MeetingPageProps {
  params: {
    lng: string;
  };
}

export default function MeetingPage({ params: { lng } }: MeetingPageProps) {
  return (
    <>
      <MeetingParticipateHeader />
      <QueryAsyncBoundary rejectedFallback={RejectedFallback}>
        <HydrationProvider
          queryMultipleFn={[
            getMeetingParticipating,
            getMeetingHosting,
            getMeetingRejected,
            getMeetingNotEstimated,
          ]}
          queryMultipleKey={[
            Keys.getMeetingParticipating(),
            Keys.getMeetingHosting(),
            Keys.getMeetingRejected(),
            Keys.getMeetingNotEstimated(),
          ]}
        >
          <ContentSection />
        </HydrationProvider>
      </QueryAsyncBoundary>
      <Footer page="meeting" lng={lng} />
    </>
  );
}

우리의 의문점은 이렇게 모아서 수행하는게 성능상 문제를 야기하는가? 였다.

왜 묶어서 처리하는가?

HydrationProvider의 동작을 분석해보자

export default async function HydrationProvider({
  children,
  queryKey,
  queryMultipleKey,
  queryFn,
  queryMultipleFn,
  isInfiniteQuery = false,
}: StrictPropsWithChildren<HydrationProviderProps>) {
  const getQueryClient = cache(() => new QueryClient()); // QueryClient 싱글톤 인스턴스를 검색.

  const queryClient = getQueryClient();

  if (queryMultipleFn && queryMultipleKey) {
    await Promise.all(
      queryMultipleFn.map((queryFn, index) => {
        return queryClient.prefetchQuery(queryMultipleKey[index], queryFn);
      })
    );
  }

  if (queryFn && queryKey) {
    if (isInfiniteQuery) await queryClient.prefetchInfiniteQuery(queryKey, queryFn);
    else await queryClient.prefetchQuery(queryKey, queryFn);
  }
  
  // queryClient의 prefetchQuery를 통해서 데이터를 미리 가져오고 완료까지 기다림

  const dehydratedState = dehydrate(queryClient); //'dehydrate' 를 사용 하여 쿼리 캐시에서 pre-fetch 된 쿼리의 hydrate State를 가져옴.

  return <Hydrate state={dehydratedState}>{children}</Hydrate>; // pre-fetch 된 쿼리가 필요한 컴포넌트 트리를 래핑하고 dehydrate상태를 제공
}

이런 방식은 app 디렉토리에서 사용가능한 initialData or <Hydrate> 2가지 방법중 Hydrate 사용한 방법이다.

SSR | TanStack Query Docs

Hydrate 컴포넌트의 목적은 서버 사이드에서 미리 가져온 데이터를 클라이언트 사이드에 "재수화(hydrating)"

하기 위한 컴포넌트로 이 컴포넌트의 주요 목적은 서버 사이드 렌더링(SSR) 환경에서 서버에서 생성된 데이터의 상태를 클라이언트 사이드로 전달하고, 이를 클라이언트 사이드의 React Query 상태에 통합하는 것이 목적.

공식문서에 따르면 아래와 같이 하위 컴포넌트는 서버측 렌더링이 수행중일 때 클라이언트 구성요소의 중첩된 useQuery의 호출은 Hydrate가 미리 가져온 데이터에 대한 엑세스를 의미한다고 한다.

'use client'

import { useQuery } from '@tanstack/react-query'

export default function Posts() {
  // 이 useQuery는 "HydrationProvider" 컴포넌트의 어떤 하위 자식 컴포넌트에서도 발생할 수 있으며,
  // 어느 경우에든 데이터는 즉시 사용 가능합니다
  const { data } = useQuery({ queryKey: ['posts'], queryFn: getPosts })
}

아래는 내가 SSR의 이해가 부족할 때 생각했던 제안.

이 방식으로 pre-fetch된 데이터를 가져올 수 있지만 만약 pre-fetch가 실패를 하거나 해서 캐시가 안되어 있다면 데이터를 불러오지 않아 대부분의 경우 캐시 조회 및 네트워크 요청을 수행할 수 있는 useQuery를 사용하는게 좋다고 한다.

스크린샷 2024-01-15 20.00.52.png