이 문서는 Next.js App Router (React Server Component) 환경에서 프로젝트 내 TanStack Query를 설정하고 데이터를 패칭(Prefetching)하는 흐름을 코드 리뷰와 함께 알기 쉽게 정리한 문서입니다.

1. RSC 전용 QueryClient 인스턴스 생성

서버 컴포넌트에서는 요청마다 독립적인 상태 관리가 필요합니다. 이를 위해 싱글톤을 쓰지 않고 페이지 진입 시마다 새로운 QueryClient를 생성합니다.

관련 파일src/lib/react-query/client.ts

import { QueryClient } from "@tanstack/react-query";

/** RSC용 QueryClient 팩토리 — page.tsx에서 prefetchQuery 시 사용 */
export function createQueryClient() {
  return new QueryClient({
    defaultOptions: {
      queries: {
        staleTime: 60 * 1000,
        retry: 1,
      },
    },
  });
}

2. 안전한 사전 데이터 패칭 (Prefetching)

서버 단에서 화면 게시에 필요한 API 데이터를 미리 호출합니다.

관련 파일src/lib/react-query/prefetch.ts

export async function safePrefetchQuery(
  queryClient: QueryClient,
  options: FetchQueryOptions,
  scope: string,): Promise<void> {
  try {
    await queryClient.prefetchQuery(options);
  } catch (error) {
    // API 에러 시 앱 크래시 방지 및 콘솔 로깅
    logPrefetchError(scope, error);
  }
}

3. Dehydration과 HydrationBoundary 컴포넌트 래핑

서버에서 가져온 데이터를 컴포넌트가 재사용할 수 있도록 클라이언트 측에 내려주는 단계입니다.

사용 예시 파일src/app/products/[id]/page.tsx

import { dehydrate, HydrationBoundary } from "@tanstack/react-query";
import { createQueryClient, safePrefetchQuery, PRODUCT_KEYS } from "@/lib/react-query";
import { productService } from "@/services/product";
import ProductDetail from "./ProductDetail";

export default async function ProductDetailPage({ params }: IProductDetailPageProps) {
  const { id } = await params;
  const productId = Number(id);
  const queryClient = createQueryClient();

  // 1. 서버 쪽에서 데이터를 미리 패칭
  if (productId > 0) {
    await safePrefetchQuery(
      queryClient,
      {
        queryKey: PRODUCT_KEYS.detail(productId),
        queryFn: () => productService.getProduct(productId),
      },
      "products/[id]/page",
    );
  }

  // 2. 패칭 된 상태를 dehydrate()로 직렬화하여 클라이언트에 주입
  return (
    <HydrationBoundary state={dehydrate(queryClient)}>
      <ProductDetail productId={id} />
    </HydrationBoundary>
  );
}

4. 클라이언트 컴포넌트에서 데이터 즉시 사용

이렇게 부모(RSC) 영역에서 수화(Hydration)된 데이터는 자식 컴포넌트 내부에서 최초 렌더링 시 **초기 캐시값(Initial Data)**으로 즉각 투입됩니다.