핵심 개념
💡왜 설정이 다른가요?
서버는 여러 사용자의 요청을 동시에 처리합니다. 하나의 QueryClient를 공유하면 사용자 A의 데이터가 사용자 B에게 노출될 수 있습니다.
클라이언트는 한 사용자만 사용하므로 QueryClient를 재사용해도 안전합니다. 오히려 재사용해야 캐시가 유지됩니다.
✅3가지 데이터 페칭 전략
- CSR: 클라이언트에서 useQuery로 fetch
- SSR + Hydration: 서버에서 prefetch → 클라이언트로 전달
- Streaming: Suspense로 점진적 렌더링
기존 코드의 문제점
문제가 있는 코드
1// ❌ 기존 코드의 문제점2 3// 문제 1: 모듈 레벨에서 호출4export const queryClient = getQueryClient();5// → 서버에서 이 모듈이 import될 때마다 실행됨6 7// 문제 2: Provider props에서 직접 호출8<QueryClientProvider client={getQueryClient()}>9// → 매 렌더링마다 호출될 수 있음❌왜 문제인가요?
- 모듈 레벨 호출: 서버에서 모듈이 캐시되면 모든 요청이 같은 QueryClient를 공유할 수 있음
- Props에서 직접 호출: React 렌더링 사이클에서 예측하기 어려운 동작
올바른 패턴
권장 패턴 (Next.js App Router)
1// ✅ 올바른 패턴 (Next.js App Router)2 3'use client';4import { useState } from 'react';5import { QueryClient, QueryClientProvider } from '@tanstack/react-query';6 7function makeQueryClient() {8 return new QueryClient({ /* 옵션 */ });9}10 11let browserQueryClient: QueryClient | undefined;12 13function getQueryClient() {14 if (typeof window === 'undefined') {15 // 서버: 항상 새로 생성 (요청 격리)16 return makeQueryClient();17 }18 // 클라이언트: 싱글톤19 if (!browserQueryClient) {20 browserQueryClient = makeQueryClient();21 }22 return browserQueryClient;23}24 25export default function QueryProvider({ children }) {26 // useState 초기화 함수로 전달 → 마운트 시 1회만 실행27 const [queryClient] = useState(getQueryClient);28 29 return (30 <QueryClientProvider client={queryClient}>31 {children}32 </QueryClientProvider>33 );34}✅핵심 포인트
typeof window === 'undefined'로 서버/클라이언트 구분useState(getQueryClient)로 마운트 시 1회만 실행- 서버에서는 매번 새 인스턴스, 클라이언트에서는 싱글톤 유지
인터랙티브 데모
각 방식의 차이를 직접 체험해보세요. 네트워크 탭을 열어 데이터 로딩 타이밍을 확인할 수 있습니다.
방식 비교
| 특성 | CSR | SSR + Hydration | Streaming |
|---|---|---|---|
| 초기 HTML | 빈 상태 / 스켈레톤 | 완성된 데이터 | 스켈레톤 → 데이터 |
| TTFB | 빠름 | 느림 (데이터 대기) | 빠름 |
| SEO | 불리 | 유리 | 유리 |
| 사용 훅 | useQuery | useQuery + prefetch | useSuspenseQuery |
| 적합한 경우 | 대시보드, 인증 필요 페이지 | SEO 중요, 정적 콘텐츠 | 복잡한 페이지, 병렬 데이터 |