서버 컴포넌트 기반 Next.js App Router에서 데이터를 미리 패칭한 뒤, TanStack Query를 통해 클라이언트 상태로 하이드레이션하여 사용하는 방법과 그 구조적 배경에 대해 정리한다.
- 이 문서에서는 TanStack Query와 React Query를 동일한 의미로 사용한다.
- TanStack Query는 React Query의 후속 이름이며, 현재는 공식적으로 통합된 명칭이다.
TanStack Query는 기본적으로 CSR(Client Side Rendering) 기반의 상태 관리 도구이다. 반면, Next.js App Router는 SSR(Server Side Rendering)과 Server Components에 기반한 프레임워크이다.
이 둘이 결합되면, 데이터를 언제/어디서/어떻게 패칭하고, 이를 클라이언트에서 어떤 방식으로 재사용할 것인지에 대한 구조적 충돌이 발생한다.
이 충돌은 특히 서버에서 미리 패칭한 데이터를 클라이언트에서도 동일하게 캐시된 상태로 사용하고자 할 때 가장 두드러진다.
💡 TTFB (Time To First Byte)
브라우저가 서버에 요청을 보내고 나서 첫 번째 바이트를 받을 때까지 걸리는 시간을 의미한다.
짧은 TTFB는 서버가 빠르게 응답했다는 것을 의미하며, 웹사이트 성능이 좋다는 신호로 해석된다.
async
Server Component 내부에서 await fetch()
또는 쿼리 함수를 호출해 데이터를 가져온다.useQuery()
를 호출해도 이 데이터를 "이미 존재하는 캐시"로 인식하지 못한다.useQuery()
가 호출될 때, 해당 queryKey
로 캐시된 데이터가 없으면 자동으로 fetch를 시작한다.HydrationBoundary
를 이용한 공식 프리패칭 전략// app/page.tsx
import {
dehydrate,
HydrationBoundary,
QueryClient,
} from '@tanstack/react-query'
import { queryKey, fetchData } from '@/lib/api';
export default async function Page() {
const queryClient = new QueryClient()
await queryClient.prefetchQuery({
queryKey,
queryFn: fetchData,
});
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<ClientComponent />
</HydrationBoundary>
);
}
getQueryClient()
는 서버에서만 작동하는 독립적인 queryClient 인스턴스를 반환한다. 이는 요청 간 데이터 공유를 방지한다.prefetchQuery()
는 서버에서 데이터를 미리 가져와 queryClient의 캐시에 저장한다.dehydrate(queryClient)
는 이 queryClient의 상태를 직렬화 가능한 형태로 변환하여 HydrationBoundary
에 전달할 수 있게 만든다.HydrationBoundary
는 클라이언트에서 해당 데이터를 초기 캐시 상태로 인식하게 만든다.ClientComponent
내의 useQuery()
는 서버에서 미리 가져온 데이터가 캐시에 있다고 인식하고 fetch를 생략한다.클라이언트 렌더링 기반의 일반적인 요청 흐름은 다음과 같다.
1. |-> 마크업 (데이터 없음)
2. |-> JavaScript 로드
3. |-> useQuery 실행 → fetch
초기 렌더링에 필요한 데이터가 클라이언트에서 다시 요청되기 때문에, 사용자에게 콘텐츠가 보이기까지 최소 3단계의 네트워크 왕복이 필요하다.
반면, SSR을 적용하면 다음과 같은 구조로 최적화할 수 있다.
1. |-> 마크업 (초기 데이터 포함)
2. |-> JavaScript 로드 및 하이드레이션
초기 데이터가 이미 마크업에 포함되어 있으므로, 클라이언트는 추가 fetch 없이 즉시 콘텐츠를 렌더링할 수 있다.
HydrationBoundary
를 활용한 프리패칭 전략은 단순한 코드 최적화 기법이 아니라, 서버 중심 렌더링(SSR)과 클라이언트 중심 캐시 시스템을 자연스럽게 연결하기 위한 핵심적인 기법이다. 이를 정확히 이해하기 위해서는 React Query와 SSR이 어떤 방식으로 통합되는지 개념적으로 살펴볼 필요가 있다.
TanStack Query 공식 문서에서는 이 과정을 다음의 세 단계로 설명하고 있다.
서버에서 데이터를 사전 패칭(prefetch)
React Query의 prefetchQuery()
를 통해, 서버에서 필요한 데이터를 먼저 가져온다.
가져온 데이터를 직렬화 가능한 형태로 변환(dehydrate)
dehydrate()
함수를 사용해 React Query의 내부 캐시 상태를 직렬화할 수 있는 형태로 변환한다. 이 데이터는 HTML 마크업과 함께 클라이언트로 전달된다.
클라이언트에서 캐시 복원(hydrate)
클라이언트는 HydrationBoundary
를 통해 이 데이터를 React Query의 초기 캐시로 인식하고, 동일한 쿼리를 다시 요청하지 않는다.
이 전략의 핵심은 초기 페이지 렌더링 시점에 데이터를 포함한 마크업을 제공함으로써, 클라이언트가 추가 요청 없이 콘텐츠를 바로 렌더링할 수 있도록 하는 데 있다.