고민의 발단
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 사용한 방법이다.
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를 사용하는게 좋다고 한다.
내 생각에는 Hydrate를 적절히 잘 사용하고 있다고 생각한다.
그래서 묶어서 처리하는게 좋은가?
나는 좋다고 생각한다.
필요한 데이터들을 pre-fetch하기에 유저가 빈 화면을 보게되긴 하겠지만 Loding 인디케이터와 같은 부분으로 매꿀 수 있는 부분이라고 생각하고
-
순차적 데이터 가져오기를
사용하면 경로의 요청이 서로 종속되므로 폭포가 생성됩니다. 한 가져오기가 다른 가져오기의 결과에 따라 달라지기 때문에 이 패턴을 원하거나 리소스를 절약하기 위해 다음 가져오기 전에 조건이 충족되기를 원하는 경우가 있을 수 있습니다. 그러나 이 동작은 의도하지 않은 것일 수도 있으며 로딩 시간이 길어질 수도 있습니다.
-
병렬 데이터 가져오기를
사용하면 경로의 요청이 즉시 시작되고 동시에 데이터가 로드됩니다. 이렇게 하면 클라이언트-서버 폭포수와 데이터를 로드하는 데 걸리는 총 시간이 줄어듭니다.
next js 공식 문서에는 이렇게 설명이 되어있고 내 생각에도 묶어서 처리하는게 좋은것 같다.