[Next.js] tanstack Query 세팅 최적화

y1nlog·2025년 3월 14일
post-thumbnail

Next.js에서 탠스택쿼리 세팅하는 법이 워낙 헷갈렸기에 정리해보는 오늘의 주제.

tanstack Query 세팅 최적화

처음 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를 사용한다.

queryClient 사용

// 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을 적극 활용하려고 할 때는 위 코드 형태가 적합하다.

lazy initialization

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)는 뭐지?

💎 lazy initialization이란

리소스를 미리 로딩하거나 할당하지 않고, 처음으로 사용될 때만 로딩/할당하여 성능을 최적화하는 방식이다.

useState를 통해 구현할 수 있다.
따라서 Next.js에서 쓰려면 "use client"선언이 필수적임.


[Hook 시리즈] Lazy initialization 이 대체 뭔데 그래서
위 블로그 글을 참고하며 이해해 본 지연 초기화.

useState(함수)는 초기화할때만 함수 실행되고, useState(함수())로 적으면 리렌더링될 때 마다 함수 실행이 되는 차이가 잘 정리되어 있어서 참고하며 공부했다.

(출처 : https://velog.io/@samkong/)

  • useState(함수()) 형태에서는 함수가 초기화될 때마다 실행되므로, 불필요한 계산이 이루어진다. 따라서 함수를 콜백 형태로 넘겨주는 방식(useState(함수))이 더 효율적이다. (위 블로그에서 말하는 useState(함수)의 케이스)

  • useState(함수)에서는 함수 자체를 값으로 넘기기 때문에, useState가 실행될 때 함수가 실행되지 않는다. 대신, useState는 해당 함수가 반환하는 값을 첫 번째 렌더링 시에만 실행하여 상태를 초기화한다. 이미 초기화가 되었다면, 그 후에는 함수가 실행되지 않는다!

  • 함수() 형태는 함수를 실행시킨 후 결과값을 초기화하는 방식이다. 반면, 함수 콜백형태는 초기화가 필요한지 여부를 체크하고, 필요 시 내부 콜백을 실행하여 상태를 설정한다. => 불필요한 계산 방지!

지연 초기화(lazy initialization) 이용하기

최종적으로 사용한 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로 선언해주어, 타입 안전성 확보.

profile
FE / Data Science

0개의 댓글