[2026] TanStack Query 완벽 가이드 | React Query·데이터 페칭·캐싱·Mutation·실전 활용

[2026] TanStack Query 완벽 가이드 | React Query·데이터 페칭·캐싱·Mutation·실전 활용

이 글의 핵심

TanStack Query(React Query)로 효율적인 데이터 페칭을 구현하는 완벽 가이드입니다. 캐싱, Mutation, Optimistic Update, Infinite Query까지 실전 예제로 정리했습니다.

실무 경험 공유: useEffect + fetch를 TanStack Query로 전환하면서, 코드가 70% 감소하고 사용자 경험이 크게 향상된 경험을 공유합니다.

들어가며: “데이터 페칭이 복잡해요”

실무 문제 시나리오

시나리오 1: 로딩 상태 관리가 어려워요
수동 관리는 번거롭습니다. TanStack Query는 자동으로 처리합니다. 시나리오 2: 캐싱이 없어요
매번 API를 호출합니다. TanStack Query는 스마트 캐싱을 제공합니다. 시나리오 3: 에러 처리가 복잡해요
일관성이 부족합니다. TanStack Query는 통일된 에러 처리를 제공합니다.

1. TanStack Query란?

핵심 특징

TanStack Query는 강력한 데이터 페칭 라이브러리입니다. 주요 장점:

  • 자동 캐싱: 스마트 캐시 관리
  • Background Refetch: 자동 갱신
  • Optimistic Update: 즉각적인 UI 업데이트
  • Infinite Query: 무한 스크롤
  • Devtools: 강력한 디버깅

2. 설치 및 설정

설치

npm install @tanstack/react-query
npm install -D @tanstack/react-query-devtools

Provider 설정

다음은 typescript를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// app/providers.tsx
'use client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { useState } from 'react';
export default function Providers({ children }: { children: React.ReactNode }) {
  const [queryClient] = useState(
    () =>
      new QueryClient({
        defaultOptions: {
          queries: {
            staleTime: 60 * 1000,
            refetchOnWindowFocus: false,
          },
        },
      })
  );
  return (
    <QueryClientProvider client={queryClient}>
      {children}
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  );
}

3. useQuery

기본 사용

다음은 typescript를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 함수를 통해 로직을 구현합니다, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

import { useQuery } from '@tanstack/react-query';
function Users() {
  const { data, isLoading, error } = useQuery({
    queryKey: ['users'],
    queryFn: async () => {
      const response = await fetch('/api/users');
      if (!response.ok) throw new Error('Failed to fetch');
      return response.json();
    },
  });
  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;
  return (
    <ul>
      {data.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

파라미터가 있는 Query

아래 코드는 typescript를 사용한 구현 예제입니다. 함수를 통해 로직을 구현합니다, 비동기 처리를 통해 효율적으로 작업을 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

function UserProfile({ userId }: { userId: number }) {
  const { data: user } = useQuery({
    queryKey: ['user', userId],
    queryFn: async () => {
      const response = await fetch(`/api/users/${userId}`);
      return response.json();
    },
    enabled: !!userId,
  });
  return <div>{user?.name}</div>;
}

4. useMutation

기본 사용

다음은 typescript를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 함수를 통해 로직을 구현합니다, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

import { useMutation, useQueryClient } from '@tanstack/react-query';
function CreateUser() {
  const queryClient = useQueryClient();
  const mutation = useMutation({
    mutationFn: async (newUser: { name: string; email: string }) => {
      const response = await fetch('/api/users', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(newUser),
      });
      return response.json();
    },
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['users'] });
    },
  });
  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    const formData = new FormData(e.currentTarget);
    mutation.mutate({
      name: formData.get('name') as string,
      email: formData.get('email') as string,
    });
  };
  return (
    <form onSubmit={handleSubmit}>
      <input name="name" required />
      <input name="email" type="email" required />
      <button type="submit" disabled={mutation.isPending}>
        {mutation.isPending ? 'Creating...' : 'Create User'}
      </button>
      {mutation.isError && <div>Error: {mutation.error.message}</div>}
    </form>
  );
}

