이 문서는 Next.js App Router (React Server Component) 환경에서 프로젝트 내 TanStack Query를 설정하고 데이터를 패칭(Prefetching)하는 흐름을 코드 리뷰와 함께 알기 쉽게 정리한 문서입니다.
서버 컴포넌트에서는 요청마다 독립적인 상태 관리가 필요합니다. 이를 위해 싱글톤을 쓰지 않고 페이지 진입 시마다 새로운 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,
},
},
});
}
서버 단에서 화면 게시에 필요한 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);
}
}
prefetchQuery를 바로 쓰지 않고 try-catch 블록으로 예외 처리가 된 유틸리티 함수를 이용합니다.서버에서 가져온 데이터를 컴포넌트가 재사용할 수 있도록 클라이언트 측에 내려주는 단계입니다.
사용 예시 파일: 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>
);
}
<HydrationBoundary>에 담긴 직렬화된 데이터가 HTML과 함께 클라이언트 브라우저로 전송됩니다.이렇게 부모(RSC) 영역에서 수화(Hydration)된 데이터는 자식 컴포넌트 내부에서 최초 렌더링 시 **초기 캐시값(Initial Data)**으로 즉각 투입됩니다.