TanStack Query: 서버 상태 관리의 끝판왕 가이드 🚀
TanStack Query (React Query)를 처음 시작하는 당신을 위해
Table Of Contents
- TL;DR
- TanStack Query란?
- TanStack Query의 기능들
- Auto Refetching (자동 재요청)
- Parallel Queries (병렬 쿼리)
- Dependent Queries (의존적인 쿼리)
- Automatic Garbage Collection (자동 가비지 컬렉션)
- Request Cancellation (요청 취소)
- Prefetching (사전 요청)
- SSR Support (SSR 지원)
- Auto Caching (자동 캐싱)
- Paginated/Cursor Queries (페이지네이션/커서 쿼리)
- Load-More/Infinite Scroll Queries (더 보기/무한 스크롤 쿼리)
- TanStack Query의 기능들
- 설치 및 사용하기
- 개념
- 여담
TL;DR
- TanStack Query(구 React Query): 웹 어플리케이션에서 서버 상태 가져오기, 캐싱, 동기화 및 업데이트를 쉽게 해 주는 라이브러리
- React에서 사용하는 법
TanStack Query란?
짧게 말하면, TanStack Query는 웹 어플리케이션에서 서버 상태 가져오기, 캐싱, 동기화 및 업데이트를 쉽게 해 주는 라이브러리다.
먼저 공식 소개글을 보자.
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는 아래와 같은 기능들을 제공한다.
나는 당장 사용할 것 같은 기능들만 알아봤다. 그런데도 이렇게 많다...😯
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 (사전 요청)
- 데이터를 미리 가져와 빠르게 보여줄 수 있다.
queryClient.prefetchQuery
를 호출해 사용할 수 있다.- 예시
await queryClient.prefetchQuery({ queryKey, queryFn });
- docs: https://tanstack.com/query/latest/docs/reference/QueryClient#queryclientprefetchquery
SSR Support (SSR 지원)
- cf) CSR과 SSR
- CSR: 서버에서 빈 HTML과 JS 파일을 클라이언트로 넘겨주면, 클라이언트에서 렌더링하는 방식
- 브라우저에서는 1. 마크업(빈 내용) -> 2. JS로 렌더링 -> 3. Query 요청 순서로 페이지가 생성된다.
- SSR: 서버에서 초기 HTML을 생성하고 클라이언트로 넘겨주는 방식
- 브라우저에서는 1. 마크업(내용 및 초기 데이터 포함) -> 2. JS를 연결해 인터렉션 가능 순서로 페이지가 생성된다.
- CSR: 서버에서 빈 HTML과 JS 파일을 클라이언트로 넘겨주면, 클라이언트에서 렌더링하는 방식
QueryClientProvider
와QueryClient
를 통해 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에서
- 새 instance에서
useQuery({ queryKey: ['todos'], queryFn: fetchTodos })
라는 요청이 들어온다.- 우선
['todos']
이라는 키로 캐싱된 데이터가 있기 때문에, 해당 데이터를 바로 반환한다. - 하지만 이 데이터는 오래된 데이터이기 때문에, 새로 쿼리 요청을 보내고, 받아온 데이터를 다시
['todos']
라는 키로 캐싱한다.
- 우선
-
- 만약 이 두 instance가 unmount되고 5분(
gcTime
)이 지나면,['todos']
으로 캐싱된 데이터는 삭제된다.
- 만약 이 두 instance가 unmount되고 5분(
-
- 만약 이 두 instance가 unmount되고 5분이 지나기 전에 새로운 instance가
useQuery({ queryKey: ['todos'], queryFn: fetchTodos })
요청을 보내면 캐싱된 데이터를 반환하고, 새로운 데이터를 받아 온 뒤에 이 데이터를 저장해둔다.
- 만약 이 두 instance가 unmount되고 5분이 지나기 전에 새로운 instance가
- 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 (더 보기/무한 스크롤 쿼리)
- "더보기" 버튼이나 무한 스크롤을 이용해 데이터를 추가적으로 로드하는 경우를 처리할 수 있다.
useInfiniteQuery
를 호출해 사용한다.- docs: https://tanstack.com/query/latest/docs/framework/react/guides/infinite-queries#infinite-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를 제공해줬다면, 이제 useQuery
나 useQueries
, 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에 연결된 비동기식 데이터에 대한 선언적 종속성...이다.
- Query를 구독하려면 고유한 key와 데이터를 resolve하거나 error를 던지는 Promise를 반환하는 함수를 전달해야 한다.
- docs: https://tanstack.com/query/latest/docs/framework/react/guides/queries
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
- Mutation은 Query와 달리, 데이터의 생성, 업데이트, 삭제나 server-side의 효과를 다루기 위한 것이다.
useMutation
hook을 호출해 사용할 수 있다.- docs: https://tanstack.com/query/latest/docs/framework/react/guides/mutations
여담
왜 이름이 TanStack일까...🤔
하고 페이지를 내려보고 있었는데
개발자 계정이 @TannerLinsley
라고 한다. 그러면 Tanner의 stack...같은 느낌이겠지?