이 글에서 정리하는 내용
TanStack Query Hydration 오류는 서버에서 prefetch한 데이터와 클라이언트 QueryClient가 같은 캐시를 공유하지 못할 때 자주 생깁니다. QueryClient 생성 위치, dehydrate/HydrationBoundary, queryKey 일치, Provider 배치를 같이 확인해야 합니다.
HydrationBoundary 문제를 고친 뒤에도 데이터가 갱신되지 않는다면 TanStack Query 화면 갱신 문제 해결에서 query invalidation과 캐시 갱신 흐름을 이어서 점검하세요.
- 내 증상이 이거면 여기부터 보세요
- 먼저 적용할 핵심 수정 코드
- 왜 이런 오류가 생기는가
- 실제 작업에서 점검하는 순서
- 그래도 안 될 때 볼 예외 케이스
- 다음에 같은 문제를 줄이는 체크리스트
내 증상이 이거면 여기부터 보세요

Next.js에서 TanStack Query를 붙이면 서버에서 데이터를 미리 가져오고 클라이언트에서 이어받는 구조가 됩니다. 이때 QueryClient를 매 렌더마다 새로 만들거나 queryKey가 조금만 달라져도 서버 캐시가 클라이언트에서 이어지지 않습니다.
| 증상 | 실제 에러 메시지 | 먼저 볼 위치 | 바로 해볼 조치 | 이동할 섹션 |
|---|---|---|---|---|
| Provider 오류 | No QueryClient set | QueryClientProvider | 앱 루트 Provider 확인 | 핵심 수정 코드 |
| 서버/클라이언트 내용 불일치 | Hydration failed | HydrationBoundary | dehydrate state 전달 확인 | 핵심 수정 코드 |
| prefetch는 했는데 다시 요청 | query refetches | queryKey | 서버/클라이언트 queryKey 일치 | 왜 생기는가 |
| pending query가 깨짐 | dehydrated as pending | 비동기 에러 처리 | prefetch 에러와 staleTime 확인 | 예외 케이스 |
오류 해결 글에서는 실제 에러 문구가 본문에 있어야 검색해서 다시 찾기 쉽습니다. 프로젝트명, 사용자명, 토큰, 절대 경로처럼 민감하거나 불필요한 값은 빼고 대표 문구만 남깁니다.
Hydration failed because the initial UI does not match what was rendered on the server.
No QueryClient set, use QueryClientProvider to set one
A query that was dehydrated as pending ended up rejecting
Text content does not match server-rendered HTML
먼저 적용할 핵심 수정 코드
원인 설명을 오래 읽기 전에 아래 설정부터 현재 코드와 대조해보세요. Provider에서 QueryClient를 안정적으로 만들고, 서버 컴포넌트에서 prefetchQuery 후 dehydrate 결과를 HydrationBoundary에 넘깁니다. 중요한 것은 오류를 덮는 옵션을 추가하는 것이 아니라, 실행 환경과 설정 파일이 같은 기준으로 동작하게 만드는 것입니다.
클라이언트 Provider 설정
"use client"
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import { useState } from "react"
export function QueryProvider({ children }: { children: React.ReactNode }) {
const [queryClient] = useState(() => new QueryClient())
return (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
)
}
클라이언트에서 QueryClient가 렌더마다 새로 만들어지지 않도록 useState 초기화 함수를 사용합니다.
서버 prefetch와 HydrationBoundary
import { HydrationBoundary, QueryClient, dehydrate } from "@tanstack/react-query"
export default async function Page() {
const queryClient = new QueryClient()
await queryClient.prefetchQuery({
queryKey: ["posts"],
queryFn: getPosts,
})
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<PostList />
</HydrationBoundary>
)
}
서버에서 prefetch한 queryKey와 클라이언트 useQuery의 queryKey가 같아야 캐시가 이어집니다.
왜 이런 오류가 생기는가
Hydration은 서버에서 만든 HTML과 클라이언트가 처음 그린 화면을 맞추는 과정입니다. TanStack Query 데이터가 서버와 클라이언트에서 다르게 들어오면 React는 같은 화면이라고 판단하지 못합니다.
QueryClient는 캐시 저장소입니다. 클라이언트 컴포넌트가 렌더될 때마다 QueryClient를 새로 만들면 방금 받은 캐시를 스스로 버리는 구조가 됩니다.
HydrationBoundary는 서버에서 dehydrate한 캐시를 클라이언트 영역으로 넘기는 경계입니다. Provider만 있다고 자동으로 서버 prefetch 결과가 연결되는 것은 아닙니다.
실제 작업에서 점검하는 순서
첫 번째로 오류가 나온 단계를 나눕니다. 개발 서버에서만 보이는지, 빌드에서 실패하는지, 배포나 CI에서만 실패하는지에 따라 봐야 할 파일이 달라집니다. 같은 메시지라도 실행 위치가 다르면 원인도 다를 수 있습니다.
두 번째로 한 번에 여러 설정을 바꾸지 않습니다. TanStack Query Hydration 오류를 해결하다 보면 관련 파일을 전부 고치고 싶어지지만, 그러면 어떤 변경이 실제 해결책이었는지 알기 어렵습니다. 핵심 설정 하나를 바꾸고 검증 명령을 실행한 뒤 다음 설정으로 넘어가야 합니다.
세 번째로 저장소에 남는 기준으로 정리합니다. 개인 PC에서만 통과하는 임시 조치가 아니라 설정 파일, 패키지 버전, 배포 설정처럼 팀원이 같은 기준으로 재현할 수 있는 형태가 되어야 합니다.
그래도 안 될 때 볼 예외 케이스
기본 수정 후에도 같은 메시지가 남는다면 캐시, 버전 차이, 경로 대소문자, 실행 위치를 같이 봐야 합니다. 오류 메시지는 하나처럼 보여도 실제로는 개발 서버, 타입 검사, 번들러, 배포 환경이 서로 다른 설정을 읽어서 생기는 경우가 많습니다.
- Next.js App Router에서 Provider는 클라이언트 컴포넌트여야 합니다.
- staleTime이 너무 짧으면 hydration 직후 즉시 refetch되어 깜빡임이 생길 수 있습니다.
- queryFn 안에서 window나 localStorage를 바로 읽으면 서버 prefetch 단계에서 실패합니다.
다음에 같은 문제를 줄이는 체크리스트

TanStack Query Hydration 문제는 Provider, QueryClient, HydrationBoundary, queryKey가 한 세트로 맞아야 줄어듭니다. 한 군데만 고치면 해결된 것처럼 보이다가 다른 페이지에서 같은 문제가 반복될 수 있습니다.
- QueryClientProvider가 앱 루트에 있는지 확인합니다.
- 클라이언트 QueryClient를 렌더마다 새로 만들지 않습니다.
- prefetchQuery와 useQuery의 queryKey를 같은 배열로 맞춥니다.
- dehydrate 결과를 HydrationBoundary state로 넘깁니다.
결국 TanStack Query Hydration 오류는 한 줄짜리 우회 코드보다 확인 순서가 중요합니다. 에러 문구를 단계별로 나누고, 설정 파일과 실행 명령을 같은 기준으로 맞추면 같은 문제를 훨씬 짧게 끝낼 수 있습니다.