맨처음 Tanstack-query에 대해 접하게 된 것은 1명의 멘토와 소수의 멘티들을 관리하는 프론트엔드 프로그램이 있다고 해서 참여하였다가 해당 라이브러리를 알게 되었다.
해당 프로그램에서 NewYorkTimes에서 발행하는 오픈 API를 사용해 뉴스 기사들을 필터하고 무한 스크롤로 받아볼 수 있는 서비스를 제작해보라는 과제를 내주셨다. 그때, 서버 상태를 관리하기 위해서 Tanstack-query를 많이 사용하니, 공식 문서를 직접 읽어보며 적용 시켜보라는 말씀을 해주셨다.

react 공식 문서도 한참을 해매며 찾던 내가,,,개념을 잘 알지도 못하는 server state를 관리하기 위해서 새로운 라이브러리의 (심지어 영문의,,,) 공식 문서를 떠돌기란 절대 쉽지 않은 경험이었고,,,^_^
꾸역꾸역 무한 스크롤을 구현해내기 위해서 여러 구글링 선생님들과 포스팅 면담을 하며 무사히 프로젝트는 끝냈지만, 나에게 Tanstack-query의 깊이 심연을 들여다 본다는 것은 무서운 일로 남아 있었다.
그러한 핑계로,,, 해당 서버 상태 관리 라이브러리를 사용해 왔지만 왜 사용하는가에 대해 알지 못한 채 사용하고 있었다. 하지만, 나는 이 미지의 Tanstack Query에 대해 알아보기 전에 먼저 서버 상태에 대해 알고 넘어가야 했다.
상태(State)
- 렌더링 시 영향을 미치는 자바스크립트 객체
위 상태의 정의에 따라 전역 변수는 어디에서든 접근할 수 있고, 그 object의 변화에 따라 렌더링에 영향을 미친다고 할 수 있다.
이러한 상태는 데이터에 대한 제어, 소유 여부에 따라 클라이언트 상태와 서버 상태로 구분된다.
클라이언트 상태(Client State)
- 클라이언트(react)에서 자체적으로 생성한 자바스크립트 객체
- ex) input value, UI 변경 값(모달, 사이드 바 등을 제어하는 상태 값) 등
- 동기적이고 즉각적인 업데이트
따라서 클라이언트 상태는 서버와는 상관 없이 클라이언트가 소유하고 제어하는 데이터를 의미한다.
서버 상태(Server State)
- 클라이언트 상태와는 반대로 서버에서 받아오는 모든 서버 데이터
- 비동기적인 업데이트
- 여러 클라이언트에서 공유됨
- 시간이 지나면 "오래된(stale)" 데이터가 될 수 있음
DB에 등록되어 있는 데이터를 비동기적으로 서버에서 받아와 클라이언트의 화면에 표시하는 데 필요한 데이터를 뜻한다.
이때, 받아온 데이터를 클라이언트에서 조작할 수 없기 때문에 특정 시점에서 받아온 데이터를 사용하게 된다. 서버와 클라이언트 간의 상태가 항상 일치하지 않을 수 있어, 클라이언트에서는 더 이상 유효하지 않은 데이터를 가질 수 있다.
(서버에서 데이터를 받아온 특정 시점 이후에 DB 내에서 데이터가 업데이트되거나 삭제될 수 있고, 클라이언트에서 업데이트를 위해 수정될 수 있기 때문)
다시 정리하자면, 위에서 말한 비동기적으로 받아온 데이터를 특정 시점에서 클라이언트에서 받아와 사용한다. 이 데이터가 서버와 일치하지 않은, 유효하지 않은 데이터를 클라이언트에서 사용할 수 있다. 이 부분에서 클라이언트는 서버 데이터를 자체적으로 제어하고 관리할 수 없어 서버 상태를 유동적으로 관리가 필요하다는 것이다.
이때 서버 상태를 관리하는 라이브러리인 Tanstack-query가 빛을 발한다.
react의 경우 state가 업데이트되면 렌더링이 변화하게 된다. 이 점을 보았을 때 우리는 client state와 server state를 분리하는 이유를 알 수 있다.
보통의 경우 server state는 전역으로 상태를 관리하게 된다. 그 이유는 다음과 같다.
만약 상태를 분리하지 않고 redux나 recoil, useContext 등 전역 상태 관리 라이브러리를 사용한다고 생각해보자.
하나의 store에서 client state와 server state를 제어해야할 때, 문제가 일어난다. client state는 UI에 변화를 주기 위해 사용되는데, server state는 서버에서 데이터를 제공 받기 위해 state를 업데이트하게 되면 UI의 변화를 담당하는 state까지 모두 렌더링이 실행된다. 하나의 store에 저장된 두 state 중 어느 한 쪽에만 변경이 일어난다 해도 두 컴포넌트 모두 리렌더링된다는 것이다. 이 문제를 해결하기 위해 useMemo 등을 사용하여 의존성을 사용하여 memorization 할 수 있다. 그러나 어플리케이션의 복잡도가 높아진다.

