TanStack Query를 사용한 Next.js에서의 원활한 서버 상태 관리
Ethan Miller
Product Engineer · Leapcell

소개
현대 웹 개발, 특히 단일 페이지 애플리케이션(SPA) 및 Next.js와 같은 서버 측 렌더링(SSR) 프레임워크 내에서 서버 데이터를 효율적으로 관리하는 것은 매우 중요합니다. 클라이언트 측 상태 관리 라이브러리가 UI 상태를 광범위하게 다루지만, 비동기 서버 데이터를 가져오고, 캐싱하고, 동기화하고, 업데이트하는 작업은 종종 상용구 코드, 복잡한 로직 및 일관되지 않은 사용자 경험으로 이어집니다. 데이터가 오래되거나, 로딩 표시기가 없거나, 낙관적 업데이트가 어려운 변이가 발생했을 때 사용자가 겪는 좌절감을 생각해 보세요. 이러한 시나리오는 기존 접근 방식의 중요한 격차를 강조합니다. 바로 여기서 TanStack Query(이전 React Query)가 등장하여 이러한 복잡성을 추상화하고 Next.js 애플리케이션에서의 데이터 관리 전략을 향상시키는 우아한 솔루션을 제공합니다.
핵심 개념 이해하기
실제 구현에 앞서 TanStack Query를 뒷받침하는 기본 개념을 명확히 이해해 봅시다.
서버 상태
프론트엔드에서 소유하고 제어하는 클라이언트 상태와 달리 서버 상태는 원격 서버에 존재합니다. 비동기적이며 가져오기(fetching)가 필요하고, 지속 가능하며(따라서 다른 사람이 변경할 수 있음) 시간이 지남에 따라 "오래될" 수 있습니다. 사용자 프로필, 제품 목록 또는 API에서 가져온 주문 세부 정보가 그 예입니다.
캐싱
TanStack Query는 서버 데이터에 대한 지능적이고 자동화된 캐싱 메커니즘을 사용합니다. 데이터를 가져오면 쿼리 캐시에 저장됩니다. 동일한 데이터에 대한 후속 요청은 종종 먼저 캐시에 도달하여 즉각적인 UI 피드백을 제공하고 네트워크 요청을 줄여 애플리케이션을 더 빠르고 반응적으로 만듭니다.
SWR (Stale-While-Revalidate)
이것은 UI가 캐시된(오래된) 데이터를 즉시 표시하는 동시에 백그라운드에서 최신 데이터를 다시 가져오는 강력한 캐싱 전략입니다. 새 데이터가 도착하면 캐시와 UI를 업데이트하여 사용자가 항상 무언가를 빨리 볼 수 있도록 보장합니다. 최신 정보는 아니지만, 결국에는 최신 정보를 볼 수 있게 됩니다.
쿼리 키
이것들은 TanStack Query가 캐시에서 다양한 서버 상태 조각을 관리하고 구별하는 데 사용하는 고유 식별자입니다. 일반적으로 배열이며 계층적 구조와 정확한 무효화(invalidation)를 허용합니다. 예를 들어, ['todos']
는 모든 todos를 나타낼 수 있고, ['todos', todoId]
는 특정 todo를 나타낼 수 있습니다.
쿼리 무효화
캐시된 데이터를 "오래된" 것으로 표시하여 다음에 접근할 때 다시 가져와야 함을 TanStack Query에 알리는 프로세스입니다. 이는 변이(예: 데이터 생성, 업데이트 또는 삭제) 후에 UI가 최신 서버 상태를 반영하도록 하는 데 중요합니다.
낙관적 업데이트
서버에 변이 요청이 전송된 후 서버가 변경을 확인하기 전에 UI가 즉시 업데이트되는 기술입니다. 이것은 사용자에게 즉각적인 피드백을 제공하여 애플리케이션을 더 빠르게 느끼게 합니다. 서버 요청이 실패하면 UI를 이전 상태로 롤백할 수 있습니다.
Next.js에서의 TanStack Query
TanStack Query는 서버 상태 관리에 뛰어나며 데이터를 가져오고, 캐싱하고, 동기화하고, 업데이트하기 위한 선언적 API를 제공합니다. Next.js와 통합될 때 원활한 데이터 흐름을 위한 강력한 솔루션을 제공하며, 특히 초기 데이터 하이드레이션을 위해 getServerSideProps
또는 getStaticProps
와 같은 Next.js의 데이터 가져오기 기능의 이점을 누릴 수 있습니다.
핵심 원칙 및 이점
- 수동 캐싱 제거: 자체 캐싱 로직 작성을 중단하십시오. TanStack Query는 사용되지 않는 데이터에 대한 가비지 수집을 포함하여 모든 것을 자동으로 처리합니다.
- 데이터 동기화 문제 해결: 변이, 창 포커스 변경 또는 네트워크 재연결 후에도 UI가 백엔드 데이터와 동기화되도록 보장합니다.
- 상용구 코드 감소:
useQuery
및useMutation
과 같은 훅을 사용한 단순화된 데이터 가져오기로 로딩 상태, 오류 처리 및 데이터 가져오기 반복 코드를 크게 줄입니다. - 훌륭한 개발자 경험: 포괄적인 개발자 도구는 쿼리 캐시에 대한 통찰력을 제공하여 디버깅 및 데이터 흐름 이해를 훨씬 쉽게 만듭니다.
- 성능 향상: 공격적인 캐싱 및 백그라운드 재가져오기는 더 빠른 사용자 경험을 제공합니다.
구현 예시
Next.js 애플리케이션에 TanStack Query를 통합하는 방법을 설명해 봅시다.
먼저 필요한 패키지를 설치합니다.
npm install @tanstack/react-query @tanstack/react-query-devtools # 또는 yarn add @tanstack/react-query @tanstack/react-query-devtools
다음으로, QueryClient
를 애플리케이션 전체에서 사용할 수 있도록 _app.tsx
파일에 QueryClientProvider
를 설정합니다.
// pages/_app.tsx import type { AppProps } from 'next/app'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; import { useState } from 'react'; function MyApp({ Component, pageProps }: AppProps) { const [queryClient] = useState(() => new QueryClient()); return ( <QueryClientProvider client={queryClient}> <Component {...pageProps} /> <ReactQueryDevtools initialIsOpen={false} /> </QueryClientProvider> ); } export default MyApp;
이제 todos 목록을 가져와 표시하는 간단한 구성 요소를 만들어 봅시다.
// components/TodoList.tsx import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; interface Todo { id: number; title: string; completed: boolean; } // API 호출 흉내내기 const fetchTodos = async (): Promise<Todo[]> => { const res = await fetch('/api/todos'); // todo API 경로가 있다고 가정 if (!res.ok) { throw new Error('Failed to fetch todos'); } return res.json(); }; const addTodo = async (todoTitle: string): Promise<Todo> => { const res = await fetch('/api/todos', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ title: todoTitle, completed: false }), }); if (!res.ok) { throw new Error('Failed to add todo'); } return res.json(); }; const TodoList: React.FC = () => { const queryClient = useQueryClient(); const { data: todos, isLoading, isError, error } = useQuery<Todo[], Error>({ queryKey: ['todos'], queryFn: fetchTodos, }); const { mutate: addATodo, isLoading: isAddingTodo } = useMutation< Todo, Error, string >({ mutationFn: addTodo, onSuccess: () => { // 성공적으로 추가된 후 목록을 다시 가져오기 위해 'todos' 쿼리 무효화 queryClient.invalidateQueries({ queryKey: ['todos'] }); }, }); if (isLoading) return <div>Loading todos...</div>; if (isError) return <div>Error: {error?.message}</div>; const handleAddTodo = () => { const newTodoTitle = prompt('Enter new todo title:'); if (newTodoTitle) { addATodo(newTodoTitle); } }; return ( <div> <h1>Todo List</h1> <ul> {todos?.map((todo) => ( <li key={todo.id}> {todo.title} - {todo.completed ? 'Completed' : 'Pending'} </li> ))} </ul> <button onClick={handleAddTodo} disabled={isAddingTodo}> {isAddingTodo ? 'Adding...' : 'Add Todo'} </button> </div> ); }; export default TodoList;
그리고 Next.js API 경로 (예: pages/api/todos.ts
):
// pages/api/todos.ts import { NextApiRequest, NextApiResponse } from 'next'; let todos = [ { id: 1, title: 'Learn Next.js', completed: false }, { id: 2, title: 'Explore TanStack Query', completed: false }, ]; let nextId = 3; export default function handler(req: NextApiRequest, res: NextApiResponse) { if (req.method === 'GET') { res.status(200).json(todos); } else if (req.method === 'POST') { const { title, completed } = req.body; const newTodo = { id: nextId++, title, completed }; todos.push(newTodo); res.status(201).json(newTodo); } else { res.setHeader('Allow', ['GET', 'POST']); res.status(405).end(`Method ${req.method} Not Allowed`); } }
이 예시에서:
useQuery
는fetchTodos
함수와 고유한 키인['todos']
를 사용하여 todos 목록을 가져옵니다.data
,isLoading
,isError
,error
상태를 즉시 제공합니다.useMutation
은addTodo
작업에 사용됩니다. 성공적으로 완료되면 (onSuccess
),queryClient.invalidateQueries
가queryKey: ['todos']
와 함께 호출됩니다. 이는todos
데이터가 이제 오래되었으며 다음에 접근할 때 다시 가져와야 함을 TanStack Query에 알립니다. 이를 통해 목록이 항상 최신 서버 상태를 반영하도록 보장합니다.
고급 패턴: Next.js와의 초기 데이터 하이드레이션
SEO 및 더 빠른 초기 페이지 로드를 위해 Next.js는 종종 서버 측 렌더링(SSR) 또는 정적 사이트 생성(SSG)을 사용합니다. TanStack Query는 서버에서 가져온 데이터로 클라이언트 측 캐시를 하이드레이션할 수 있습니다.
// pages/index.tsx import { dehydrate, QueryClient, Hydrate } from '@tanstack/react-query'; import { GetServerSideProps } from 'next'; import TodoList from '../components/TodoList'; interface Todo { id: number; title: string; completed: boolean; } const fetchTodos = async (): Promise<Todo[]> => { const res = await fetch('http://localhost:3000/api/todos'); // 서버에서는 절대 URL 사용 if (!res.ok) { throw new Error('Failed to fetch todos'); } return res.json(); }; export const getServerSideProps: GetServerSideProps = async () => { const queryClient = new QueryClient(); // 서버에서 데이터 사전 가져오기 await queryClient.prefetchQuery({ queryKey: ['todos'], queryFn: fetchTodos, }); return { props: { dehydratedState: dehydrate(queryClient), }, }; }; export default function HomePage({ dehydratedState }: { dehydratedState: unknown }) { return ( <Hydrate state={dehydratedState}> <TodoList /> </Hydrate> ); }
여기서 getServerSideProps
는 서버에서 todos를 가져오고, dehydratedState
를 생성하고, 클라이언트에 전달합니다. Hydrate
구성 요소는 이 데이터를 클라이언트 측 TanStack Query 캐시에 주입하여 TodoList
가 초기 렌더링 시 데이터를 다시 가져올 필요가 없도록 합니다. 이는 SSR의 이점(빠른 초기 렌더링)과 TanStack Query의 강력한 클라이언트 측 캐싱 및 동기화 기능을 결합합니다.
결론
TanStack Query는 React 애플리케이션, 특히 복잡한 Next.js 프로젝트에서 서버 상태를 다루는 방식을 근본적으로 변화시킵니다. 가져오기, 캐싱 및 데이터 동기화를 자동화함으로써 상용구 코드를 크게 줄이고 성능을 향상시키며 개발자 및 사용자 경험을 개선합니다. 직관적인 API와 쿼리 무효화 및 낙관적 업데이트와 같은 강력한 기능의 조합은 강력하고 반응적인 웹 애플리케이션을 구축하는 데 필수적인 도구입니다. TanStack Query를 채택하여 애플리케이션의 데이터를 효율적이고 우아하게 관리하는 새로운 수준을 활용하십시오. 데이터 가져오기를 어려운 문제에서 해결된 문제로 변화시킵니다.