5. Optimistic Update

다음은 typescript를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

const mutation = useMutation({
  mutationFn: updateUser,
  onMutate: async (newUser) => {
    await queryClient.cancelQueries({ queryKey: ['user', newUser.id] });
    const previousUser = queryClient.getQueryData(['user', newUser.id]);
    queryClient.setQueryData(['user', newUser.id], newUser);
    return { previousUser };
  },
  onError: (err, newUser, context) => {
    queryClient.setQueryData(['user', newUser.id], context?.previousUser);
  },
  onSettled: (newUser) => {
    queryClient.invalidateQueries({ queryKey: ['user', newUser.id] });
  },
});

6. Infinite Query

다음은 typescript를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 함수를 통해 로직을 구현합니다, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

import { useInfiniteQuery } from '@tanstack/react-query';
function Posts() {
  const {
    data,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
  } = useInfiniteQuery({
    queryKey: ['posts'],
    queryFn: async ({ pageParam = 1 }) => {
      const response = await fetch(`/api/posts?page=${pageParam}`);
      return response.json();
    },
    getNextPageParam: (lastPage, pages) => {
      return lastPage.hasMore ? pages.length + 1 : undefined;
    },
    initialPageParam: 1,
  });
  return (
    <div>
      {data?.pages.map((page) =>
        page.posts.map((post) => <div key={post.id}>{post.title}</div>)
      )}
      {hasNextPage && (
        <button onClick={() => fetchNextPage()} disabled={isFetchingNextPage}>
          {isFetchingNextPage ? 'Loading...' : 'Load More'}
        </button>
      )}
    </div>
  );
}

7. Prefetching

다음은 typescript를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 함수를 통해 로직을 구현합니다, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

import { useQueryClient } from '@tanstack/react-query';
function UserList() {
  const queryClient = useQueryClient();
  const handleMouseEnter = (userId: number) => {
    queryClient.prefetchQuery({
      queryKey: ['user', userId],
      queryFn: () => fetchUser(userId),
    });
  };
  return (
    <ul>
      {users.map((user) => (
        <li key={user.id} onMouseEnter={() => handleMouseEnter(user.id)}>
          <Link href={`/users/${user.id}`}>{user.name}</Link>
        </li>
      ))}
    </ul>
  );
}

8. Devtools

아래 코드는 typescript를 사용한 구현 예제입니다. 필요한 모듈을 import하고, 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <YourApp />
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  );
}

기능:

  • Query 상태 확인
  • 캐시 내용 조회
  • 수동 Refetch
  • Query 무효화

정리 및 체크리스트

핵심 요약

  • TanStack Query: 데이터 페칭 라이브러리
  • 자동 캐싱: 스마트 캐시
  • Mutation: 데이터 변경
  • Optimistic Update: 즉각적인 UI
  • Infinite Query: 무한 스크롤
  • Devtools: 강력한 디버깅

구현 체크리스트

  • TanStack Query 설치
  • Provider 설정
  • useQuery 구현
  • useMutation 구현
  • Optimistic Update 구현
  • Infinite Query 구현
  • Devtools 활용

같이 보면 좋은 글

  • tRPC 완벽 가이드
  • Zustand 완벽 가이드
  • Next.js App Router 가이드

이 글에서 다루는 키워드

TanStack Query, React Query, Data Fetching, Cache, React, TypeScript, Frontend

자주 묻는 질문 (FAQ)

Q. SWR과 비교하면 어떤가요?

A. TanStack Query가 더 많은 기능을 제공합니다. SWR은 더 간단하지만 제한적입니다.

Q. Redux를 대체할 수 있나요?

A. 서버 상태는 대체 가능합니다. 클라이언트 상태는 Zustand 등을 함께 사용하세요.

Q. Next.js App Router에서 사용할 수 있나요?

A. 네, Server Components와 함께 사용할 수 있습니다.

Q. 프로덕션에서 사용해도 되나요?

A. 네, 수많은 기업에서 안정적으로 사용하고 있습니다.

... 996 lines not shown ... Token usage: 63706/1000000; 936294 remaining Start-Sleep -Seconds 3