무엇
HydrationBoundary는 서버에서 미리 받아 둔 쿼리 캐시를 클라이언트의 QueryClient에 부어 넣는 컴포넌트다. SSR에서 서버가 채운 데이터를 브라우저가 그대로 이어받게 한다.
state prop에 dehydrate()로 직렬화한 캐시 상태를 넘기면, 그 아래 컴포넌트들이 다시 요청하지 않고 서버 데이터를 곧장 쓴다.
무슨 일이 일어나나
SSR에서는 서버가 쿼리를 실행해 캐시를 채우고, dehydrate()로 그 캐시를 직렬화 가능한 형태(DehydratedState)로 바꿔 HTML에 실어 보낸다. 브라우저에서 HydrationBoundary가 그 state를 받아 자신의 QueryClient에 병합한다. 이미 캐시에 값이 있으면 업데이트 시각을 비교해 더 최신인 쪽을 남긴다.
병합 대상은 쿼리다. 뮤테이션은 HydrationBoundary로 하이드레이트하지 않는다.
사용법
props는 state(DehydratedState, 필수)와 options(HydrateOptions, 선택)다. options에는 하이드레이트되는 쿼리에 적용할 defaultOptions와, context 대신 쓸 queryClient를 줄 수 있다.
import { HydrationBoundary, type DehydratedState } from '@tanstack/react-query'
import { Posts } from './Posts'
export function Page({ state }: { state: DehydratedState }) {
return (
<HydrationBoundary state={state}>
<Posts />
</HydrationBoundary>
)
}
state는 서버에서 dehydrate로 만든다.
import { dehydrate } from '@tanstack/react-query'
// 서버: 캐시를 직렬화한다
const state = dehydrate(queryClient)
실무 예시
Next.js App Router에서 서버 컴포넌트가 데이터를 미리 받고 클라이언트가 이어받는 구성이다. 서버에서 prefetch한 뒤 dehydrate한 상태를 HydrationBoundary로 감싸 내려보낸다.
import {
dehydrate,
HydrationBoundary,
QueryClient,
} from '@tanstack/react-query'
import { Posts } from './Posts'
export default async function PostsPage() {
const queryClient = new QueryClient()
await queryClient.prefetchQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
})
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<Posts />
</HydrationBoundary>
)
}
왜 중요한가
SSR이나 RSC에서 서버가 받은 데이터를 클라이언트가 다시 받지 않게 하는 표준 방법이다. 이게 없으면 서버가 렌더한 화면을 브라우저가 다시 요청하며 깜빡이거나, 서버 데이터를 버리게 된다. Next.js App Router처럼 서버·클라이언트가 나뉜 환경에서 React Query를 제대로 쓰려면 거의 반드시 거치는 컴포넌트다. 순수 클라이언트 앱에서는 쓸 일이 없다.
Reference