Skill
2026년 01월 04일

next.js app router 에서의 tanstack-query (1)

app router에서 tanstack-query의 사용

  • 이 포스팅의 핵심은 app router에서 서버컴포넌트와 클라이언트 상태 관리의 조화를 이해하는것이 핵심이다.
  • 단순히 데이터를 가져오는 것을 넘어 캐싱, 프리페칭, hydration 전략까지 다룰 수 있어야한다.

커리큘럼

1. Lesson 1 (환경 설정): App Router에서 QueryClient를 안전하게 생성하고 주입하는 방법

(Singleton 패턴 주의).

2. Lesson 2 (기본 사용): 클라이언트 컴포넌트에서 useQuery로 데이터 가져오기.

3. Lesson 3 (서버 연동 - Hydration): 서버 컴포넌트에서 미리 데이터를 가져와(Prefetch)

클라이언트로 전달하기 (initialData vs HydrationBoundary).

4. Lesson 4 (UX 최적화): useSuspenseQuery와 React Suspense를 활용한 로딩 처리.

5. Lesson 5 (데이터 변경): useMutation을 활용한 찜하기(Wishlist) 기능 구현.

Lesson 1 (환경설정)

가장 먼저 할 일은 tanstack-query를 사용할 수 있도록 Provider로 감싸는 일을 해야한다.

  • next.js의 app router에서는 주의할 점이 있다.
  1. 서버 컴포넌트에서는 ‘Context’를 사용할 수 없는데 어떻게 Provider를 감싸지?
  2. Provider에 사용되는 QueryClient는 전역변수로 만들면 안되나?
* 1: 서버 컴포넌트는 Context를 못 쓰니, 'use client'를 붙인 providers.tsx 파일로 따로 빼서 감싸준다. * 2: 서버는 여러 사람이 공유하는 공공장소라 재생성(Client)을 써야 하고, 브라우저는 나만의 공간이라 재사용(Client)을 써야 한다.

  • 환경 셋팅 (bun 을 사용)
bun add @tanstack/react-query && bun add -d @tanstack/react-query-devtools
// .../query-client.tsx import { isServer, QueryClient } from "@tanstack/react-query"; function makeQueryClient() { return new QueryClient({ defaultOptions: { queries: { // SSR 환경에서는 보통 0 이상의 staleTime을 설정하여 // 클라이언트에서 즉시 재요청하는 것을 방지합니다. staleTime: 60 * 1000, refetchOnWindowFocus: false, }, }, }); } // 브라우저에서 싱글톤으로 쿼리 클라이언트를 관리 let browserQueryClient: QueryClient | undefined = undefined; export function getQueryClient() { if (isServer) { // 서버단에서는 쿼리 계속 재생성 return makeQueryClient(); } else { // 브라우저단에서는 싱글톤 재사용 if (!browserQueryClient) browserQueryClient = makeQueryClient(); return browserQueryClient; } }
// .../query-provider.tsx "use client"; import { QueryClientProvider } from "@tanstack/react-query"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; import { getQueryClient } from "./query-client"; export function QueryProviders({ children }: { children: React.ReactNode }) { // NOTE: QueryClient를 초기화할 때 useState를 피하세요. // Suspense 경계가 없을 경우 초기 렌더링 시 클라이언트가 버려질 수 있습니다. const queryClient = getQueryClient(); return ( <QueryClientProvider client={queryClient}> {children} <ReactQueryDevtools initialIsOpen={false} /> </QueryClientProvider> ); }
  • Provider 적용
// app/layout.tsx export default function RootLayout({ ... ... return( <QueryProviders>{children}</QueryProviders> ) })