이 글에서 정리하는 내용
TanStack Query v5 기준으로 staleTime과 gcTime의 차이를 정리합니다. 캐시가 남아 있는데도 요청이 다시 나가는 이유, v4의 cacheTime이 v5에서 gcTime으로 바뀐 이유, 실제 React 화면에서 두 옵션을 나눠 설정하는 기준을 함께 봅니다.
- 캐시가 있는데도 다시 요청되는 상황
- staleTime은 데이터를 다시 확인할지 정하는 기준
- gcTime은 inactive 캐시를 언제 버릴지 정하는 기준
- 목록과 상세 화면 이동으로 보는 흐름
- 화면 성격에 맞게 설정 기준 잡기
- 정리
캐시가 있는데도 다시 요청되는 상황

TanStack Query를 처음 쓰면 캐시가 있다는 말 때문에 같은 API 요청이 다시 나가지 않을 거라고 기대하기 쉽습니다. 그런데 실제 React 화면에서는 목록 페이지를 보고 상세 페이지에 들어갔다가 다시 목록으로 돌아왔을 때이전 데이터가 바로 보이면서도 네트워크 요청이 다시 나가는 경우가 있습니다.
이 상황에서 “캐시가 안 먹은 건가?”라고 판단하면 원인을 잘못 잡을 수 있습니다. 캐시는 남아 있을 수 있습니다. 다만 TanStack Query가 그 데이터를 fresh한 데이터로 보지 않기 때문에, 화면에는 캐시 데이터를 먼저 보여주고 뒤에서는 서버 데이터를 다시 확인하는 흐름이 생깁니다.
TanStack Query v5 기준 기본값은 staleTime: 0, gcTime: 5분입니다. 이 기본값에서는 데이터를 가져온 직후에도 곧바로 stale 상태가 될 수 있고, 사용하지 않는 쿼리는 inactive 상태가 된 뒤 기본 5분 동안 캐시에 남아 있다가 제거될 수 있습니다.
여기서 두 옵션의 역할을 분리해야 합니다. staleTime은 데이터가 얼마나 오래 fresh로 취급될지를 정합니다. 반면 gcTime은 쿼리가 더 이상 사용되지 않는 unused 또는 inactive 상태가 된 뒤, 캐시를 언제 제거할지에 관여합니다. 요청이 다시 나가는 문제와 캐시가 사라지는 문제는 서로 다른 축에서 봐야 합니다.
특히 stale 상태의 쿼리는 새 쿼리 인스턴스가 마운트될 때, 브라우저 창에 포커스가 다시 돌아올 때, 네트워크가 재연결될 때 백그라운드 refetch 대상이 될 수 있습니다. 그래서 화면을 이동하지 않았더라도 브라우저 탭을 잠깐 바꿨다가 돌아왔을 때 요청이 다시 보일 수 있습니다.
이 동작을 불필요한 요청으로만 보면 설정을 과하게 막게 됩니다. TanStack Query의 기본값은 서버 데이터가 오래된 상태로 남는 것을 피하려는 쪽에 가깝습니다. 화면에 캐시 데이터를 먼저 보여주고, 필요하면 뒤에서 다시 확인하는 구조라고 보면 네트워크 탭의 움직임도 덜 이상하게 보입니다.
staleTime은 데이터를 다시 확인할지 정하는 기준
staleTime은 서버에서 받아온 데이터를 얼마 동안 fresh로 볼지 정하는 옵션입니다. fresh 상태인 동안에는 같은 쿼리가 다시 마운트되어도 불필요한 refetch를 줄일 수 있습니다. 반대로 stale 상태가 되면 캐시 데이터는 남아 있어도 다시 가져올 수 있는 후보가 됩니다.
이 차이는 목록 화면에서 바로 드러납니다. 상품 목록, 게시글 목록, 공지사항 목록처럼 사용자가 자주 왔다 갔다 하는 화면은 데이터를 매번 새로 확인하지 않아도 되는 경우가 많습니다. 기본값 그대로 두면 화면을 다시 열 때마다 요청이 너무 자주 보일 수 있습니다. 이때 먼저 볼 옵션이 staleTime입니다.
import { useQuery } from '@tanstack/react-query'; const ONE_MINUTE = 1000 * 60; type Product = { id: string; name: string; price: number;
}; function ProductList({ category }: { category: string }) { const { data, isFetching } = useQuery({ queryKey: ['products', category], queryFn: () => getProducts(category), staleTime: ONE_MINUTE}); const products: Product[] = data ?? []; return ( <section> {isFetching && <p>상품 목록을 다시 확인하는 중입니다.</p>} <ProductGrid items={products} /> </section> );
} 이 예시는 같은 카테고리의 상품 목록을 1분 동안 fresh로 취급합니다. 사용자가 1분 안에 상세 페이지를 보고 다시 목록으로 돌아오면같은 쿼리에 대해 바로 refetch가 발생하는 일을 줄일 수 있습니다. 여기서 staleTime은 캐시를 보관하는 시간이 아니라 “이 시간 안에는 방금 받은 데이터를 다시 검증하지 않아도 된다”는 기준입니다.
그렇다고 모든 데이터에 긴 staleTime을 주면 되는 것은 아닙니다. 결제 상태, 주문 진행 상태, 실시간 알림처럼 사용자가 최신 값을 기대하는 데이터는 오래 fresh로 두면 오히려 화면이 늦게 반응하는 것처럼 보일 수 있습니다. 요청 횟수를 줄이는 것보다 최신성이 더 중요한 화면도 있습니다.
따라서 staleTime은 성능 최적화 숫자로만 정하지 않는 편이 좋습니다. 데이터가 얼마나 자주 바뀌는지, 사용자가 화면을 다시 열었을 때 어느 정도의 최신성을 기대하는지, 백그라운드 refetch가 보여도 괜찮은 흐름인지까지 같이 봐야 합니다.
예를 들어 이벤트 페이지의 필터 목록이나 블로그 카테고리 목록처럼 짧은 시간 안에 자주 바뀌지 않는 데이터라면 몇 분 정도의 staleTime을 줄 수 있습니다. 반대로 관리자 화면에서 결제 승인 여부를 확인하는 데이터라면 같은 기준을 적용하기 어렵습니다. 화면 이름보다 데이터의 성격을 먼저 보는 것이 더 정확합니다.
gcTime은 inactive 캐시를 언제 버릴지 정하는 기준
gcTime은 이름 그대로 garbage collection과 연결해서 이해하는 것이 더 정확합니다. TanStack Query v5에서는 기존 cacheTime이라는 이름이 gcTime으로 바뀌었습니다. cacheTime이라는 이름은 데이터가 캐시에 남아 있는 전체 시간처럼 보이지만, 실제로는 쿼리가 사용되지 않는 상태가 된 뒤의 보관 시간에 가까웠습니다.
쿼리를 사용하는 컴포넌트가 화면에 있으면 해당 쿼리는 active 상태입니다. 이때 gcTime이 곧바로 캐시 삭제 타이머처럼 움직이는 것은 아닙니다. 해당 쿼리를 구독하는 컴포넌트가 모두 사라지고 unused 또는 inactive 상태가 된 뒤에야 gcTime 기준이 의미를 갖습니다.
import { QueryClient } from '@tanstack/react-query'; const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 1000 * 60, gcTime: 1000 * 60 * 10}}}); 위 설정은 데이터를 1분 동안 fresh로 보고, 쿼리가 inactive 상태가 된 뒤에는 10분 동안 캐시에 남겨둡니다. 여기서 staleTime과 gcTime은 서로 대신할 수 없습니다. gcTime을 길게 잡아도 데이터가 stale 상태라면 refetch 조건에서 다시 요청될 수 있습니다.
반대로 staleTime을 길게 잡아도 inactive 상태의 캐시가 무조건 오래 남는 것은 아닙니다. 사용하지 않는 캐시를 언제 제거할지는 gcTime이 담당합니다. 즉, 요청이 자주 나가는 문제를 해결하려고 gcTime부터 늘리면 기대한 결과가 나오지 않을 가능성이 큽니다.
이 이름 변경은 글을 읽는 사람 입장에서도 도움이 됩니다. gcTime이라고 부르면 “캐시 전체 생존 시간”보다 “안 쓰는 캐시를 수거하기 전까지 기다리는 시간”이라는 의미가 더 잘 드러납니다. v5 기준 글을 작성할 때도 cacheTime이라는 이름보다 gcTime을 기준으로 설명하는 것이 혼동을 줄입니다.
gcTime을 길게 잡아야 하는 대표적인 경우는 사용자가 짧은 간격으로 같은 화면에 돌아오는 구조입니다. 목록과 상세를 오가거나, 설정 탭을 여러 번 열어보는 화면에서는 inactive 캐시가 너무 빨리 사라지면 매번 새 요청처럼 느껴질 수 있습니다. 반대로 검색어 조합이 많고 결과 데이터가 큰 화면에서는 캐시를 오래 남기는 것이 메모리 부담으로 이어질 수 있습니다.
목록과 상세 화면 이동으로 보는 흐름
두 옵션은 시간 흐름으로 보면 훨씬 구분하기 쉽습니다. 사용자가 상품 목록 페이지에 처음 들어오면 useQuery가 실행되고 네트워크 요청이 나갑니다. 요청이 성공하면 데이터는 ['products', category] 같은 아래 캐시됩니다.
기본 설정이라면 staleTime이 0이기 때문에 이 데이터는 곧바로 stale 상태가 됩니다. stale이라고 해서 화면에 못 쓰는 데이터가 되는 것은 아닙니다. 캐시 데이터로 화면을 그릴 수 있지만, 다시 확인할 수 있는 상태가 된 것입니다.
이후 사용자가 상품 상세 페이지로 이동하면 목록 컴포넌트가 언마운트될 수 있습니다. 목록 쿼리를 사용하는 컴포넌트가 더 이상 없다면 그 쿼리는 inactive 상태가 됩니다. 이때부터 gcTime이 의미를 갖습니다. 기본값 기준으로는 5분 안에 다시 목록으로 돌아올 경우 이전 캐시가 남아 있을 수 있습니다.
다시 목록으로 돌아왔을 때 캐시가 남아 있다면 사용자는 이전 목록을 먼저 볼 수 있습니다. 하지만 그 데이터가 stale 상태라면 새 인스턴스 마운트 조건에 의해 백그라운드 refetch가 같이 일어날 수 있습니다. 그래서 화면에는 데이터가 즉시 보이는데 네트워크 요청도 함께 보이는 흐름이 만들어집니다.
만약 사용자가 목록을 떠난 뒤 오래 지나서 돌아왔다면 상황이 달라집니다. inactive 상태가 된 뒤 gcTime이 지났다면 해당 쿼리 캐시는 제거되었을 수 있습니다. 이때는 이전 데이터를 즉시 보여주기보다 새 요청을 통해 다시 가져오는 흐름이 됩니다.
이 흐름을 한 번 잡아두면 DevTools나 네트워크 탭을 볼 때 확인 순서도 정해집니다. 요청이 다시 나가는지 먼저 보고, 그다음 데이터가 캐시에 남아 있었는지 확인합니다. 요청이 다시 나갔다고 해서 곧바로 캐시가 비었다고 결론 내리면 staleTime 문제를 놓치기 쉽습니다.
| 상황 | 먼저 확인할 옵션 | 확인 이유 |
|---|---|---|
| 페이지를 다시 열 때 API 요청이 자주 보임 | staleTime | 데이터가 stale 상태라 refetch 조건에 걸릴 수 있음 |
| 브라우저 탭으로 돌아올 때 요청이 나감 | staleTime | stale query는 창 포커스 복귀 시 백그라운드 refetch 대상이 될 수 있음 |
| 다른 화면에 갔다 오면 이전 데이터가 사라져 있음 | gcTime | inactive 캐시가 garbage collection 되었을 수 있음 |
| 사용하지 않는 캐시가 메모리에 오래 남음 | gcTime | unused query의 보관 시간을 줄여야 할 수 있음 |
화면 성격에 맞게 설정 기준 잡기

