Web/Next&React
Tanstack Query
창욱씨
2026. 9. 13. 21:05
반응형
TanStack Query
서버에서 가져온 데이터를 브라우저 메모리에 캐싱하고, 그 캐시의 생명주기를 자동으로 관리해주는 프론트엔드 라이브러리
"서버 상태 관리"라는 표현이 흔히 쓰이지만, 실제로는 비동기 데이터 캐시 자동 관리가 더 정확한 설명이다.
어디서 동작하는가
브라우저(클라이언트)에서 동작한다. Node.js 서버가 아니다.
┌──────────────────┐ HTTP 요청 ┌──────────────┐
│ 브라우저 │ ───────────────────▶ │ 백엔드 서버 │
│ │ │ │
│ React 앱 │ ◀─────────────────── │ (아무 서버) │
│ + TanStack Query│ JSON 응답 │ │
│ (여기서 동작!) │ │ │
└──────────────────┘ └──────────────┘
캐시 저장/관리 DB 조회/응답
리페치 판단
로딩/에러 추적백엔드 서버는 평소처럼 API 응답만 해주면 되고, 서버 쪽에서 바뀌는 건 없다.
캐시 저장 위치
JavaScript 메모리(RAM)에 저장된다. QueryClient 객체 내부에 Map 형태로 존재한다.
const queryClient = new QueryClient();
// 내부적으로 대략 이런 구조
{
['todos']: { data: [...], dataUpdatedAt: 1694600000000 },
['user', 1]: { data: {...}, dataUpdatedAt: 1694600050000 },
}
| 저장 장소 | 아닌 것 |
|---|---|
| ✅ JS 메모리 (QueryClient 객체) | ❌ DOM / 화면 |
| ❌ localStorage / sessionStorage | |
| ❌ 서버 |
새로고침하면 캐시는 전부 사라진다. (필요하면 persistQueryClient 플러그인으로 영속화 가능)
동작 원리: 이벤트 기반 리페치
타이머로 주기적으로 요청하지 않는다. 특정 이벤트가 발생했을 때, 데이터가 stale(오래된) 상태이면 다시 요청한다.
리페치를 트리거하는 이벤트
| 이벤트 | 설명 | 기본값 |
|---|---|---|
| 컴포넌트 마운트 | 화면에 나타날 때 | ✅ 켜짐 |
| 윈도우 포커스 | 다른 탭 갔다가 돌아올 때 | ✅ 켜짐 |
| 네트워크 재연결 | 와이파이 끊겼다 연결될 때 | ✅ 켜짐 |
| 수동 무효화 | invalidateQueries 호출 |
개발자가 직접 |
refetchInterval |
주기적 폴링 (옵트인) | ❌ 꺼짐 |
staleTime이 핵심
staleTime: 0 (기본값)
→ 가져온 즉시 stale 취급 → 이벤트마다 리페치
staleTime: 5분
→ 5분간 fresh → 이벤트가 발생해도 리페치 안 함시나리오 예시
1. 할일 목록 페이지 진입 → 마운트 → 캐시 없음 → 서버 요청 → 저장
2. 다른 페이지 갔다가 돌아옴 → 마운트 → 캐시 있음
→ fresh면 캐시 반환 (요청 안 함)
→ stale이면 캐시 먼저 보여주고 백그라운드에서 서버 요청
3. 다른 탭 갔다가 돌아옴 → 포커스 → stale이면 리페치
4. 새 할일 추가 → invalidateQueries → 즉시 리페치주요 API
useQuery — 데이터 조회 (Read)
캐싱, 자동 리페치, 중복 제거 등을 담당한다.
const { data, isLoading, error } = useQuery({
queryKey: ['todos'],
queryFn: () => fetch('/api/todos').then(r => r.json()),
});
useMutation — 데이터 변경 (Create / Update / Delete)
캐싱은 없다. 변경 요청 후 캐시를 어떻게 갱신할지 관리한다.
const mutation = useMutation({
mutationFn: (newTodo) => axios.post('/api/todos', newTodo),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
});
mutation.mutate({ title: '새 할일' });
조회만 되는 게 아니다
| 용도 | Hook |
|---|---|
| 조회 (Read) | useQuery |
| 변경 (Create/Update/Delete) | useMutation |
둘이 세트로 사용하는 게 일반적이다. 변경 후 invalidateQueries로 조회 캐시를 갱신하는 패턴.
적용 범위
서버 액션 전용이 아니다. Promise를 반환하는 아무 비동기 함수와 동작한다.
// fetch, axios, GraphQL, 로컬 DB 등 뭐든 가능
useQuery({
queryKey: ['todos'],
queryFn: () => 아무_async_함수(),
});
Next.js App Router와의 관계
기능이 겹친다
| 기능 | TanStack Query | Next.js App Router |
|---|---|---|
| 데이터 조회 | useQuery |
서버 컴포넌트 + fetch |
| 데이터 변경 | useMutation |
서버 액션 + useActionState |
| 캐시 무효화 | invalidateQueries |
revalidatePath / revalidateTag |
| 로딩 상태 | isPending |
useFormStatus |
실제로는?
둘 중 하나만 쓰거나, 역할을 나눠서 같이 쓰기도 한다.
| 상황 | 추천 |
|---|---|
| 정적 페이지, 블로그, 초기 데이터 로드 | App Router만으로 충분 |
| 무한 스크롤, 실시간 검색, 폴링 등 인터랙티브 기능 | TanStack Query 같이 사용 |
| Next.js 안 씀 (Vite, CRA 등) | TanStack Query 거의 필수 |
둘을 섞으면 캐시 관리 포인트가 두 개가 되므로, 프로젝트 성격에 맞게 취사선택하는 것이 중요하다.
핵심 포인트 요약
| 질문 | 답 |
|---|---|
| 뭐 하는 라이브러리? | 비동기 데이터 캐시 자동 관리 |
| 어디서 동작? | 브라우저 (클라이언트) |
| 캐시는 어디에? | JS 메모리 (새로고침 시 사라짐) |
| 주기적으로 요청? | ❌ 이벤트 기반 (마운트, 포커스, 네트워크 등) |
| 조회만? | ❌ 변경도 가능 (useMutation) |
| 서버 액션 전용? | ❌ 아무 async 함수와 동작 |
| SSR? | ❌ 클라이언트 캐시 라이브러리 (SSR 지원은 가능) |
| Next.js App Router와? | 겹치는 부분 있어서 취사선택 |
반응형