
Next.js에서 탠스택쿼리 세팅하는 법이 워낙 헷갈렸기에 정리해보는 오늘의 주제.
처음 next.js에 tanstack을 세팅하는 법을
TanStack Query 공식문서 - Advanced Server Rendering 를 토대로 배웠는데,
이는 SSR(서버 사이드 렌더링)과 클라이언트 측 동작을 분리하여 최적화하는 코드라고 볼 수 있다.
// In Next.js, this file would be called: app/providers.tsx
'use client'
// Since QueryClientProvider relies on useContext under the hood, we have to put 'use client' on top
import {
isServer,
QueryClient,
QueryClientProvider,
} from '@tanstack/react-query'
function makeQueryClient() {
return new QueryClient({
defaultOptions: {
queries: {
// With SSR, we usually want to set some default staleTime
// above 0 to avoid refetching immediately on the client
staleTime: 60 * 1000,
},
},
})
}
let browserQueryClient: QueryClient | undefined = undefined
function getQueryClient() {
if (isServer) {
// Server: always make a new query client
return makeQueryClient()
} else {
// Browser: make a new query client if we don't already have one
// This is very important, so we don't re-make a new client if React
// suspends during the initial render. This may not be needed if we
// have a suspense boundary BELOW the creation of the query client
if (!browserQueryClient) browserQueryClient = makeQueryClient()
return browserQueryClient
}
}
export default function Providers({ children }: { children: React.ReactNode }) {
// NOTE: Avoid useState when initializing the query client if you don't
// have a suspense boundary between this and the code that may
// suspend because React will throw away the client on the initial
// render if it suspends and there is no boundary
const queryClient = getQueryClient()
return (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
)
}
staleTime: SSR을 사용하면 클라이언트에서 즉시 stale한 상태가 되기 때문에(default : 0), 다시 값을 가져오는 것을 방지하기 위해 기본 staleTime을 0 이상으로 설정한다.
function getQueryClient : 서버에서는 매번 새로운 Query Client를 생성하고, 브라우저에서는 캐싱된 Query Client를 사용한다.
// In Next.js, this file would be called: app/layout.tsx
import Providers from './providers'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<head />
<body>
<Providers>{children}</Providers>
</body>
</html>
)
}
root 경로에 있는 공통 layout에 children을 Provider로 감싸주어 전체 페이지에서 해당 queryClient를 공유할 수 있도록 한다.
특징
- 위 코드를 사용했을 때, 서버에서는 항상 새로운 Query Client를 만들고, 클라이언트에서는 이미 만들어진 Query Client를 재사용하여 성능을 최적화할 수 있기 때문에 SSR을 적극 활용하려고 할 때는 위 코드 형태가 적합하다.
Needs#1. 보다 코드를 간단하게 가져갈 수 없을까?
Needs#2. 그러면서도 리렌더링이 일어날 때마다 새로운 queryClient를 만들지 않았으면 좋겠다.
공식문서에서 제공한 것 처럼 isServer 상태에 따라 browserQueryClient, queryClient를 분리하여 browserQueryClient를 재사용할 수도 있지만,
const Provider = ({ children }: { children: React.ReactNode }) => {
const queryClient = new QueryClient();
return (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
};
위 코드처럼 작성해도 queryClient를 사용하는 데에는 문제가 없었다. useQuery, useMutation 등 탠스택쿼리에서 제공하는 메소드를 사용하는데 아무런 문제가 없었지만...
but, 내부 로직 상으로는 비효율이 발생한다는 점.

성능 최적화엔 끝이 없다..🙃
위에서 정리했던 니즈 2가지 중,
Needs#1. 보다 코드를 간단하게 가져갈 수 없을까?는 만족할 수 있겠지만, 페이지가 리렌더링 될때마다 항상 new QueryClient()를 통해 queryClient가 재 생성된다는 문제가 있었다.
그래서 Needs#2. 그러면서도 리렌더링이 일어날 때마다 새로운 queryClient를 만들지 않았으면 좋겠다.를 만족하기 위해 바로 lazy initialization이라는 개념을 활용할 수 있단 것을 알게 되었다.
(병연튜터님께 감사를 드립니다 🙏🏻)
여기서 문제! ☝🏻
지연 로딩(lazy loading)은 리액트를 다루며 몇 번 다루어 본 개념이고, next.js로 넘어와서도 Suspense를 배우면서 겨우 익숙하게 느끼게 되었다.
그런데 지연 초기화(lazy initialization)는 뭐지?
리소스를 미리 로딩하거나 할당하지 않고, 처음으로 사용될 때만 로딩/할당하여 성능을 최적화하는 방식이다.
useState를 통해 구현할 수 있다.
따라서 Next.js에서 쓰려면 "use client"선언이 필수적임.
[Hook 시리즈] Lazy initialization 이 대체 뭔데 그래서
위 블로그 글을 참고하며 이해해 본 지연 초기화.
useState(함수)는 초기화할때만 함수 실행되고, useState(함수())로 적으면 리렌더링될 때 마다 함수 실행이 되는 차이가 잘 정리되어 있어서 참고하며 공부했다.

(출처 : https://velog.io/@samkong/)
useState(함수()) 형태에서는 함수가 초기화될 때마다 실행되므로, 불필요한 계산이 이루어진다. 따라서 함수를 콜백 형태로 넘겨주는 방식(useState(함수))이 더 효율적이다. (위 블로그에서 말하는 useState(함수)의 케이스)
useState(함수)에서는 함수 자체를 값으로 넘기기 때문에, useState가 실행될 때 함수가 실행되지 않는다. 대신, useState는 해당 함수가 반환하는 값을 첫 번째 렌더링 시에만 실행하여 상태를 초기화한다. 이미 초기화가 되었다면, 그 후에는 함수가 실행되지 않는다!
함수() 형태는 함수를 실행시킨 후 결과값을 초기화하는 방식이다. 반면, 함수 콜백형태는 초기화가 필요한지 여부를 체크하고, 필요 시 내부 콜백을 실행하여 상태를 설정한다. => 불필요한 계산 방지!
최종적으로 사용한 next.js에서 typeScript로 작성한 tanstackQuery - queryClient Provider.tsx 코드이다.
"use client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { useState } from "react";
const Provider = ({ children }: { children: React.ReactNode }) => {
const [queryClient] = useState<QueryClient>(() => new QueryClient()); // lazy-initialization
return (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
};
export default Provider;
useState의 초기값에 지연 초기화 적용 => 초기 렌더링 시에만 콜백함수를 실행시켜 새로운 queryClient를 설정해준다.
리렌더링 시에도 초기값이 설정된 상태이므로, 새로운 queryClient를 생성하지 않고 기존 값을 사용하게 된다.
state 변수인 queryClient의 타입을 제네릭을 사용해 QueryClient로 선언해주어, 타입 안전성 확보.