TanStack Query의 라이프사이클: staleTime과 gcTime의 차이점

크리스가 TanStack Query를 도입하고 나서 고개를 갸웃거린다.
"캐싱을 해준다더니 왜 창을 클릭할 때마다 API 요청이 계속 나가는 거지?"
반대로 어떤 상황에서는, "아까 닫았던 탭을 다시 열었는데 왜 로딩 스피너가 안 뜨고 옛날 데이터가 그대로 있지?"라며 당황하기도 한다.
이 혼란의 원인은 TanStack Query의 두 가지 핵심 타이머, staleTime과 gcTime의 차이를 명확히 이해하지 못했기 때문이다. 이 둘은 비슷해 보이지만, 데이터를 언제 "상했다"고 판단할지와 언제 "메모리에서 지울지"를 결정하는 전혀 다른 역할을 한다.
오늘은 리액트 쿼리 초보자가 가장 많이 헷갈리는 이 두 개념의 라이프사이클을 완벽하게 정리한다.
1. 데이터의 신선도: Fresh vs Stale
TanStack Query에서 데이터는 크게 두 가지 상태를 가진다.
staleTime: 유통기한
충격적인 사실은 TanStack Query의 staleTime 기본값이 0ms라는 점이다.
즉, 데이터를 받아오는 즉시 그 데이터는 상한 것(Stale)으로 간주한다. 그래서 크리스가 창을 포커스 하거나 컴포넌트를 다시 마운트 할 때마다, 쿼리는 "어? 이거 상했네. 다시 가져와야겠다"라고 판단하고 즉시 재요청(Refetch)을 보냈던 것이다.
// useMyQuery.ts
const { data } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
// 1분 동안은 "신선하다"고 우긴다.
// 1분 내에는 컴포넌트가 다시 마운트 되어도 API 요청을 안 보낸다.
staleTime: 1000 * 60,
});2. 메모리 관리: Inactive와 gcTime
그렇다면 gcTime(구 cacheTime)은 무엇일까? 이것은 데이터가 사용되지 않을 때(Inactive) 메모리에 얼마나 남아있을지를 결정한다.
사용자가 페이지를 이동해서 useQuery를 사용하는 컴포넌트가 화면에서 사라졌다고 가정해보자(Unmount). 이때 데이터는 즉시 삭제되지 않고 Inactive 상태로 캐시에 남는다.
gcTime: 쓰레기통 비우는 시간
크리스가 겪은 "데이터가 그대로 남아있는 현상"은, 비록 데이터가 상했을지라도(stale), 아직 메모리에서 삭제되지 않았기(gcTime 안쪽) 때문에 캐시 된 데이터를 먼저 보여준 것이다.
3. 라이프사이클 한눈에 보기
이 과정을 시각화하면 다음과 같다.
4. 실전 전략: 어떻게 설정해야 할까?
상황에 따라 이 두 값을 조절하는 전략이 필요하다.
전략 1: 기본값 사용 (staleTime: 0, gcTime: 5분)
전략 2: 정적 데이터 (staleTime: Infinity, gcTime: Infinity)
// static-data.ts
useQuery({
queryKey: ['settings'],
queryFn: fetchSettings,
staleTime: Infinity, // 영원히 신선함
gcTime: Infinity, // 메모리에서 절대 안 지움
});전략 3: 잦은 갱신 방지 (staleTime: 30초)
핵심 정리
이제 캐시의 수명을 제어할 수 있게 되었다. 하지만 캐시를 저장하는 이름표(Key)를 엉망으로 관리하면 데이터가 꼬이거나 중복 호출이 발생한다. 유지보수하기 좋은 키 관리 전략은 무엇일까?
"효율적인 Query Key 관리 전략: 팩토리 패턴으로 유지보수성 높이기" 편에서 계속된다.