QueriesHydration
<QueriesHydration/>는 실험 기능이므로 이 인터페이스는 변경될 수 있습니다.
서버 컴포넌트에서 여러 개의 쿼리를 미리 불러오고 클라이언트 컴포넌트에 자동으로 hydrate하는 컴포넌트입니다.
React Server Components를 사용하는 환경에서 서버 측에서 데이터를 미리 불러와 클라이언트로 전달할 때 유용합니다.
💡 추천 읽기: 이 컴포넌트를 사용하기 전에 TanStack Query Advanced Server Rendering 가이드 를 읽어보시는 것을 권장합니다. 이 가이드는 Server Components, streaming, 그리고 Next.js App Router에 대해 다룹니다. 이러한 개념을 이해하면
QueriesHydration를 훨씬 더 편하게 사용할 수 있습니다.
기본 사용법
QueriesHydration에서 queryOptions를 사용하여 서버에서 데이터를 미리 불러올 수 있습니다. HTML Streaming을 최대화하려면 각 컴포넌트를 별도의 Suspense와 QueriesHydration으로 감싸는 것이 좋습니다:
page.tsx (RSC)
import { QueriesHydration } from '@suspensive/react-query'
import { Suspense } from '@suspensive/react'
import { userQueryOptions, postsQueryOptions } from './queries'
import { UserProfile, PostList } from './_components'
// 서버 컴포넌트
const PostsPage = ({ userId }) => {
return (
<>
<Suspense fallback={<div>Loading user...</div>}>
<QueriesHydration queries={[userQueryOptions(userId)]}>
<UserProfile userId={userId} />
</QueriesHydration>
</Suspense>
<Suspense fallback={<div>Loading posts...</div>}>
<QueriesHydration queries={[postsQueryOptions(userId)]}>
<PostList userId={userId} />
</QueriesHydration>
</Suspense>
</>
)
}Infinite Query 사용하기
QueriesHydration에서 infiniteQueryOptions도 사용할 수 있습니다. 일반 쿼리와 infinite 쿼리를 같은 배열에 섞어서 사용할 수 있습니다. HTML Streaming을 최대화하려면 각 컴포넌트를 별도의 Suspense와 QueriesHydration으로 감싸는 것이 좋습니다:
import { QueriesHydration } from '@suspensive/react-query'
import { Suspense } from '@suspensive/react'
import { queryOptions, infiniteQueryOptions } from '@tanstack/react-query'
import { UserProfile, InfinitePostList } from './_components'
const userQueryOptions = (userId: string) =>
queryOptions({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
})
const postsInfiniteQueryOptions = (userId: string) =>
infiniteQueryOptions({
queryKey: ['posts', userId],
queryFn: ({ pageParam = 0 }) => fetchPosts(userId, pageParam),
getNextPageParam: (lastPage, allPages) => lastPage.nextCursor,
initialPageParam: 0,
})
// 서버 컴포넌트
const PostsPage = ({ userId }: { userId: string }) => {
return (
<>
<Suspense fallback={<div>Loading user...</div>}>
<QueriesHydration queries={[userQueryOptions(userId)]}>
<UserProfile userId={userId} />
</QueriesHydration>
</Suspense>
<Suspense fallback={<div>Loading posts...</div>}>
<QueriesHydration queries={[postsInfiniteQueryOptions(userId)]}>
<InfinitePostList userId={userId} />
</QueriesHydration>
</Suspense>
</>
)
}Props
queries
서버에서 미리 불러올 쿼리 또는 infinite 쿼리들의 배열입니다. 각 쿼리는 queryKey를 필수로 가져야 합니다. 일반 쿼리와 infinite 쿼리를 같은 배열에 섞어서 사용할 수 있습니다.
type QueriesHydrationProps = {
queries: (
| WithRequired<QueryOptions<any, any, any, any>, 'queryKey'>
| WithRequired<UseInfiniteQueryOptions<any, any, any, any, any>, 'queryKey'>
)[]
children: ReactNode
queryClient?: QueryClient // 선택사항 (기본값: new QueryClient())
skipSsrOnError?: boolean | { fallback: ReactNode } // 선택사항 (기본값: true)
timeout?: number // 선택사항 (단위: ms)
}queryClient
선택적으로 사용할 QueryClient 인스턴스를 전달할 수 있습니다. 전달하지 않으면 새로운 QueryClient 인스턴스가 생성됩니다.
import { QueryClient } from '@tanstack/react-query'
import { QueriesHydration } from '@suspensive/react-query'
const queryClient = new QueryClient()
const Page = async () => {
return (
<QueriesHydration queryClient={queryClient} queries={[]}>
{/* ... */}
</QueriesHydration>
)
}skipSsrOnError
서버에서 쿼리를 불러오는 중 에러가 발생했을 때의 동작을 제어합니다. 이 옵션은 데이터 페칭이 발생하는 3단계 시점을 이해하면 더 명확합니다:
- RSC(React Server Component):
QueriesHydration에서 쿼리 실행 - RCC(React Client Component) - Server:
useSuspenseQuery에서 캐시된 데이터가 없으면 쿼리 실행 - RCC(React Client Component) - Browser:
useSuspenseQuery에서 캐시나 fresh한 데이터가 없으면 쿼리 실행
1단계에서 실패하면 2단계도 같은 원인으로 실패할 가능성이 높습니다. 하지만 3단계(브라우저)에서는 성공할 수 있습니다 (예: 인증 관련 이슈 등). 브라우저에서는 재시도를 통해 다시 성공할 가능성이 있어, skipSsrOnError를 통해 RSC에서 실패했다면 RCC(server)를 통하지 않고 바로 RCC(browser)에서 페칭하도록 하는 것이 더 좋습니다.
중요: 1단계에서 성공한 경우(에러 없음), 데이터는 이미 클라이언트에 hydrate되어 있습니다. 이 경우 브라우저의 개발자도구 네트워크 탭에서 확인할 수 있듯이 2단계와 3단계에서 추가적인 fetch 요청이 발생하지 않습니다. 데이터가 캐시에서 제공되기 때문입니다. 이것이 QueriesHydration를 사용한 서버 사이드 prefetching의 효율성을 보여줍니다.
true(기본값): 1단계에서 실패하면 SSR을 건너뛰고 3단계(브라우저)에서 재시도합니다false: 1단계에서 실패해도 hydration 없이 2단계로 진행합니다 (서버에서 다시 페칭 시도){ fallback: ReactNode }: 1단계에서 실패하면 SSR을 건너뛰고 3단계로 가는 동안 커스텀 폴백 UI를 표시합니다
import { QueriesHydration } from '@suspensive/react-query'
import { Suspense } from '@suspensive/react'
import {
userQueryOptions,
postsQueryOptions,
commentsQueryOptions,
} from './queries'
import { UserProfile, PostList, CommentList } from './_components'
const Page = async ({ userId }: { userId: number }) => {
return (
<>
{/* 1단계 실패 시 SSR을 건너뛰고 브라우저에서 재시도 (기본 동작) */}
<Suspense fallback={<div>Loading...</div>}>
<QueriesHydration queries={[userQueryOptions(userId)]}>
<UserProfile />
</QueriesHydration>
</Suspense>
{/* 1단계 실패 시 커스텀 폴백과 함께 SSR을 건너뛰고 브라우저에서 재시도 */}
<Suspense fallback={<div>Loading...</div>}>
<QueriesHydration
queries={[postsQueryOptions()]}
skipSsrOnError={{
fallback: <div>서버에서 데이터를 불러올 수 없습니다...</div>,
}}
>
<PostList />
</QueriesHydration>
</Suspense>
{/* 1단계 실패 시에도 2단계(RCC server)에서 재시도 */}
<Suspense fallback={<div>Loading...</div>}>
<QueriesHydration
queries={[commentsQueryOptions()]}
skipSsrOnError={false}
>
<CommentList />
</QueriesHydration>
</Suspense>
</>
)
}timeout
서버에서 쿼리를 불러오는 시간을 제한합니다. 쿼리를 불러오는 시간이 제한 시간을 초과하면 에러가 발생합니다. 설정하지 않으면 timeout이 적용되지 않습니다.
skipSsrOnError가 true 혹은 { fallback: ReactNode } 형태로 전달되었을 때, 쿼리를 불러오는 시간이 제한 시간을 초과하면 서버에서 렌더링을 포기하고, 브라우저에서 렌더링을 시도합니다.
import { QueriesHydration } from '@suspensive/react-query'
import { Suspense } from '@suspensive/react'
import { userQueryOptions, postsQueryOptions } from './queries'
import { UserProfile, PostList } from './_components'
const Page = async ({ userId }: { userId: number }) => {
return (
<>
<Suspense fallback={<div>Loading...</div>}>
{/* 쿼리를 1000ms 이상 불러오면 에러가 발생합니다 */}
<QueriesHydration queries={[userQueryOptions(userId)]} timeout={1000}>
<UserProfile />
</QueriesHydration>
</Suspense>
</>
)
}동기: 서버 컴포넌트에서 여러 쿼리를 간편하게 프리페칭하기
React Server Components 환경에서는 서버에서 데이터를 미리 불러와 초기 로딩 상태를 제거할 수 있습니다. 하지만 여러 개의 쿼리를 수동으로 prefetch하고 dehydrate하는 작업은 번거롭습니다.
기존 방식: 수동으로 prefetch 및 dehydrate
page.tsx (RSC)
import {
QueryClient,
dehydrate,
HydrationBoundary,
} from '@tanstack/react-query'
import { Suspense } from '@suspensive/react'
import { userQueryOptions, postsQueryOptions } from './queries'
import { UserProfile, PostList } from './_components'
// 서버 컴포넌트
const PostsPage = ({ userId }: { userId: number }) => {
return (
<>
<Suspense fallback={<div>Loading user...</div>}>
<UserProfileWithData userId={userId} />
</Suspense>
<Suspense fallback={<div>Loading posts...</div>}>
<PostListWithData userId={userId} />
</Suspense>
</>
)
}
// HTML Streaming을 위해 각 서버 컴포넌트를 분리해야 합니다
const UserProfileWithData = async ({ userId }: { userId: number }) => {
const queryClient = new QueryClient()
try {
await queryClient.ensureQueryData(userQueryOptions(userId))
} catch (error) {
return (
// queryClient.ensureQueryData 실패 시 ClientOnly를 사용하여 SSR을 막고 브라우저에서 바로 렌더링
<ClientOnly fallback={<div>Loading user...</div>}>
<UserProfile userId={userId} />
</ClientOnly>
)
}
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<UserProfile userId={userId} />
</HydrationBoundary>
)
}
const PostListWithData = async ({ userId }: { userId: number }) => {
const queryClient = new QueryClient()
try {
await queryClient.ensureQueryData(postsQueryOptions(userId))
} catch (error) {
return (
// queryClient.ensureQueryData 실패 시 ClientOnly를 사용하여 SSR을 막고 브라우저에서 바로 렌더링
<ClientOnly fallback={<div>Loading posts...</div>}>
<PostList userId={userId} />
</ClientOnly>
)
}
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<PostList userId={userId} />
</HydrationBoundary>
)
}QueriesHydration 사용
<QueriesHydration/>를 사용하면 이 모든 과정이 자동으로 처리됩니다:
page.tsx (RSC)
import { QueriesHydration } from '@suspensive/react-query'
import { Suspense } from '@suspensive/react'
import { userQueryOptions, postsQueryOptions } from './queries'
import { UserProfile, PostList } from './_components'
// 서버 컴포넌트
const PostsPage = ({ userId }: { userId: number }) => {
return (
<>
<Suspense fallback={<div>Loading user...</div>}>
<QueriesHydration queries={[userQueryOptions(userId)]}>
<UserProfile userId={userId} />
</QueriesHydration>
</Suspense>
<Suspense fallback={<div>Loading posts...</div>}>
<QueriesHydration queries={[postsQueryOptions(userId)]}>
<PostList userId={userId} />
</QueriesHydration>
</Suspense>
</>
)
}주요 이점:
- 간결한 코드: QueryClient 생성, prefetch, dehydrate 과정을 자동화
- 병렬 데이터 페칭:
Promise.all을 사용하여 모든 쿼리를 병렬로 처리 - 타입 안전성: queryKey가 필수로 요구되어 실수를 방지
- 일관된 패턴: 여러 쿼리를 일관되게 처리
Dependent Queries와 Streaming
Dependent queries(의존적 쿼리)를 처리하면서도 각 컴포넌트를 별도의 QueriesHydration으로 감싸면 독립적인 Suspense 경계로 Streaming의 이점을 최대화할 수 있습니다. 같은 queryClient를 공유하여 첫 번째 쿼리 결과를 두 번째 쿼리에서 사용할 수 있습니다.
page.tsx (RSC)
import { QueryClient } from '@tanstack/react-query'
import { QueriesHydration } from '@suspensive/react-query'
import { Suspense } from '@suspensive/react'
import {
productQueryOptions,
productReviewsQueryOptions,
relatedProductsQueryOptions,
} from './queries'
import { ProductInfo, ProductReviews, RelatedProducts } from './_components'
const ProductPage = async ({ productId }: { productId: string }) => {
const queryClient = new QueryClient()
// 1. 먼저 상품 정보를 가져옴
const product = await queryClient.ensureQueryData(
productQueryOptions(productId)
)
return (
<>
{/* 상품 정보 */}
<Suspense fallback={<div>Loading product...</div>}>
<QueriesHydration
queryClient={queryClient}
queries={[productQueryOptions(productId)]}
>
<ProductInfo productId={productId} />
</QueriesHydration>
</Suspense>
{/* 상품 리뷰: 상품 정보에 의존 (예: 상품 카테고리로 필터링) */}
<Suspense fallback={<div>Loading reviews...</div>}>
<QueriesHydration
queryClient={queryClient}
queries={[productReviewsQueryOptions(productId)]}
>
<ProductReviews productId={productId} />
</QueriesHydration>
</Suspense>
{/* 관련 상품: 상품 정보에 의존 (같은 카테고리) */}
<Suspense fallback={<div>Loading related products...</div>}>
<QueriesHydration
queryClient={queryClient}
queries={[relatedProductsQueryOptions(product.categoryId)]}
>
<RelatedProducts categoryId={product.categoryId} />
</QueriesHydration>
</Suspense>
</>
)
}이 패턴의 장점:
- 독립적인 Suspense 경계: 각 컴포넌트가 독립적으로 스트리밍되어,
ProductInfo가 먼저 준비되면 먼저 렌더링됩니다 - 같은 queryClient 공유: 캐시가 공유되어
product데이터를 재사용합니다 - Dependent queries 지원:
product결과의categoryId를relatedProductsQueryOptions에 전달할 수 있습니다 - 점진적 렌더링: 각 컴포넌트가 준비되는 대로 순차적으로 스트리밍됩니다
예제
Next.js streaming과 함께 QueriesHydration를 사용하는 실제 예제입니다:

