서버와 클라이언트 사이에서 실제로 일어나는 일
Next.js에서 React Query를 함께 사용할 때 가장 많이 오해되는 부분이 Hydrate와 Dehydrate입니다.
서버에서 데이터를 미리 가져오고, 클라이언트에서 그 상태를 복원한다는 개념까지는 대부분 알고 있지만, 실제로 어떤 형태로 데이터가 전달되는지, JSON.stringify()가 어디에서 일어나는지 정확히 이해하는 사람은 많지 않습니다.
이 글에서는 그 과정을 코드 단위로 분명히 짚어보겠습니다.
React Query에서의 SSR 개념
React Query는 기본적으로 클라이언트 캐싱 라이브러리지만, Next.js의 SSR(Server-Side Rendering)과 조합하면 초기 데이터를 서버에서 미리 가져와서 렌더링하고, 클라이언트에서는 그 데이터를 그대로 이어받아 사용할 수 있습니다.
이 구조의 핵심은 두 가지입니다.
dehydrate(): 서버에서 React Query 캐시를 직렬화 가능한 형태로 변환Hydrate: 클라이언트에서 그 상태를 역직렬화하여 복원
이 과정을 통해 서버에서 미리 가져온 데이터를 클라이언트가 네트워크 요청 없이 바로 사용할 수 있게 됩니다.
코드 예시
// pages/_app.tsx
import { useState } from 'react';
import { Hydrate, QueryClient, QueryClientProvider } from '@tanstack/react-query';
export default function MyApp({ Component, pageProps }) {
const [queryClient] = useState(() => new QueryClient());
return (
<QueryClientProvider client={queryClient}>
<Hydrate state={pageProps.dehydratedState}>
<Component {...pageProps} />
</Hydrate>
</QueryClientProvider>
);
}
// pages/posts/[id].tsx
import { dehydrate, QueryClient } from '@tanstack/react-query';
import { getPost } from '@/lib/api';
import { useQuery } from '@tanstack/react-query';
export const getServerSideProps = async (ctx) => {
const { id } = ctx.params;
const queryClient = new QueryClient();
await queryClient.fetchQuery(['post', id], () => getPost(id));
return {
props: {
dehydratedState: dehydrate(queryClient),
},
};
};
export default function PostPage({ id }) {
const { data } = useQuery(['post', id], () => getPost(id));
return <div>{data.title}</div>;
}
Dehydrate와 Hydrate의 실제 의미
많은 글에서 dehydrate를 직렬화, hydrate를 역직렬화라고 단순히 설명하지만, React Query 내부 동작은 조금 다릅니다.
dehydrate()는 React Query 내부 캐시를 JSON으로 직렬화할 수 있는 JS 객체로 변환하는 함수입니다.
즉, Map이나 함수, 클래스 인스턴스 같은 직렬화 불가능한 데이터를 제거하고, 순수한 객체 형태로 변환합니다.
아직 문자열은 아닙니다.
공식 문서에서도 이렇게 설명되어 있습니다.
“This result is not in serialized form, you need to do that yourself if desired.”
(이 결과는 직렬화된 형태가 아니며, 필요하다면 직접 직렬화해야 한다.)
따라서 dehydrate()가 문자열을 반환하는 것은 아닙니다.
단지 “직렬화 가능한 객체를 반환”할 뿐입니다.
실제 문자열 변환은 Next.js가 담당한다
React Query는 객체 형태의 데이터를 반환합니다.
그런데 브라우저가 받는 시점에는 JSON 문자열이 되어 있죠.
그 이유는 Next.js가 getServerSideProps의 반환값을 HTML 안에 넣을 때 내부적으로 JSON.stringify()를 수행하기 때문입니다.
즉, 서버에서는 JS 객체 형태였던 데이터가
HTML 렌더링 단계에서 문자열로 변환되어 <script id="__NEXT_DATA__"> 태그 안에 삽입됩니다.
브라우저에서 페이지 소스를 보면 다음과 같습니다.
<script id="__NEXT_DATA__" type="application/json">
{
"props": {
"pageProps": {
"dehydratedState": {
"queries": [
{
"queryKey": ["post",123],
"state": { "data": {"title":"Hello SSR"}, "status":"success" }
}
]
}
}
}
}
</script>
Next.js가 자동으로 JSON.stringify()를 호출해 넣은 결과입니다.
브라우저가 hydration을 진행할 때 이 문자열을 JSON.parse()로 다시 객체로 복원하고,
그 객체가 pageProps.dehydratedState로 전달됩니다.
React Query의 <Hydrate>는 이 데이터를 받아서 캐시에 병합합니다.
데이터 흐름 요약
| 단계 | 형태 | 설명 |
|---|---|---|
dehydrate(queryClient) | JS 객체 | React Query 캐시를 JSON-safe 객체로 변환 |
| Next.js HTML 렌더링 | JSON 문자열 | 내부적으로 JSON.stringify() 수행 |
| 브라우저에서 hydration | JS 객체 | Next.js가 JSON.parse()로 복원 |
<Hydrate state={...}> | React Query 캐시 병합 | 캐시에 복원 완료 |
언제 SSR + Hydration을 써야 할까
이 구조는 모든 페이지에 적용하면 비효율적입니다.
적용 여부는 서비스의 성격에 따라 결정해야 합니다.
SSR + Hydration을 사용하는 게 좋은 경우
- SEO가 중요한 페이지 (블로그, 뉴스, 상품 상세 등)
- 첫 화면 렌더링 속도가 중요한 랜딩 페이지
- 공개형 콘텐츠 중심 서비스
굳이 사용할 필요가 없는 경우
- 로그인 이후 개인화된 화면, 관리자 페이지
- 데이터가 자주 바뀌는 실시간 영역
- 내부 탐색용 CSR 페이지
서버 부하를 줄이는 운영 원칙
- SSR은 필요한 페이지만 최소 적용
SEO나 초기 렌더링이 중요한 페이지만 SSR로 처리하고, 나머지는 ISR이나 CSR로 전환한다. - 필요한 데이터만 Prefetch
한 페이지에서 여러 쿼리를 동시에 SSR로 처리하지 않는다.
화면에 꼭 필요한 핵심 쿼리만 prefetch한다. - Hydration 후 재요청 방지
이미 서버에서 받아온 데이터를 클라이언트에서 다시 요청하지 않도록 설정한다.useQuery(['post', id], getPost, { staleTime: 30_000, refetchOnWindowFocus: false, }); - 캐시 계층 적극 활용
revalidate 옵션으로 정적 페이지를 자동 갱신하고,
CDN이나 CloudFront 같은 외부 캐시를 적극 활용한다.
React Query v4 이후의 변화
React Query v3에서는 서버 캐시의 cacheTime이 기본 5분으로 설정되어 있어,
SSR 요청마다 메모리 사용량이 쌓일 수 있었습니다.
v4부터는 서버 캐시의 cacheTime이 Infinity로 변경되어,
요청이 끝나면 즉시 캐시가 정리됩니다.
결론
정리하자면 다음과 같습니다.
dehydrate()는 문자열을 반환하지 않는다.
React Query 캐시를 JSON으로 직렬화 가능한 JS 객체 형태로 변환한다.JSON.stringify()는 Next.js가 HTML 렌더링 시 자동으로 수행한다.- 브라우저는 이 문자열을 JSON.parse()로 다시 객체로 복원한다.
<Hydrate>는 이 객체를 React Query 캐시에 병합한다.- SSR은 SEO나 초기 렌더링 성능이 중요한 페이지에만 선택적으로 적용하는 것이 좋다.
이 과정을 정확히 이해하면, SSR과 CSR의 경계를 명확히 나눌 수 있고,
불필요한 서버 부하 없이 React Query의 캐싱 이점을 온전히 활용할 수 있습니다.

댓글 남기기