[Next.js] React Query + Devtools 세팅 (with TanStack)

오병훈·2025년 4월 14일
0

🧠 TanStack React Query 세팅 시 우리가 선택한 구조와 그 이유

Next.js 프로젝트에서 @tanstack/react-query를 사용할 때 단순히 QueryClientProvider만 감싸는 게 아니라, 우리는 별도의 ReactQueryProvider 파일을 만들어 전역 세팅, Devtools, 상태 관리까지 함께 처리했습니다.
그렇게 한 이유와 선택들을 정리해보았습니다!


📦 1. 왜 @tanstack/react-query-devtools를 썼을까?

React Query는 비동기 상태를 매우 편리하게 다뤄주지만,
내부에서 어떤 쿼리가 등록되어 있는지, 현재 로딩 상태인지, 에러가 났는지, 데이터는 캐시되어 있는지 등을 시각적으로 확인할 수 없다면 디버깅이 어렵습니다.

이를 위해 @tanstack/react-query-devtools에서 제공하는 <ReactQueryDevtools />를 사용하면, 개발자 도구처럼 화면 오른쪽 하단에 쿼리 상태를 실시간으로 볼 수 있습니다.

🔍 Devtools의 주요 기능

  • 현재 등록된 쿼리 리스트 보기
  • stale, fresh, inactive 상태 확인
  • refetch, invalidate, remove 버튼 제공
  • 쿼리 캐시된 실제 데이터 확인 가능

🚫 이 Devtools는 개발 환경에서만 보여야 하므로, 아래처럼 조건부로 렌더링하는 것이 일반적이라고 합니다.

{process.env.NODE_ENV === 'development' && (
  <ReactQueryDevtools initialIsOpen={false} />
)}

🧩 2. 왜 ReactQueryProvider.tsx 파일로 따로 뺐을까?

React Query는 프로젝트 전체에서 공유하는 전역 상태이기 때문에,
QueryClientProvider는 최상단(layout.tsx)에서 한 번만 래핑해주면 됩니다.

하지만 QueryClient 인스턴스를 직접 layout.tsx에 하드코딩하면, 아래와 같은 문제가 발생할 수 있습니다.:

  • 컴포넌트가 리렌더될 때마다 QueryClient가 재생성됨 → 쿼리 캐시가 초기화됨
  • 로직이 layout 파일에 섞여 복잡해짐
  • Devtools 등 React Query 관련 부가 설정을 관리하기 어려움

👉 그래서 React Query 관련 설정을 전부 한 곳에 모은 ReactQueryProvider를 따로 만들어 분리해줍니다.


🐢 3. 왜 useState로 QueryClient를 lazy하게 생성할까?

React에서는 컴포넌트가 리렌더링될 때마다 함수 바디가 다시 실행되기 때문에,
아래처럼 쓰면 QueryClient계속 새로 만들어져서 캐시가 리셋되는 문제가 발생합니다.

const queryClient = new QueryClient() // ❌ 매번 새로 생성됨

이를 방지하기 위해 useState의 lazy initializer를 사용하면,
최초 렌더 시 딱 한 번만 QueryClient가 생성되고, 이후에는 유지됩니다.

const [queryClient] = useState(() => new QueryClient())

즉, useState(() => new QueryClient())는 React Query 사용 시 이용이 권장된다고 합니다!

3-1. 왜 useMemo나 useRef보다 useState를 선호할까?

React Query 공식 문서에서는 QueryClient 인스턴스를 useStateuseRef를 사용하여 컴포넌트 내부에서 생성할 것을 권장합니다. 이는 SSR 환경에서 여러 사용자 요청 간에 데이터가 공유되는 것을 방지하기 위함입니다. useState를 사용하면 컴포넌트 생명주기 동안 동일한 QueryClient 인스턴스를 유지할 수 있습니다.

  • useMemo: useMemo는 메모이제이션을 위해 사용되며, 의존성 배열이 변경될 때마다 값을 재계산합니다. 그러나 QueryClient는 한 번만 생성되어야 하므로, 의존성 배열에 따라 재생성이 발생할 수 있는 useMemo는 적합하지 않습니다.

  • useRef: useRef는 변경 가능한 참조를 유지하지만, 초기화 시점에서만 값을 설정하며 이후에는 변경되지 않습니다. useRef를 사용하여 QueryClient를 생성할 수도 있지만, useState를 사용하면 상태 업데이트와 관련된 React의 기능을 활용할 수 있습니다.

따라서, useState를 사용하여 QueryClient를 지연 초기화하면 SSR 환경에서의 데이터 공유 문제를 방지하고, 컴포넌트 생명주기 동안 일관된 인스턴스를 유지할 수 있습니다.


4. 🧠 최종 코드 구조

// ReactQueryProvider.tsx
'use client'

import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
import { useState } from 'react'

export default function ReactQueryProvider({ children }: { children: React.ReactNode }) {
  const [queryClient] = useState(() => new QueryClient())

  return (
    <QueryClientProvider client={queryClient}>
      {children}
      {process.env.NODE_ENV === 'development' && (
        <ReactQueryDevtools initialIsOpen={false} />
      )}
    </QueryClientProvider>
  )
}
// layout.tsx
import ReactQueryProvider from '@/providers/react-query-provider'

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <ReactQueryProvider>
          {children}
        </ReactQueryProvider>
      </body>
    </html>
  )
}
profile
Front-End Developer

0개의 댓글