TanStack Query 보존: gcTime은 화면을 떠난 데이터를 언제 지우는가
상품 미리보기를 닫았다 다시 열 때마다 로딩이 반복되는 실패를 관찰하고, 비활성 Query의 재사용 가치와 메모리 비용으로 보존 시간을 정한다.
목차
표준·구현·측정·해석 표시는 무엇인가요?
- 표준웹 표준이나 언어 명세가 정한 동작
- 구현특정 기술이나 브라우저가 실제로 구현한 동작
- 측정명시한 환경에서 직접 실행해 관찰한 결과
- 해석앞선 근거에서 도출한 설계 판단
- 미확인아직 공식 근거나 재현 결과를 확인하지 못한 내용
30초 요약
gcTime은 화면에 보이는 데이터의 정확도를 정하지 않는다. 같은 Query 결과를 실제로 구독하는 곳이
하나도 없어진 뒤, 그 사본을 메모리에 얼마나 더 남길지 정한다. 너무 짧으면 잠깐 화면을 오갔을 뿐인데
로딩과 요청이 반복되고, 너무 길면 다시 쓰지 않을 많은 사본이 메모리에 남는다.
이 글을 관통하는 상황: 미리보기를 다시 열 때마다 로딩한다
상품 목록에서 미리보기를 열고 닫는다고 하자. ProductPreview가 화면에 있을 때만 useQuery가
같은 상품을 구독한다. 요청 번호를 응답에 넣어, 남은 캐시를 재사용했는지 새 요청을 보냈는지 화면에서
확인한다.
아래 코드는 staleTime: Infinity로 최신성에 따른 다시 가져오기를 막고 gcTime의 효과만 관찰한다.
먼저 EXPERIMENT_GC_TIME을 1초로 둔다. 네트워크 지연 600밀리초는 로딩 전환을 눈으로 보기 위한
실험값이다.
import {
QueryClient,
QueryClientProvider,
useQuery,
} from '@tanstack/react-query'
import { useState } from 'react'
const queryClient = new QueryClient()
const EXPERIMENT_GC_TIME = 1000
let requestCount = 0
async function fetchProduct() {
const requestNumber = requestCount + 1
requestCount = requestNumber
await new Promise((resolve) => setTimeout(resolve, 600))
return { id: 'coffee', name: '원두 정기 배송', requestNumber }
}
function ProductPreview() {
const productQuery = useQuery({
queryKey: ['product', 'coffee'],
queryFn: fetchProduct,
staleTime: Infinity,
gcTime: EXPERIMENT_GC_TIME,
})
if (productQuery.isPending) return <p>상품을 읽는 중…</p>
if (productQuery.isError) return <p>상품을 읽지 못했습니다.</p>
return (
<section>
<h2>{productQuery.data.name}</h2>
<p>이 사본을 만든 요청: {productQuery.data.requestNumber}회째</p>
</section>
)
}
function ProductList() {
const [isPreviewOpen, setIsPreviewOpen] = useState(false)
return (
<main>
<button onClick={() => setIsPreviewOpen((open) => !open)}>
{isPreviewOpen ? '미리보기 닫기' : '미리보기 열기'}
</button>
{isPreviewOpen ? (
<ProductPreview />
) : (
<p>미리보기가 닫혀 있습니다.</p>
)}
</main>
)
}
export default function App() {
return (
<QueryClientProvider client={queryClient}>
<ProductList />
</QueryClientProvider>
)
}미리보기를 열어 첫 결과가 나타난 뒤 닫는다. 1초 넘게 기다렸다 다시 열면 상품을 읽는 중…이
나타나고 요청 번호가 2로 바뀐다. 데이터는 여전히 fresh였지만, 쓰지 않는 동안 캐시에서 삭제되어
새 요청부터 시작한 것이다.
화면에서 안 보임과 inactive는 같지 않다
ProductPreview만 같은 키를 사용하므로 닫을 때 Query가 inactive가 된다. 그러나 화면 다른 곳의
장바구니 요약도 ['product', 'coffee']를 구독한다면 미리보기를 닫아도 Query는 active다. 화면에서
가려졌는지, 앱이 배경으로 갔는지, 스타일로 숨겼는지가 직접 기준은 아니다.
React Native도 같다. 화면 전환 뒤 이전 화면 컴포넌트가 계속 mount되어 Query를 구독한다면 inactive가 아니다. Web route와 Native screen 모두 실제 컴포넌트 구독이 사라졌는지 확인해야 한다.
gcTime이 지나면 비활성 사본을 삭제한다
여기서 가비지 컬렉션은 TanStack Query 캐시 항목을 지우는 동작을 말한다. JavaScript 엔진이 어떤 객체 메모리를 실제 회수할 수 있는지 판단하는 일반 가비지 컬렉션 이론 전체를 뜻하지 않는다.
5초 실험으로 재사용과 삭제를 모두 확인한다
앞 코드에서 다음 한 줄만 바꾼 뒤 브라우저를 전체 새로고침한다. 새로고침은 앞 실험의
queryClient, 요청 횟수와 캐시를 초기화하기 위해 필요하다.
const EXPERIMENT_GC_TIME = 5000이제 다음 순서로 확인한다.
- 미리보기를 열고 첫 결과와 요청 번호 1을 확인한다.
- 닫은 뒤 2초 안에 다시 연다. 로딩 없이 요청 번호 1인 사본이 즉시 나타난다.
- 다시 닫고 5초 넘게 기다린다.
- 다시 열면 로딩이 나타나고 요청 번호가 2로 바뀐다.
5초 안의 재방문은 남은 사본을 재사용하고, 5초 밖의 재방문은 삭제된 Query를 새로 읽는다. 5초는 시간 차이를 빠르게 확인하기 위한 실험값이지 운영 권장값이 아니다.
최신성과 보존은 서로 독립적이다
이 실험에서 staleTime: Infinity인데도 1초 뒤 새 요청이 생긴 이유가 여기에 있다. 최신성 정책은
“남아 있는 사본을 다시 확인할까”를 답한다. 사본 자체가 삭제됐다면 다시 mount할 때 읽을 데이터가
없으므로 최초 요청과 로딩부터 시작한다.
반대로 gcTime을 길게 해도 서버 원본과 사본이 같은지는 보장하지 않는다. 오래된 사본을 오래
남기는 것과 최신 사본을 남기는 것은 다른 문제다. 최신성은 13편의 staleTime과 다시 가져오기
계기로 판단한다.
재방문 시간과 메모리 비용으로 정한다
다음 네 가지를 함께 본다.
- 재방문 간격: 목록과 상세를 오가는 사용자가 몇 초나 몇 분 안에 돌아오는가?
- 다시 읽기 비용: 응답 지연, 서버 부하와 모바일 네트워크 비용이 얼마나 큰가?
- 사본 크기: 한 Query 응답이 작고 단순한가, 큰 목록과 이미지 메타데이터를 담는가?
- 키의 개수: 사용자 한 명이 세션 동안 서로 다른 상품·검색 조건 Query를 얼마나 만들 수 있는가?
작고 자주 되찾는 분류표는 비교적 길게 남길 수 있다. 큰 검색 결과가 사용자 입력마다 새
queryKey를 만든다면 긴 gcTime은 다시 쓰지 않을 많은 사본을 쌓을 수 있다. Infinity는 자동
삭제를 끄므로 “요청을 줄인다”는 이유 하나로 전체 Query의 공통값으로 두지 않는다.
메모리 보존은 영구 저장 계약이 아니다
gcTime의 대상은 현재 Query 캐시의 메모리 사본이다. 브라우저 새로고침이나 앱 프로세스 종료 뒤에도
데이터를 남기는 영구 저장 정책, 로그아웃 때 사용자별 캐시를 비우는 보안 정책, 오프라인 복원은 각각
별도 설계가 필요하다. gcTime을 길게 했다는 사실만으로 이 계약들이 해결되지는 않는다.
문제를 만났을 때의 조사 순서
- 반복 로딩이 같은
queryKey를 다시 mount하는 상황인지 확인한다. - 닫는 순간 같은 Query의 다른 구독이 남아 있는지 확인한다.
- 다시 열 때 최초 대기 상태와 실제 요청이 생기는지 관찰한다.
staleTime을 고정해 최신성에 따른 배경 요청과 캐시 삭제를 분리한다.- 짧은 실험
gcTime으로 보존 전후를 재현한 뒤 운영 재방문 간격과 메모리 비용을 측정한다.
한 번 더 예측하고 확인한다
staleTime: Infinity,gcTime: 1초에서 2초간 닫았다 열면 로딩과 요청 번호가 어떻게 될지 예측한다.- 같은 설정에서 다른 컴포넌트가 같은 Query를 계속 구독하면 2초 뒤에도 삭제될지 예측한다.
gcTime: 5초에서 2초 만에 돌아올 때와 6초 만에 돌아올 때의 차이를 확인한다.gcTime을 길게 바꿔도 서버 값의 최신성이 자동으로 보장되는지 설명한다.
문제 상품 미리보기를 잠깐 닫았다 열 때마다 로딩과 요청이 반복됨
잘못된 판단 fresh 데이터이므로 화면에서 사라져도 계속 캐시에 남을 것임
관찰 구독이 0개가 된 뒤 짧은 gcTime이 지나 Query가 삭제됨
원인 staleTime의 최신성 판정과 gcTime의 메모리 보존을 같은 것으로 봄
행동 대표 재방문 시간과 사본 비용으로 비활성 보존 시간을 결정
검증 보존 시간 안에는 요청 번호 재사용, 시간이 지나면 새 요청 발생PR에서 바로 확인할 항목
- 같은 Query를 사용하는 구독이 언제 0개가 되는지 설명할 수 있는가?
staleTime과gcTime의 책임을 각각 최신성과 비활성 보존으로 구분했는가?- 운영값을 재방문 간격, 응답 크기, 키 개수와 재요청 비용으로 설명할 수 있는가?
Infinity를 쓰면 자동 삭제가 꺼져도 괜찮은 Query 집합인지 확인했는가?- Web route와 Native screen 전환에서 컴포넌트가 실제로 unmount되는지 확인했는가?
- 메모리 캐시와 영구 저장·로그아웃 삭제 정책을 혼동하지 않는가?
기억할 질문은 하나다.
이 Query를 아무도 쓰지 않는 동안, 다시 돌아올 가능성을 위해 얼마 동안 메모리에 남길 것인가?
Active recall
기억에서 꺼내 보기
답을 쓰지 않아도 됩니다. 먼저 머릿속으로 답한 뒤 펼쳐서 정답·이유·흔한 오해를 비교하세요.
01화면 일부를 닫으면 그 Query는 무조건 inactive가 되는가?
정답
아니다. 다른 컴포넌트에 같은 Query 결과를 실제로 구독하는 곳이 남아 있으면 여전히 active다.
관련 설명 다시 읽기왜 그런가
gcTime의 시작점은 눈에 보이는지 여부가 아니라 Query의 active observer가 하나도 없어지는 시점이다.
선택한 상태와 다음 복습일은 이 브라우저에만 저장됩니다.
02staleTime이 Infinity면 gcTime이 짧아도 사본이 계속 남는가?
정답
아니다. fresh인 사본도 inactive가 된 뒤 gcTime이 지나면 캐시에서 삭제될 수 있다.
관련 설명 다시 읽기왜 그런가
staleTime은 다시 확인할지, gcTime은 사용하지 않는 사본을 남길지 결정한다.
선택한 상태와 다음 복습일은 이 브라우저에만 저장됩니다.
03gcTime은 어떤 질문에서 출발해 정해야 하는가?
정답
사용자가 같은 데이터를 다시 찾는 시간 안에 사본을 남겨 재요청과 로딩을 줄일 가치가 메모리 비용보다 큰가에서 출발한다.
관련 설명 다시 읽기왜 그런가
재방문 간격, 응답 크기, 생성 가능한 queryKey 수와 다시 요청하는 비용을 함께 본다.
선택한 상태와 다음 복습일은 이 브라우저에만 저장됩니다.
출처와 검증 범위
아래 날짜는 링크를 마지막으로 열어 본 날입니다. 문서 상단의 검증일은 글의 설명과 적용 범위를 다시 확인한 날입니다.
- Caching ExamplesTanStack · 공식 문서 · 확인 2026-08-09
- useQueryTanStack · 공식 문서 · 확인 2026-08-09
- Important DefaultsTanStack · 공식 문서 · 확인 2026-08-09