Skip to Content

QueriesHydration

<QueriesHydration/>는 실험 기능이므로 이 인터페이스는 변경될 수 있습니다.

서버 컴포넌트에서 여러 개의 쿼리를 미리 불러오고 클라이언트 컴포넌트에 자동으로 hydrate하는 컴포넌트입니다.

React Server Components를 사용하는 환경에서 서버 측에서 데이터를 미리 불러와 클라이언트로 전달할 때 유용합니다.

💡 추천 읽기: 이 컴포넌트를 사용하기 전에 TanStack Query Advanced Server Rendering 가이드 를 읽어보시는 것을 권장합니다. 이 가이드는 Server Components, streaming, 그리고 Next.js App Router에 대해 다룹니다. 이러한 개념을 이해하면 QueriesHydration를 훨씬 더 편하게 사용할 수 있습니다.

기본 사용법

QueriesHydration에서 queryOptions를 사용하여 서버에서 데이터를 미리 불러올 수 있습니다. HTML Streaming을 최대화하려면 각 컴포넌트를 별도의 SuspenseQueriesHydration으로 감싸는 것이 좋습니다:

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을 최대화하려면 각 컴포넌트를 별도의 SuspenseQueriesHydration으로 감싸는 것이 좋습니다:

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단계 시점을 이해하면 더 명확합니다:

  1. RSC(React Server Component): QueriesHydration에서 쿼리 실행
  2. RCC(React Client Component) - Server: useSuspenseQuery에서 캐시된 데이터가 없으면 쿼리 실행
  3. 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

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/>를 사용하면 이 모든 과정이 자동으로 처리됩니다:

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> </> ) }

주요 이점:

  1. 간결한 코드: QueryClient 생성, prefetch, dehydrate 과정을 자동화
  2. 병렬 데이터 페칭: Promise.all을 사용하여 모든 쿼리를 병렬로 처리
  3. 타입 안전성: queryKey가 필수로 요구되어 실수를 방지
  4. 일관된 패턴: 여러 쿼리를 일관되게 처리

Dependent Queries와 Streaming

Dependent queries(의존적 쿼리)를 처리하면서도 각 컴포넌트를 별도의 QueriesHydration으로 감싸면 독립적인 Suspense 경계로 Streaming의 이점을 최대화할 수 있습니다. 같은 queryClient를 공유하여 첫 번째 쿼리 결과를 두 번째 쿼리에서 사용할 수 있습니다.

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> </> ) }

이 패턴의 장점:

  1. 독립적인 Suspense 경계: 각 컴포넌트가 독립적으로 스트리밍되어, ProductInfo가 먼저 준비되면 먼저 렌더링됩니다
  2. 같은 queryClient 공유: 캐시가 공유되어 product 데이터를 재사용합니다
  3. Dependent queries 지원: product 결과의 categoryIdrelatedProductsQueryOptions에 전달할 수 있습니다
  4. 점진적 렌더링: 각 컴포넌트가 준비되는 대로 순차적으로 스트리밍됩니다

예제

Next.js streaming과 함께 QueriesHydration를 사용하는 실제 예제입니다:

Next.js Streaming React Query Example

: 라이브 데모에서 “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를 반드시 포함해야 합니다.

버전 기록

VersionChanges
v3.14.0<QueriesHydration/>가 실험 기능으로 추가되었습니다.
수정된 날짜: