🌐 웹개발

TanStack Query

TanStack Query

서버 상태 관리 라이브러리. 캐싱, 동기화, 업데이트 자동화.

📖 상세 설명

TanStack Query(구 React Query)는 서버 상태(server state)를 관리하는 강력한 비동기 데이터 페칭 라이브러리입니다. 캐싱, 백그라운드 업데이트, 중복 요청 제거, 낙관적 업데이트 등 복잡한 데이터 동기화 로직을 선언적으로 처리합니다.

핵심 개념으로 Query(읽기)와 Mutation(쓰기)이 있습니다. useQuery는 GET 요청에, useMutation은 POST/PUT/DELETE 요청에 사용합니다. queryKey로 캐시를 식별하고, staleTime과 cacheTime으로 캐시 전략을 세밀하게 제어합니다.

TanStack Query는 React, Vue, Solid, Svelte 등 다양한 프레임워크를 지원합니다. DevTools를 통해 캐시 상태를 시각적으로 확인할 수 있어 디버깅이 용이하며, TypeScript와의 타입 추론도 뛰어납니다.

SWR과 비교하면 TanStack Query가 더 많은 기능을 제공합니다. Infinite Query, Prefetching, Placeholder Data, Query Invalidation 등 복잡한 시나리오를 처리할 수 있어 대규모 애플리케이션에 적합합니다.

💻 코드 예제

import {
  useQuery,
  useMutation,
  useQueryClient,
  QueryClient,
  QueryClientProvider,
} from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';

// Provider 설정
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 1000 * 60 * 5, // 5분간 fresh
      retry: 3,
      refetchOnWindowFocus: true,
    },
  },
});

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <MyApp />
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  );
}

// 기본 Query
function TodoList() {
  const { data, isLoading, error, refetch } = useQuery({
    queryKey: ['todos'],
    queryFn: () => fetch('/api/todos').then(r => r.json()),
    staleTime: 1000 * 60, // 1분
  });

  if (isLoading) return <Spinner />;
  if (error) return <Error message={error.message} />;

  return (
    <ul>
      {data.map(todo => <TodoItem key={todo.id} {...todo} />)}
    </ul>
  );
}

// Mutation과 캐시 무효화
function CreateTodo() {
  const queryClient = useQueryClient();

  const mutation = useMutation({
    mutationFn: (newTodo: { title: string }) =>
      fetch('/api/todos', {
        method: 'POST',
        body: JSON.stringify(newTodo),
      }).then(r => r.json()),

    // 낙관적 업데이트
    onMutate: async (newTodo) => {
      await queryClient.cancelQueries({ queryKey: ['todos'] });
      const previousTodos = queryClient.getQueryData(['todos']);

      queryClient.setQueryData(['todos'], (old: Todo[]) => [
        ...old,
        { id: Date.now(), ...newTodo, completed: false }
      ]);

      return { previousTodos };
    },

    onError: (err, newTodo, context) => {
      queryClient.setQueryData(['todos'], context?.previousTodos);
    },

    onSettled: () => {
      queryClient.invalidateQueries({ queryKey: ['todos'] });
    },
  });

  return (
    <button
      onClick={() => mutation.mutate({ title: '새 할일' })}
      disabled={mutation.isPending}
    >
      {mutation.isPending ? '추가 중...' : '추가'}
    </button>
  );
}

// 무한 스크롤 (Infinite Query)
function InfinitePostList() {
  const {
    data,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
  } = useInfiniteQuery({
    queryKey: ['posts'],
    queryFn: ({ pageParam = 0 }) =>
      fetch(`/api/posts?cursor=${pageParam}`).then(r => r.json()),
    getNextPageParam: (lastPage) => lastPage.nextCursor,
  });

  return (
    <div>
      {data?.pages.map((page, i) => (
        <React.Fragment key={i}>
          {page.posts.map(post => <Post key={post.id} {...post} />)}
        </React.Fragment>
      ))}
      <button onClick={() => fetchNextPage()} disabled={!hasNextPage}>
        {isFetchingNextPage ? '로딩...' : '더 보기'}
      </button>
    </div>
  );
}

🗣️ 실무 대화 예시

기술 미팅에서:

"전역 상태로 서버 데이터 관리하면 동기화 지옥이에요. TanStack Query 쓰면 캐싱, 리페칭, 로딩 상태 다 알아서 처리되고, DevTools로 캐시도 바로 확인할 수 있어요."

기술 면접에서:

"staleTime과 cacheTime의 차이는요?" - "staleTime은 데이터가 fresh로 간주되는 시간이고, cacheTime은 비활성 쿼리가 메모리에 유지되는 시간입니다. stale이면 백그라운드 리페칭이 일어납니다."

코드 리뷰에서:

"mutation 성공 후 관련 쿼리 invalidate 안 하고 있네요. invalidateQueries로 목록 갱신해야 추가된 항목이 바로 보여요. 아니면 setQueryData로 직접 캐시 업데이트하세요."

⚠️ 주의사항

🔗 관련 용어

SWR React Suspense REST API Redux

📚 더 배우기

📄 TanStack Query 공식 문서 📄 TanStack Query - Mutations Guide 📄 TanStack Query - Optimistic Updates