실제 프로젝트에서는 모든 쿼리에 같은 시간을 넣기보다 화면 성격에 맞게 나누는 쪽이 더 안정적입니다. 먼저 staleTime은 데이터의 최신성 요구에 맞춥니다. 자주 바뀌는 데이터는 짧게 두고, 자주 바뀌지 않는 데이터는 조금 길게 잡을 수 있습니다.
예를 들어 공지사항 목록이나 카테고리 목록은 몇 분 정도 fresh로 보아도 큰 문제가 없는 경우가 많습니다. 사용자가 페이지를 이동했다가 돌아올 때마다 같은 데이터를 다시 요청하는 것보다, 일정 시간 동안은 캐시 데이터를 믿고 화면을 빠르게 보여주는 쪽이 자연스럽습니다.
반대로 주문 상태, 결제 상태, 재고 수량처럼 사용자 행동에 직접 영향을 주는 데이터는 다르게 봐야 합니다. 이런 데이터에 긴 staleTime을 주면 요청은 줄어들 수 있지만, 사용자가 오래된 상태를 보고 판단할 위험이 생깁니다. 이 경우에는 짧은 staleTime을 유지하거나 별도의 refetch 조건을 설계해야 합니다.
gcTime은 화면을 떠난 뒤 다시 돌아올 가능성과 데이터 크기를 기준으로 봅니다. 목록과 상세를 자주 오가는 구조라면 inactive 캐시를 어느 정도 남겨두는 것이 사용자 경험에 유리합니다. 반면 검색 조건이 매우 다양하고 결과 데이터가 큰 화면에서는 캐시를 오래 남기는 것이 부담이 될 수 있습니다.
const noticeQuery = useQuery({ queryKey: ['notices'], queryFn: getNotices, staleTime: 1000 * 60 * 5, gcTime: 1000 * 60 * 30}); const orderStatusQuery = useQuery({ queryKey: ['order', orderId], queryFn: () => getOrderStatus(orderId), staleTime: 0}); 공지사항은 5분 동안 fresh로 보고, 화면에서 사라진 뒤에도 30분 정도 캐시에 남겨둘 수 있습니다. 사용자가 공지 목록과 상세를 오가거나 다른 메뉴를 보고 돌아오는 상황을 고려한 설정입니다. 주문 상태는 성격이 다릅니다. 사용자가 최신 상태를 기대하기 때문에 staleTime을 길게 잡는 것이 맞지 않을 수 있습니다.
거의 변하지 않는 기준 데이터라면 staleTime: Infinity도 검토할 수 있습니다. 다만 이 설정은 수동 invalidation 전까지 자동 refetch를 기대하지 않겠다는 의도가 있어야 합니다. v5에서 사용할 수 있는 'static'은 invalidation 이후에도 refetch를 막는 성격이 더 강하므로, 단순히 요청을 줄이고 싶다는 이유로 적용하기에는 범위가 큽니다.
설정값을 고를 때는 먼저 질문을 나누면 됩니다. “이 데이터는 얼마 동안 다시 확인하지 않아도 되는가?”는 staleTime의 질문입니다. “이 화면을 떠난 뒤 캐시를 얼마 동안 남겨둘 것인가?”는 gcTime의 질문입니다. 같은 캐시 옵션처럼 보여도 출발점이 다릅니다.
실무에서는 전역 기본값을 무리하게 강하게 잡기보다, 데이터 성격이 분명한 쿼리부터 개별 설정을 주는 방식이 관리하기 쉽습니다. 모든 쿼리에 긴 staleTime을 넣으면 요청은 줄어들 수 있지만, 최신성이 필요한 화면까지 같이 느려질 수 있습니다. 반대로 모든 쿼리를 기본값으로만 두면 목록과 상세를 오가는 화면에서 불필요한 refetch가 자주 보일 수 있습니다.
과 을 안정적으로 쓰려면 먼저 캐시를 나누는 기준인 TanStack Query queryKey 배열 설계도 함께 확인하는 것이 좋습니다.
정리
staleTime과 gcTime을 헷갈리는 이유는 둘 다 캐시와 관련된 시간처럼 보이기 때문입니다. 하지만 역할은 분명히 다릅니다. staleTime은 데이터가 fresh로 취급되는 시간이고이 시간이 지나 stale 상태가 되면 새 인스턴스 마운트, 창 포커스, 네트워크 재연결 같은 조건에서 백그라운드 refetch 대상이 될 수 있습니다.
gcTime은 쿼리가 사용 중일 때가 아니라 unused 또는 inactive 상태가 된 뒤 캐시를 언제 제거할지 정하는 옵션입니다. v5에서 cacheTime이라는 이름이 gcTime으로 바뀐 것도 이 동작을 더 정확히 드러내기 위한 변경으로 볼 수 있습니다.
요청이 너무 자주 나가는 문제를 만나면 먼저 staleTime을 확인합니다. 화면을 떠났다가 돌아왔을 때 이전 데이터가 남아 있지 않다면 gcTime을 확인합니다. 이 기준을 나눠두면 TanStack Query 캐시 설정을 단순히 숫자 외우기가 아니라 화면 흐름에 맞춰 판단할 수 있습니다.
“TanStack Query staleTime gcTime 차이: 캐시 시간 기준”에 대한 2개의 생각