TanStack Query: 서버 상태 관리의 끝판왕 가이드 🚀

TanStack Query (React Query)를 처음 시작하는 당신을 위해


Table Of Contents


TL;DR


  • TanStack Query(구 React Query): 웹 어플리케이션에서 서버 상태 가져오기, 캐싱, 동기화 및 업데이트를 쉽게 해 주는 라이브러리
  • React에서 사용하는 법
    1. npm i @tanstack/react-query 등으로 @tanstack/react-query 설치 (패키지 매니저는 자유롭게 사용)
    2. QueryClientProvider로 app 감싸주기 자세히 →
    3. useQuery 등의 hook으로 query 요청하기 자세히 →

TanStack Query란?


짧게 말하면, TanStack Query는 웹 어플리케이션에서 서버 상태 가져오기, 캐싱, 동기화 및 업데이트를 쉽게 해 주는 라이브러리다.

 

먼저 공식 소개글을 보자.

1. main description

Powerful asynchronous state management for TS/JS, React, Solid, Vue, Svelte and Angular

TS/JS, React, Solid, Vue, Svelte and Angular를 위한 강력한 비동기 상태 관리 매니저

TanStack Query는 v3까지는 React Query라는 이름으로 제공되었는데, v4부터 React 뿐만이 아니라 Solid, Vue, Svelte, Angular같은 프레임워크, 그리고 Vanilla JS/TS에서도 사용할 수 있도록 확장되었다.

더불어, TanStack에는 TanStack Query 말고도 풀스택 React 프레임워크인 TanStack Start(알파버전), React를 위한 라우팅 라이브러리 TanStack Router, 표 형식의 데이터 표시와 상호작용을 편하게 해 주는 TanStack Table, form 상태 관리를 위한 TanStack From같이 편의성을 위한 라이브러리가 많으니 한번 알아보면 좋을 것 같다.

TanStack Query의 기능들

TanStack Query는 아래와 같은 기능들을 제공한다.

2. features

나는 당장 사용할 것 같은 기능들만 알아봤다. 그런데도 이렇게 많다...😯

Auto Refetching (자동 재요청)

  • TanStack 쿼리는 쿼리 데이터가 오래되었으면 백그라운드에서 자동으로 새 데이터를 요청할 수 있다.

  • refetchOnMount, refetchOnWindowFocus, refetchOnReconnect 옵션을 통해서 설정할 수 있다.

    • refetchOnMount:
    • refetchOnWindowFocus:
    • refetchOnReconnect:
  • 설정 가능한 옵션은 이렇다.

    • true(기본값): 만약 데이터가 오래되었으면 새로 데이터를 요청한다.
    • false: 데이터를 새로 요청하지 않는다.
    • "always": 항상 데이터를 새로 요청한다.
    • 함수(((query: Query) => boolean | "always")): 다음 함수에 따라서 데이터를 요청할 지 결정한다.
  • 예시: 사용자가 다시 윈도우로 돌아왔을 때, 데이터가 오래되었으면 새로 데이터를 요청한다.

    const { data, error, isLoading } = useQuery( "yourQueryKey", fetchDataFunction, { refetchOnWindowFocus: true, // true, false, "always", 또는 함수 중 하나를 전달한다. } );
  • docs: https://tanstack.com/query/latest/docs/framework/react/reference/useQuery#usequery

Parallel Queries (병렬 쿼리)

  • 여러 쿼리를 병렬로 동시에 실행할 수 있다.

  • 실행해야 하는 쿼리 수가 동적으로 변하는 경우, hook 규칙에 위배되기 때문에 for문 등을 사용할 수 없다.

    • 잘못된 예시
      function App({ users }) { for (let i = 0; i <= COUNT; i++) { const usersQuery = useQuery({ queryKey: [`user ${i}`], queryFn: () => fetchUserById(user.id), }); } }
  • 이런 경우, useQueries를 호출하여 아래와 같이 수가 정해지지 않은 쿼리를 동적으로 실행할 수 있다.

  • 예시

    function App({ users }) { const userQueries = useQueries({ queries: users.map((user) => { return { queryKey: ["user", user.id], queryFn: () => fetchUserById(user.id), }; }), }); }
  • docs: https://tanstack.com/query/latest/docs/framework/react/guides/parallel-queries#parallel-queries

Dependent Queries (의존적인 쿼리)

  • 이전 쿼리의 결과로 다른 쿼리를 실행할 수 있다.

  • enabled 옵션을 통해서 설정할 수 있다.

  • 예시: useQuery에서 유저 정보에 따라서 유저의 프로젝트를 받아 오는 예시

    // 1. 유저를 받아오기 const { data: user } = useQuery({ queryKey: ["user", email], queryFn: getUserByEmail, }); const userId = user?.id; // 2. 유저 정보에 따라서 프로젝트 받아오기 const { status, fetchStatus, data: projects, } = useQuery({ queryKey: ["projects", userId], queryFn: getProjectsByUser, // enabled로 userId를 주어서, userId가 들어오기 전까지는 쿼리가 실행되지 않는다. enabled: !!userId, });
  • docs: https://tanstack.com/query/latest/docs/framework/react/guides/dependent-queries

Automatic Garbage Collection (자동 가비지 컬렉션)

  • 사용하지 않는 쿼리 데이터를 메모리에서 자동으로 삭제한다.

Request Cancellation (요청 취소)

  • 쿼리가 오래되었거나, 비활성화되면 쿼리를 중단할 수 있다. 쿼리가 중단되면 상태는 쿼리를 요청하기 전 상태로 revert된다.

    • 기본적으로, 쿼리는 Promise가 resolve되지 않는 한(컴포넌트가 unmount되어도!) 취소되지 않는다.

    • 이런 경우, queryClient.cancelQueries({queryKey })를 호출하여 수동으로 쿼리를 중지할 수 있다.

    • ex) 요청이 완료되는 데 오랜 시간이 걸리면, 취소 버튼을 클릭하여 요청을 중지한다.

      const query = useQuery({ queryKey: ["todos"], queryFn: async ({ signal }) => { const resp = await fetch("/todos", { signal }); return resp.json(); }, }); const queryClient = useQueryClient(); return ( <button onClick={(e) => { e.preventDefault(); queryClient.cancelQueries({ queryKey: ["todos"] }); }} > Cancel </button> );
  • docs: https://tanstack.com/query/latest/docs/framework/react/guides/query-cancellation

Prefetching (사전 요청)

SSR Support (SSR 지원)

  • cf) CSR과 SSR
    • CSR: 서버에서 빈 HTML과 JS 파일을 클라이언트로 넘겨주면, 클라이언트에서 렌더링하는 방식
      • 브라우저에서는 1. 마크업(빈 내용) -> 2. JS로 렌더링 -> 3. Query 요청 순서로 페이지가 생성된다.
    • SSR: 서버에서 초기 HTML을 생성하고 클라이언트로 넘겨주는 방식
      • 브라우저에서는 1. 마크업(내용 및 초기 데이터 포함) -> 2. JS를 연결해 인터렉션 가능 순서로 페이지가 생성된다.
  • QueryClientProviderQueryClient를 통해 query 요청을 서버에서 보내는 기능을 지원한다.
  • docs: https://tanstack.com/query/latest/docs/framework/react/guides/ssr

