TanStack Query

TanStack Query No QueryClient set 오류 해결: QueryClientProvider 위치 확인하기

2026.05.22·약 17분

이 글에서 정리하는 내용

TanStack Query에서 No QueryClient set 오류가 발생했을 때 먼저 확인해야 할 지점은 queryFn이 아니라 QueryClientProvider입니다. React 프로젝트와 Next.js App Router에서 Provider를 어디에 두어야 하는지, 이미 Provider를 넣었는데도 오류가 계속 나는 경우 어떤 순서로 좁혀봐야 하는지 정리합니다.

No QueryClient set 오류가 말하는 것

TanStack Query No QueryClient set 오류가 Provider 누락에서 발생하는 구조

TanStack Query를 처음 붙일 때 No QueryClient set, use QueryClientProvider to set one 오류를 만나면 보통 useQuery 안쪽부터 보게 됩니다. API 함수가 잘못됐는지, queryKey 배열이 이상한지, axios 응답 처리가 틀렸는지 확인하게 됩니다. 하지만 이 오류는 요청 함수가 실패했다는 뜻이 아닙니다.

이 메시지는 TanStack Query가 사용할 QueryClient를 React 컴포넌트 트리 안에서 찾지 못했다는 뜻입니다. 서버에서 데이터를 못 받아온 문제가 아니라, 데이터를 관리할 공통 클라이언트가 Provider를 통해 전달되지 않은 상태입니다.

예를 들어 상품 목록 컴포넌트에서 useQuery를 바로 사용했는데 앱의 루트가 QueryClientProvider로 감싸져 있지 않다면, TanStack Query는 어떤 클라이언트를 기준으로 캐시를 만들고 요청 상태를 관리해야 할지 알 수 없습니다. 이때는 네트워크 요청이 실행되기 전에 오류가 먼저 발생합니다.

오류가 나는 컴포넌트 예시

import { useQuery } from '@tanstack/react-query'

async function fetchProducts() {
    const response = await fetch('/api/products')
    return response.json()
}

export function ProductList() {
    const { data, isLoading } = useQuery({
        queryKey: ['products'],
        queryFn: fetchProducts,
    })

    if (isLoading) {
        return <p>상품을 불러오는 중입니다.</p>
    }

    return (
        <ul>
            {data?.map((product: { id: number; name: string }) => (
                <li key={product.id}>{product.name}</li>
            ))}
        </ul>
    )
}

이 코드만 보면 queryKey도 있고 queryFn도 있습니다. 코드 자체가 무조건 틀렸다고 보기는 어렵습니다. 그런데 이 컴포넌트가 QueryClientProvider 안쪽에서 렌더링되지 않으면 같은 오류가 납니다. 그래서 이 오류를 만났을 때는 컴포넌트 내부보다 앱을 감싸는 바깥 구조를 먼저 봐야 합니다.

QueryClientProvider가 필요한 이유

QueryClient는 TanStack Query가 쿼리 캐시, 요청 상태, 재요청, 무효화 같은 서버 상태를 관리할 때 사용하는 중심 객체입니다. useQuery는 화면에서 데이터를 요청하는 hook이지만, 실제로는 이 QueryClient와 연결되어야 정상적으로 동작합니다.

QueryClientProvider는 만든 QueryClient를 React 트리 전체에 전달합니다. React의 Context Provider와 비슷하게 보면 됩니다. Provider 안쪽에 있는 컴포넌트는 같은 QueryClient를 바라보고, Provider 바깥에 있는 컴포넌트는 그 QueryClient를 찾지 못합니다.

처음에는 이 구조가 조금 번거롭게 보일 수 있습니다. 하지만 서버 상태는 여러 컴포넌트에서 동시에 쓰이는 경우가 많습니다. 상품 목록에서 받아온 데이터를 상세 버튼, 필터 영역, 새로고침 버튼, 수정 모달이 함께 참조할 수 있습니다. 이때 컴포넌트마다 요청 상태를 따로 만들면 캐시와 갱신 기준이 흩어집니다. QueryClientProvider는 이 기준을 앱의 위쪽에서 한 번 잡아주는 장치입니다.

Provider 없이 일부 컴포넌트만 렌더링한 경우

import { ProductList } from './ProductList'

export default function App() {
    return (
        <main>
            <ProductList />
        </main>
    )
}

이 구조에서는 ProductListuseQuery를 사용하더라도 위쪽에 QueryClient를 제공하는 Provider가 없습니다. ProductList의 내부 코드를 계속 수정해도 오류가 사라지지 않는 이유가 여기에 있습니다. 수정해야 할 위치는 목록 컴포넌트가 아니라 앱을 시작하는 루트 쪽입니다.