팁: 라이브 데모에서 “4. no error (Best Practice)” 케이스를 확인하고 브라우저의 개발자도구 네트워크 탭을 열어보세요. 서버에서 성공적으로 prefetch되어 클라이언트에 hydrate된 데이터이기 때문에 fetch 요청이 발생하지 않는 것을 확인할 수 있습니다. 이것이 QueriesHydration의 효율성을 보여줍니다.
SSR 비활성화하기
만약 특정 컴포넌트에서 서버 사이드 렌더링을 사용하지 않으려면, <Suspense/>에 clientOnly prop을 추가하기만 하면 됩니다. 이 경우 서버에서 데이터를 prefetch하지 않으므로 QueriesHydration도 필요하지 않습니다:
import { QueriesHydration } from '@suspensive/react-query'
import { Suspense } from '@suspensive/react'
import { userQueryOptions, postsQueryOptions } from './queries'
import { UserProfile, PostList } from './_components'
const PostsPage = ({ userId }: { userId: number }) => {
return (
<>
{/* UserProfile은 SSR을 건너뛰고 클라이언트에서만 렌더링됩니다 */}
<Suspense clientOnly fallback={<div>Loading user...</div>}>
<UserProfile userId={userId} />
</Suspense>
{/* PostList는 서버에서 prefetch되고 렌더링됩니다 */}
<Suspense fallback={<div>Loading posts...</div>}>
<QueriesHydration queries={[postsQueryOptions(userId)]}>
<PostList userId={userId} />
</QueriesHydration>
</Suspense>
</>
)
}clientOnly prop을 사용하면:
- 해당 Suspense 경계 내의 컴포넌트는 서버에서 렌더링되지 않습니다
- 클라이언트에서만 데이터를 페칭하고 렌더링합니다
- 서버 사이드 prefetch가 필요 없으므로
QueriesHydration를 사용하지 않아도 됩니다
버전별 차이점
@tanstack/react-query v5
@tanstack/react-query v5에서는 HydrationBoundary 컴포넌트를 사용합니다.
import { HydrationBoundary } from '@tanstack/react-query'
import { QueriesHydration } from '@suspensive/react-query'
// QueriesHydration는 내부적으로 HydrationBoundary를 사용합니다@tanstack/react-query v4
@tanstack/react-query v4에서는 Hydrate 컴포넌트를 사용합니다.
import { Hydrate } from '@tanstack/react-query'
import { QueriesHydration } from '@suspensive/react-query'
// QueriesHydration는 내부적으로 Hydrate를 사용합니다주의사항
- 이 컴포넌트는 async 서버 컴포넌트입니다.
- React Server Components를 지원하는 프레임워크(Next.js 13+ App Router 등)에서만 사용 가능합니다.
- 모든 쿼리는
queryKey를 반드시 포함해야 합니다.
버전 기록
| Version | Changes |
|---|---|
| v3.14.0 | <QueriesHydration/>가 실험 기능으로 추가되었습니다. |