
🧠 Tanstack Query 캐싱 메커니즘 이해하기
React Query(Tanstack Query)는 서버 상태 캐싱을 통해 불필요한 네트워크 요청을 줄이고,
사용자 경험을 개선해준다.
이번 글에서는 쿼리의 생명주기와 캐싱 메커니즘을 실제 예제를 통해 정리해봤다.
🔄 쿼리 생명주기 한눈에 보기

fetching → fresh → stale → inactive → deleted
각 상태는 쿼리가 언제 패칭되고, 언제 재사용되며, 언제 메모리에서 제거되는지를 결정한다.
🔄 Fetching
서버에 데이터 요청을 보내고 있는 상태다.
아직 응답을 받지 못해 캐시 데이터가 없거나 갱신 중이다.
✅ Fresh
서버에서 받아온 데이터가 신선하다고 판단되는 상태다.
staleTime 이내의 데이터로, 리페칭 없이 그대로 사용된다.
⚠️ Stale
데이터가 최신이라고 보장할 수 없는 상태다.
캐시된 데이터는 즉시 사용되며, 조건에 따라 백그라운드 리페칭이 발생한다.
💤 Inactive
해당 쿼리를 사용 중인 컴포넌트가 없는 상태다.
캐시는 메모리에 남아 있지만 화면에서는 사용되지 않는다.
🗑 Deleted
gcTime이 지나 캐시가 메모리에서 완전히 제거된 상태다.
이후 동일 쿼리를 요청하면 새로 데이터를 패칭한다.
🛠 React Query Devtools 설치
캐싱 상태를 눈으로 확인하기 위해 Devtools를 설치했다.
npm install @tanstack/react-query-devtools
main.tsx 설정
const queryClient = new QueryClient();
<QueryClientProvider client={queryClient}>
<ReactQueryDevtools />
<App />
</QueryClientProvider>
이를 통해 쿼리 상태(fresh / stale / inactive)를 실시간으로 확인할 수 있었다. 👀
📄 Todo 상세 페이지 구성
라우팅 설정
<Route path="/todolist/:id" element={<TodoDetailPage />} />
Todo 클릭 시 상세 페이지 이동
<Link to={`/todolist/${id}`}>{content}</Link>
이를 통해 리스트 → 상세 페이지 이동 흐름을 구성했다.
🌐 Todo 상세 데이터 패칭
API 함수
export async function fetchTodoById(id: number) {
const response = await fetch(`${API_URL}/todos/${id}`);
if (!response.ok) throw new Error("Fetch Failed");
return response.json();
}
React Query 훅
export function useTodoDataById(id: number) {
return useQuery({
queryKey: ["todos", id],
queryFn: () => fetchTodoById(id),
});
}
쿼리 키를 ["todos", id]로 설정해 ID별 캐싱이 가능하도록 했다.
📌 stale 상태와 리페칭 동작
Todo 상세 페이지에 진입하면 다음과 같은 동작을 확인할 수 있었다.

✅ 기본 동작
- 최초 진입 시 fetching → fresh
- stale 상태일 때
- 🔄 컴포넌트 마운트 시 리페칭 (Mount : 마운트)
- 🪟 브라우저 포커스 복귀 시 리페칭 (Window Focus : 윈도우 포커스)
- 🌐 네트워크 재연결 시 리페칭 (Reconnect)
- ⏱ 특정 주기마다 리페칭 (Interual)
React Query는 stale 상태라도 캐시 데이터를 즉시 렌더링하고,
백그라운드에서 최신 데이터를 다시 가져오는 전략을 사용했다.
→ 로딩 화면을 최대한 줄이기 위한 설계라는 걸 알 수 있었다. 👍
❌ 리페칭 비활성화 옵션
refetchOnMount: false,
refetchOnWindowFocus: false,
refetchOnReconnect: false,
refetchInterval: false,
필요에 따라 자동 리페칭을 세밀하게 제어할 수 있었다.
⏱ staleTime 설정
staleTime: 5000,
- 5초 동안 fresh 상태 유지
- 해당 시간 동안은 리페칭 없이 캐시 데이터만 사용
- stale이 된 이후에만 리페칭 조건이 활성화됨
🗑 inactive → deleted 흐름 (gcTime)
쿼리가 더 이상 화면에서 사용되지 않으면 inactive 상태가 된다.
gcTime: 5000,
- inactive 상태로 전환
- gcTime 이후 → deleted
- 메모리에서 완전히 제거됨 🧹
⚠️ staleTime과 gcTime은 완전히 별개의 개념이다.
- staleTime → 데이터 신선도
- gcTime → 메모리 유지 시간
🌍 전역 설정 예시
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 0,
gcTime: 5 * 60 * 1000,
refetchOnMount: true,
refetchOnWindowFocus: false,
refetchOnReconnect: false,
refetchInterval: false,
},
},
});
일반적으로 gcTime은 넉넉하게 설정해서
불필요한 재요청을 줄이는 게 좋다고 느꼈다.
✍️ 마무리 정리
- React Query는 캐싱 + UX 최적화를 기본 전략으로 사용한다.
- stale 상태에서도 즉시 렌더링 → 백그라운드 리페칭
- inactive 이후 gcTime이 지나면 메모리에서 제거
- 이 모든 흐름을 Devtools로 시각적으로 확인할 수 있었다.
“왜 React Query를 쓰는가?”에 대한 답은
이 캐싱 메커니즘 하나로 충분했다고 정리했다. 🚀
'Frontend > React' 카테고리의 다른 글
| [React] 리스트 렌더링과 key의 역할 정리 (0) | 2026.02.05 |
|---|---|
| [React] 조건부 렌더링 제대로 정리하기 (if, ? :, &&, null) (1) | 2026.02.04 |
| Zustand 미들웨어를 정리하며: immer, subscribeWithSelector, persist, devtools (0) | 2025.12.17 |
| Zustand combine 미들웨어, 처음엔 안 와닿았던 이유 (0) | 2025.12.15 |
| Zustand 그냥 쓰면 생기는 리렌더링 문제와 해결 방법 (0) | 2025.12.14 |