React 프로젝트에서 Provider 위치 확인하기

Vite 기반의 일반 React 프로젝트라면 보통 main.tsx 또는 App.tsx 근처에서 Provider를 배치합니다. 기준은 단순합니다. useQuery, useMutation, useQueryClient를 쓰는 모든 컴포넌트가 Provider 안쪽에 들어와야 합니다.

가장 흔한 방식은 앱의 루트 렌더링 위치에서 QueryClientProviderApp 전체를 감싸는 것입니다. 이렇게 하면 라우터 안쪽 페이지, 공통 레이아웃, 모달, 목록 컴포넌트가 같은 QueryClient를 사용할 수 있습니다.

main.tsx에서 전체 App 감싸기

import React from 'react'
import ReactDOM from 'react-dom/client'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import App from './App'

const queryClient = new QueryClient()

ReactDOM.createRoot(document.getElementById('root')!).render(
    <React.StrictMode>
        <QueryClientProvider client={queryClient}>
            <App />
        </QueryClientProvider>
    </React.StrictMode>
)

여기서 볼 부분은 QueryClientProviderApp 바깥에 있다는 점입니다. ProductListApp 안쪽 어디에서 렌더링되든 Provider 범위 안에 들어옵니다. 이 구조가 잡혀 있으면 No QueryClient set 오류는 대부분 루트 설정 문제에서 벗어납니다.

반대로 특정 페이지 컴포넌트 안쪽에만 Provider를 넣으면 문제가 다시 생길 수 있습니다. 예를 들어 상품 페이지에만 Provider를 넣고, 헤더의 로그인 정보나 공통 알림 컴포넌트에서 useQuery를 사용하면 그 컴포넌트는 Provider 바깥에 있을 수 있습니다. 앱 전체에서 TanStack Query를 사용할 계획이라면 루트에 가깝게 배치해야 나중에 페이지가 늘어나도 덜 흔들립니다.

Next.js App Router에서 확인할 부분

Next.js App Router에서는 Provider 위치를 잡을 때 한 가지를 더 봐야 합니다. app/layout.tsx는 기본적으로 Server Component입니다. 그런데 QueryClientProvider는 React Context를 사용하고, QueryClient는 클라이언트 쪽에서 유지되어야 합니다. 그래서 Provider 전용 컴포넌트를 따로 만들고 그 파일을 클라이언트 컴포넌트로 두는 구성이 자주 쓰입니다.

layout.tsx 전체를 클라이언트 컴포넌트로 바꾸기보다, app/providers.tsx 같은 파일을 만들어 필요한 Provider만 분리하는 방식이 더 깔끔합니다. 레이아웃은 서버 컴포넌트의 기본 구조를 유지하고, TanStack Query 설정만 클라이언트 컴포넌트로 빼는 방식입니다.

app/providers.tsx 만들기

'use client'

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

type ProvidersProps = {
    children: ReactNode
}

export function Providers({ children }: ProvidersProps) {
    const [queryClient] = useState(() => new QueryClient())

    return (
        <QueryClientProvider client={queryClient}>
            {children}
        </QueryClientProvider>
    )
}

'use client'는 이 파일이 클라이언트 컴포넌트라는 표시입니다. useStateQueryClient를 만드는 이유는 컴포넌트가 다시 렌더링될 때마다 새 QueryClient가 만들어지는 일을 피하기 위해서입니다. QueryClient가 매번 새로 만들어지면 캐시 기준도 계속 바뀌기 때문에, Provider 설정에서는 한 번 만든 인스턴스를 유지하는 쪽이 맞습니다.

app/layout.tsx에서 children 감싸기

import type { ReactNode } from 'react'
import { Providers } from './providers'

export default function RootLayout({ children }: { children: ReactNode }) {
    return (
        <html lang="ko">
            <body>
                <Providers>
                    {children}
                </Providers>
            </body>
        </html>
    )
}

이 구조에서는 app 아래의 페이지들이 Providers 안쪽으로 들어옵니다. 따라서 클라이언트 컴포넌트에서 useQuery를 사용할 때 QueryClient를 찾을 수 있습니다. 단, useQuery 자체는 클라이언트 컴포넌트에서 사용해야 합니다. Server Component에서 직접 useQuery를 호출하려고 하면 Provider 문제와 별개로 컴포넌트 경계 문제가 생깁니다.

Next.js에서 이 오류가 날 때는 providers.tsx 파일이 있는지, 파일 상단에 'use client'가 있는지, layout.tsx에서 실제로 {children}을 감싸고 있는지 순서대로 보면 됩니다. Provider 파일을 만들었는데 layout에 연결하지 않았거나, Providers 바깥에서 별도의 클라이언트 컴포넌트를 렌더링하는 경우도 함께 확인해야 합니다.