Auto Caching (자동 캐싱)

  • 데이터 요청 결과를 캐시에 저장하고, 동일한 요청 시 캐싱된 데이터를 반환한다.
  • 예시) garbage collection 시간(gcTime)이 5분이고, 데이터가 오래되었다고 간주하는 시간(staleTime)이 0(=즉시)이라고 가정한다.
    • 어떤 instance에서 useQuery({ queryKey: ['todos'], queryFn: fetchTodos })으로 요청이 들어온다.
    • 아직 ['todo']라는 키로 요청이 들어온 적이 없기 때문에, 새로 요청을 보내고, 데이터를 가져와 ['todos']라는 키로 캐싱한다.
    • staleTime이 0이므로 이 데이터는 바로 오래된 것으로 간주된다. 즉, 다음 요청에서 데이터를 새로 받아와야 하는 상태가 된다.
  • 새 instance에서 useQuery({ queryKey: ['todos'], queryFn: fetchTodos })라는 요청이 들어온다.
    • 우선 ['todos']이라는 키로 캐싱된 데이터가 있기 때문에, 해당 데이터를 바로 반환한다.
    • 하지만 이 데이터는 오래된 데이터이기 때문에, 새로 쿼리 요청을 보내고, 받아온 데이터를 다시 ['todos']라는 키로 캐싱한다.
    1. 만약 이 두 instance가 unmount되고 5분(gcTime)이 지나면, ['todos']으로 캐싱된 데이터는 삭제된다.
    1. 만약 이 두 instance가 unmount되고 5분이 지나기 전에 새로운 instance가 useQuery({ queryKey: ['todos'], queryFn: fetchTodos }) 요청을 보내면 캐싱된 데이터를 반환하고, 새로운 데이터를 받아 온 뒤에 이 데이터를 저장해둔다.
  • docs: https://tanstack.com/query/latest/docs/framework/react/guides/caching

Paginated/Cursor Queries (페이지네이션/커서 쿼리)

  • 페이지의 이동으로 인해 쿼리가 여러 번 요청되면, UI상에서 pending 상태와 success 상태가 번갈아서 보일 수 있다.
  • placeholderData: keepPreviousData 옵션을 이용하면 새로운 쿼리가 성공하기 전까지 이전 데이터를 보여줌으로써 혼란을 줄일 수 있다.
  • 예시
    const { isPending, isError, error, data, isFetching, isPlaceholderData } = useQuery({ queryKey: ["projects", page], queryFn: () => fetchProjects(page), placeholderData: keepPreviousData, });
  • docs: https://tanstack.com/query/latest/docs/framework/react/guides/paginated-queries#paginated-lagged-queries

