Next.js에서 tanstack/react-query 도입기

질문Bot·2025년 6월 21일

Next.js

목록 보기
6/13
post-thumbnail

💡 tanstack/react-query 사용이유

프로젝트를 진행하면서 Next.js에서 별도의 상태 관리 라이브러리를 도입할 필요가 있을까? 라는 고민을 정말 많이 했습니다.

사실 Next.js는 fetch 하나만으로도 충분히 데이터 패칭이 가능하고, useState로 간단하게 상태도 관리할 수 있습니다. 그런데 문제가 하나씩 복잡해지기 시작하면서 이렇게 단순하게 가기엔 한계가 명확했습니다.

예를 들자면 이런 상황에서 좋다!

  • ✅ 유저에 따라 달라지는 개인화된 검색 결과
  • ✅ 서버에서 미리 받아오고, 클라이언트에서는 재요청 없이 바로 쓰고 싶었던 초기 데이터
  • ✅ 무한스크롤처럼 클라이언트에서 페이징을 연동해야 하는 데이터

이런 걸 직접 하나하나 상태 관리로 짜려니까 너무 복잡해졌다.
그래서 결국 React Query를 도입했다.
결론부터 말하면, 지금은 "잘 썼다" 라는 생각이 든다.


🍴 설치 (npm 환경)

npm install @tanstack/react-query


react query 설정

📁 get-query-client.ts

CSR/SSR 환경 구분해서 QueryClient를 생성해주는 유틸입니다.
서버에선 매번 새로 만들고, 브라우저에선 싱글톤으로 유지되게 처리.

import { QueryClient, defaultShouldDehydrateQuery, isServer } from '@tanstack/react-query';

function makeQueryClient() {
  return new QueryClient({
    defaultOptions: {
      queries: {
        staleTime: 60 * 1000, //SSR 이후 클라이언트에서 즉시 refetch가 일어나는 걸 방지함


      },
      dehydrate: {
        shouldDehydrateQuery: (query) => defaultShouldDehydrateQuery(query) || query.state.status === 'pending',
      },
    },
  });
}

let browserQueryClient: QueryClient | undefined = undefined;

export function getQueryClient() {
  if (isServer) {
    return makeQueryClient(); // SSR: 항상 새 인스턴스 생성
  } else {
    // CSR: 브라우저에서는 싱글톤 사용
    if (!browserQueryClient) browserQueryClient = makeQueryClient(); 
    return browserQueryClient;
  }
}

🧵 Provider 설정 – providers.tsx

클라이언트 전역에 QueryClientProvider를 붙여주는 용도입니다.
layout.tsx에서 감싸주면 끝!

'use client';
import { QueryClientProvider } from '@tanstack/react-query';
import { getQueryClient } from './get-query-client';
import type * as React from 'react';

export default function Providers({ children }: { children: React.ReactNode }) {
  const queryClient = getQueryClient();

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

🎯 layout.tsx에서 전체 앱을 감싸기

import Providers from './providers'

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

💡 미리 데이터를 땡겨오고 싶다면? (SSR Prefetch + Hydration)

서버에서 데이터를 먼저 가져오고, 클라이언트에서는 다시 요청하지 않고 바로 사용하는 구조입니다.

🖥 서버 컴포넌트

import {
  dehydrate,
  HydrationBoundary,
  QueryClient,
} from '@tanstack/react-query'
import TestPage from './testpage'

export default async function ServerPage() {
  const queryClient = new QueryClient()

  await queryClient.prefetchQuery({
    queryKey: ['test'],
    queryFn: getTest,
  })

  return (
    <HydrationBoundary state={dehydrate(queryClient)}>
      <TestPage />
    </HydrationBoundary>
  )
}

💻 클라이언트 컴포넌트

'use client'

export default function TestPage() {
  const { data } = useQuery({
    queryKey: ['test'],
    queryFn: () => getTest(),
  })
  
  ....
}

😎 써보면서 느낀 장점

  • 캐싱이 진짜 좋습니다. 네트워크 요청수도 줄고 규모가 큰 프로젝트의 UX도 부드러워지는것이 느껴지네요.
  • queryKey만 잘 정리하면 API 재사용성이 매우 좋아집니다.
  • 로딩/에러/데이터 상태가 다 useQuery 하나에 녹아 있어서 코드가 깔끔해진다는 장점..
  • CSR/SSR을 같은 흐름으로 쓸 수 있습니다 → 데이터 패칭 흐름이 깔끔하게 정리됨

🍀 도입하면서 느낀 추가 포인트

🔑 1. Query Key 설계를 꼼꼼하게 해야 한다

React Query의 중심은 결국 queryKey라고 생각합니다.
이게 API의 주소이자 캐시의 식별자 역할을 하기 때문에,
처음부터 명확한 규칙을 갖고 설계하지 않으면 나중에 관리가 굉장히 어렵습니다.

특히 프로젝트가 커질수록...

  • ['search', params]
  • ['search', { keyword, subject }]
  • ['search-result', { keyword, subject }]

이렇게 쿼리 키에 일관성이 깨지기 시작하면
invalidate나 refetch 시점에서 “이게 어디까지 영향을 미치지?” 고민하게 됩니다.

👉 그래서 초기에 queryKey 네이밍 컨벤션을 잘 정해놓는 게 진짜 중요합니다.!!
(저의 추천은 => 리소스 단위로 ['user', id], ['post', postId], ['search', filters])

🌀 2. 처음에는 CSR로 개발하고 → 이후에 SSR로 확장 가능

React Query는 CSR(Client Side Rendering)만으로도 충분히 강력한 라이브러리입니다.

처음에는 그냥 클라이언트에서 useQuery()로 데이터만 잘 불러오고,
캐싱, 로딩 상태도 알아서 잘 처리되니까
진입장벽도 낮고, 일단 써보면 좋다(?)라는 생각을 합니다.

그리고 추후 SEO나 초기 로딩 최적화가 필요해지면?

그때 prefetchQuery() + dehydrate() 조합으로
SSR 구조로 확장하는것도 좋은 방법이라고 생각을 합니다.

무리하게 처음부터 SSR로 진입할 필요 없이,
단계적으로 CSR → SSR → hybrid로 발전시킬 수 있는 구조라는 게 정말 큰 장점입니다.

참고 자료

profile
유용한 정보를 전달하는 사람이 되고자 노력합니다.

0개의 댓글