Provider를 넣었는데도 오류가 계속 날 때

QueryClientProvider를 넣었는데도 오류가 계속 날 때 확인할 React 트리와 렌더링 환경

Provider를 추가했는데도 오류가 계속 난다면 단순 누락은 아닐 수 있습니다. 이때는 “Provider가 존재하는가”보다 “오류가 나는 컴포넌트가 실제로 그 Provider 안쪽에서 렌더링되는가”를 확인해야 합니다.

첫 번째로 import 경로를 봅니다. 현재 TanStack Query는 @tanstack/react-query에서 가져옵니다. 예전 코드나 검색 결과를 그대로 따라 하다가 react-query@tanstack/react-query가 섞이면 Provider와 hook이 서로 다른 패키지를 바라보는 상황이 생길 수 있습니다. Provider와 hook은 같은 패키지에서 가져와야 합니다.

패키지 import가 섞인 경우

// 피해야 할 예시
import { QueryClientProvider } from 'react-query'
import { useQuery } from '@tanstack/react-query'

이런 식으로 섞이면 코드만 봐서는 Provider를 넣은 것처럼 보이지만, 실제로는 같은 Context를 공유하지 못할 수 있습니다. Provider, QueryClient, useQuery, useMutation, useQueryClient는 모두 @tanstack/react-query에서 가져오는지 맞춰봐야 합니다.

두 번째로 라우터나 모달 렌더링 위치를 확인합니다. 일반적인 포털 모달은 React 트리상 Provider 안쪽에서 만들어지면 문제가 없지만, 별도의 React 루트로 독립 렌더링하는 구조라면 Provider 범위에서 벗어날 수 있습니다. 관리자 화면처럼 메인 앱과 분리된 위젯을 따로 렌더링하는 경우에도 같은 문제가 납니다.

세 번째로 테스트 코드와 Storybook을 확인합니다. 실제 앱에서는 Provider가 잘 감싸져 있어도, 테스트에서 컴포넌트만 단독 렌더링하면 Provider가 없습니다. 이 경우 테스트용 wrapper를 만들어 QueryClientProvider를 함께 넣어야 합니다.

테스트용 wrapper 예시

import { ReactNode } from 'react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'

export function createQueryWrapper() {
    const queryClient = new QueryClient({
        defaultOptions: {
            queries: {
                retry: false,
            },
        },
    })

    return function QueryWrapper({ children }: { children: ReactNode }) {
        return (
            <QueryClientProvider client={queryClient}>
                {children}
            </QueryClientProvider>
        )
    }
}

테스트에서는 재시도 옵션 때문에 결과가 늦게 바뀌거나 실패 원인을 찾기 어려운 경우가 있습니다. 그래서 테스트용 QueryClient에는 retry: false처럼 테스트에 맞는 기본 옵션을 따로 둘 수 있습니다. 다만 이 글에서 더 먼저 봐야 하는 부분은 테스트 옵션이 아니라, 테스트 환경에서도 Provider가 필요하다는 점입니다.

다시 같은 오류를 만나지 않기 위한 체크포인트

No QueryClient set 오류를 만나면 queryFn부터 고치기보다 Provider 위치를 먼저 확인해야 합니다. 요청 함수가 실패했다면 보통 네트워크 오류, 상태 코드, 응답 파싱 문제가 드러납니다. 하지만 이 오류는 요청을 실행하기 전에 TanStack Query가 사용할 QueryClient를 찾지 못한 상황입니다.

React 프로젝트라면 main.tsx 또는 앱의 루트에 가까운 위치에서 QueryClientProviderApp 전체를 감싸고 있는지 확인합니다. Next.js App Router라면 app/providers.tsx 파일을 클라이언트 컴포넌트로 만들고, app/layout.tsx에서 {children}을 감싸고 있는지 봅니다.

Provider를 이미 넣었다면 import가 모두 @tanstack/react-query에서 왔는지, 오류가 나는 컴포넌트가 Provider 바깥에서 따로 렌더링되고 있지 않은지, 테스트나 Storybook처럼 별도 렌더링 환경에서 wrapper가 빠진 것은 아닌지 확인합니다.

이 오류는 TanStack Query의 캐싱 전략을 깊게 이해해야만 해결되는 문제는 아닙니다. 먼저 React 트리 안에서 QueryClient가 전달되는 길을 확인하면 됩니다. 그 다음에야 queryKey, staleTime, hydration 같은 세부 설정을 보는 순서가 자연스럽습니다.

이 글이 마음에 드세요?

RSS 피드를 구독하세요!

댓글 남기기