Load-More/Infinite Scroll Queries (더 보기/무한 스크롤 쿼리)

설치 및 사용하기


설치하기

우선 라이브러리를 설치해주자! npm이나 yarn 등, 원하는 패키지 매니저로 설치할 수 있다.

npm i @tanstack/react-query

또는

pnpm add @tanstack/react-query

또는

yarn add @tanstack/react-query

또는

bun add @tanstack/react-query

ESLint 플러그인 또한 제공해주고 있으니, ESLint와 함께 사용하고 싶으면 npm i -D @tanstack/eslint-plugin-query처럼 ESLint 플러그인도 함께 설치해주자. 설정은 .eslintrc 등에서 따로 해줘야 한다!

세팅하기

TanStack Query의 hook들을 이용하려면 <QueryClientProvider>를 이용해서 앱을 한번 감싸주어야 한다. 그렇지 않으면 ``를 찾을 수 없기 때문에 에러가 발생하게 된다.

import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; const queryClient = new QueryClient(); function App() { return ( <QueryClientProvider client={queryClient}> {/* 다른 컴포넌트들 */} </QueryClientProvider> ); }

useQuery로 하나의 데이터 불러오기

Provider로 queryClient를 제공해줬다면, 이제 useQueryuseQueries, useInfiniteQuery같은 hook들을 사용할 수 있다.

query를 요청할 때에는 꼭 query key를 지정해야 한다.

아래 예시에서는 useQuery({ queryKey: ["todos"], queryFn: getTodos })에서 볼 수 있듯이, ["todos"]라는 key를 제공하고 있다.

import { useQuery, useMutation, useQueryClient, QueryClient, QueryClientProvider, } from "@tanstack/react-query"; import { getTodos, postTodo } from "../my-api"; // Create a client const queryClient = new QueryClient(); function App() { return ( // Provide the client to your App <QueryClientProvider client={queryClient}> <Todos /> </QueryClientProvider> ); } function Todos() { // Access the client const queryClient = useQueryClient(); // Queries const query = useQuery({ queryKey: ["todos"], queryFn: getTodos }); // Mutations const mutation = useMutation({ mutationFn: postTodo, onSuccess: () => { // Invalidate and refetch queryClient.invalidateQueries({ queryKey: ["todos"] }); }, }); return ( <div> <ul> {query.data?.map((todo) => ( <li key={todo.id}>{todo.title}</li> ))} </ul> <button onClick={() => { mutation.mutate({ id: Date.now(), title: "Do Laundry", }); }} > Add Todo </button> </div> ); } render(<App />, document.getElementById("root"));

개념


Query

Query Key

  • 고유한 키는 TanStack Query 내부에서 refetch나 caching 등에 이용된다.
  • 간단하게는 string 하나만 들어 있는 배열로 사용할 수 있고, 더 복잡한 앱에서는 string 배열 또는 중첩된 object도 사용할 수 있다.
  • 예시
    • 문자열 하나: useQuery({ queryKey: ['todos'], ... })
    • 문자열 여러개: useQuery({ queryKey: ['something', 'special'], ... })
    • id를 포함하는 경우: useQuery({ queryKey: ['todo', 5], ... })
    • object를 포함하기: useQuery({ queryKey: ['todos', { type: 'done' }], ... })
    • 변수와 사용하기: useQuery({ queryKey: ['todos', todoId], ... })
  • docs: https://tanstack.com/query/latest/docs/framework/react/guides/query-keys#array-keys-with-variables

Query Functions

  • 쿼리 함수는 Promise를 반환하고, 이 Promise는 데이터를 resolve하거나 error를 던져야 한다.
  • 예시
    useQuery({ queryKey: ["todos"], queryFn: fetchAllTodos }); useQuery({ queryKey: ["todos", todoId], queryFn: () => fetchTodoById(todoId), }); useQuery({ queryKey: ["todos", todoId], queryFn: async () => { const data = await fetchTodoById(todoId); return data; }, }); useQuery({ queryKey: ["todos", todoId], queryFn: ({ queryKey }) => fetchTodoById(queryKey[1]), });
  • docs: https://tanstack.com/query/latest/docs/framework/react/guides/query-functions

반환값

  • useQuery등의 hook을 통해 반환된 데이터는 status, data, error를 포함한다.
    • status === 'error'인 경우, error에서 에러를 확인할 수 있다. status === 'success'인 경우, data에서 쿼리의 결과값을 확인할 수 있다.
  • 또한, isPending(status === 'pending'), isError(status === 'error'), isSuccess(status === 'success')처럼 query의 처리 상태도 반환한다.
const result = useQuery({ queryKey: ["todos"], queryFn: fetchTodoList });

Mutation

여담


3. make me a title

왜 이름이 TanStack일까...🤔

하고 페이지를 내려보고 있었는데

개발자 계정이 @TannerLinsley라고 한다. 그러면 Tanner의 stack...같은 느낌이겠지?