이제 무서웠던 Tanstack-query에 대해 공부가 필요한 순간이 찾아왔다.
Tanstack-query의 주요 개념에 대해 알아보자면 아래와 같다.
주요 개념들을 공식 문서 예제를 보며 좀 더 자세히 알아가보자,,^_^!
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'))
(현재 예제에서는 useMutaion 훅도 사용하고 있지만 useQuery를 위주로 설명해보고자 한다.)
각 쿼리들은 queryKey로 식별된다. 그렇기 때문에 페이지 내에서 여러 컴포넌트가 동일한 데이터를 요청하는 경우, 한번만 요청하도록 중복 요청을 제거해준다. 이제 query에서 return하는 데이터들과 옵션을 확인해보자.
status
쿼리 fetch시 현재 쿼리 실행 상태를 확인할 수 있도록 status를 함께 반환한다. 그 상태 값은 아래와 같다.
pending
쿼리에 아직 데이터가 fetch되지 않은 상태
error
쿼리에서 데이터를 fetch하다가 에러가 발생한 상태
success
쿼리에서 데이터 fetch에 성공하여 데이터를 사용할 수 있는 상태
데이터 신선도 설정 옵션, staleTime
Tanstack-query에서는 데이터의 상태를 신선도 순으로 fetching, fresh, stale, inactive로 나누어 표현한다. 그리고 staleTime 옵션을 통해 데이터의 신선도를 직접 설정할 수 있다.
data
말 그대로 query fetch 성공 시에 받아볼 수 있는 데이터이다.
이 외에 Tanstack-query에서 서버 데이터를 캐시 관리를 어떻게 하는 지를 알아야 할 것 같아 추가로 정리해봤다.
캐시의 원리를 일상에 적용하여 예시를 들자면, 길을 걷다가 카페에 음료를 주문한다고 가정했을 때 아래처럼 생각할 수 있다.
📝 카페에서 메뉴 주문 시
첫 방문 손님 (캐시가 없는 경우)
- 메뉴를 보고 주문
- 바리스타가 커피를 새로 만듦
- 시간이 더 걸림
단골 손님의 단골 메뉴 (캐시가 있는 경우)
- 바리스타가 손님 취향을 기억
- 미리 준비해둔 커피를 제공
- 빠른 서비스 가능
TanStack Query의 캐시는 이와 같은 원리로 작동한다고 할 수 있다.
캐시의 구조
['todos'], ['todos', 1], ['todos', { status: 'done' }])Tanstack-query 캐시의 기본 구조를 간략하게 작성하자면 아래와 같다.
// 1. 캐시 키 (데이터를 구분하는 유니크한 값)
const { data: todos } = useQuery({
queryKey: ['todos'], // 📍 캐시 키
queryFn: () => getTodos() // 📍 실제 데이터를 가져오는 함수
})
// 2. 더 구체적인 캐시 키
const { data: todo } = useQuery({
queryKey: ['todos', 1], // 📍 특정 할일 항목
queryFn: () => getTodo(1)
})
// 3. 필터링된 데이터의 캐시 키
const { data: completedTodos } = useQuery({
queryKey: ['todos', { status: 'completed' }], // 📍 완료된 할일들
queryFn: () => getCompletedTodos()
})
캐시 라이프사이클
🏪 편의점 도시락 구매 시
신선한 도시락 (Active Cache)
- 방금 막 진열된 새 도시락
- 현재 사용 중인 데이터 & 실시간으로 필요한 정보
유통기한 내 도시락 (Soft Cache)
- 아직 먹을 수 있지만 새로운 도시락이 들어오면 교체될 수 있음
- 당장 사용하지 않지만 곧 필요할 수 있는 데이터
- gcTime(기본 5분) 동안 유지됨
폐기 대상 도시락 (Garbage Collection)
- 유통기한이 지나 폐기해야 하는 도시락
- 더 이상 필요 없는 데이터로 판단, 메모리에서 자동으로 삭제
client state와 server state의 분리
서버 상태와 클라이언트 상태를 분리하여 제어하고, 비동기적으로 관리할 수 있다.
server state 신선도 체크 및 자동 캐싱 및 refetch
설정한 신선도에 따라 쿼리를 백그라운드에서 refetch하여 서버 내에 있는 데이터와의 일치성을 유지할 수 있다.
내장된 로딩/에러 상태 관리
isFetching, isPending, isLoading과 같은 현재 쿼리 진행 상태에 맞는 boolean 값을 제공해, 에러나 예외 처리 작업에 매우 유용하다.
빠른 화면 표시
서버 요청 없이 저장된 데이터 즉시 표시를 통해 사용자 경험이 향상될 수 있다.
서버 부하 감소
queryKey값으로 식별하기 때문에 불필요한 반복 요청이 방지되어 서버 리소스를 절약할 수 있다.
오프라인 지원
이전에 캐시된 데이터로 기본적인 표시 가능하기 때문에 네트워크 불안정 시에도 동작이 가능하다.
무서운 Tanstack-query였지만, 사실 이제는 없으면 안되는 국밥같은 존재가 되었다,,ㅎㅎ
이제는 놓을 수 업서,,,★
사실 블로그 본문에서 설명한 useQuery 외에도 Tanstack-query에서는 useMutation이나 useInfiniteQuery 등 다양한 목적에 맞게 구분되어 서버 상태를 관리할 수 있도록 제공되는 기능들이 너무너무너무 많다. 그리고 그 안에 적용된 원리라던가 다양한 옵션 값들도,,,굉장히 많다.
하지만,,,필자의 체력과 정신력의 문제로 이만 줄여보고자 한다,,^__^
