TanStack Query staleTime과 gcTime은 어떤 시간을 기준으로 동작하나요?
staleTime은 캐시 데이터를 최신으로 취급하는 시간, gcTime은 사용하지 않는 캐시를 메모리에서 정리하는 시간에 관계합니다. 캐시에 값이 남아 있는 것과 재조회가 필요한 상태는 다릅니다.
적용 조건·범위: 자가 확인·변형: staleTime은 성공 데이터 수신 시점, gcTime은 구독이 사라진 시점이 기준입니다. 목록을 계속 켜 두는 것만으로 2초마다 자동 요청하지는 않습니다.
본문 근거: tanstack.com/query/latest/docs/framework/react/guides/important-defaults · tanstack.com/query/latest/docs/framework/react/reference/interfaces/UseQueryOptions
이 기술만 따로 연습하기
이 개념 직접 실행
버튼을 조작하고 로딩·결과·캐시의 변화를 확인합니다.
예제 크게 보기 (새 탭)기대 결과: 첫 요청이 끝난 뒤 2초 안에 닫았다 열면 같은 번호입니다. 데이터 수신 뒤 2초가 지났지만 닫은 지 4초 미만이면 이전 번호를 보여주며 재확인합니다. 닫은 상태로 4초가 지나면 초기 요청 중부터 시작합니다.
직접 실행: 환경 준비와 전체 파일
실행 환경은 React·TypeScript와 @tanstack/react-query 5.104.0입니다. React 환경이 없다면 React Vite 사용법 Vite 8 프로젝트 생성 실행 빌드를, 라이브러리 설치는 TanStack Query Provider와 첫 useQuery 상태 처리 실습 가이드의 설치 항목을 확인합니다. 아래는 별도 Vite 연습 앱 src/App.tsx 전체 내용입니다. src/main.tsx는 유지하며 이전 TaskFlow나 API 서버는 필요하지 않습니다. 요청 함수는 이 파일 안에 정의된 비동기 모형입니다.
import { useState } from 'react';
import {
QueryClient,
QueryClientProvider,
useQuery
} from '@tanstack/react-query';
const wait = (ms = 400) => new Promise<void>((resolve) => setTimeout(resolve, ms));
const client = new QueryClient({
defaultOptions: { queries: { retry: false, refetchOnWindowFocus: false } },
});
let requests = 0;
function Result() {
const query = useQuery({
queryKey: ['timed'],
queryFn: async () => { await wait(); return ++requests; },
staleTime: 2_000,
gcTime: 4_000,
});
return <p>{query.isPending ? '초기 요청 중' : `요청 번호 ${query.data}`}
{query.isFetching && !query.isPending ? ' / 재확인 중' : ''}</p>;
}
function Demo() {
const [visible, setVisible] = useState(true);
return (
<main>
<button onClick={() => setVisible((value) => !value)}>목록 열기/닫기</button>
{visible ? <Result /> : <p>구독 없음</p>}
</main>
);
}
export default function App() {
return (
<QueryClientProvider client={client}>
<Demo />
</QueryClientProvider>
);
}
npm run dev
npm run build
자가 확인·변형: staleTime은 성공 데이터 수신 시점, gcTime은 구독이 사라진 시점이 기준입니다. 목록을 계속 켜 두는 것만으로 2초마다 자동 요청하지는 않습니다.
검증 기록: 위 전체 코드는 타입검사·빌드와 jsdom 동작 검증을 통과했습니다. 실제 브라우저 화면과 누적 Next.js 프로젝트 통합 실행은 별도 확인 대상입니다.
staleTime
staleTime은 데이터가 fresh로 취급되는 시간입니다.
useQuery({
queryKey: taskKeys.list('all'),
queryFn: getTasks,
staleTime: 30_000,
});
위 설정은 성공적으로 받은 데이터가 약 30초 동안 fresh로 취급된다는 뜻입니다.
fresh인 동안에는 일반적인 자동 재검증 조건에서 불필요한 refetch를 줄일 수 있습니다.
TanStack Query의 기본 staleTime은 0이므로 성공한 데이터는 기본적으로 바로 stale 상태가 될 수 있습니다.
stale은 삭제가 아닙니다
stale은 캐시가 사라졌다는 뜻이 아닙니다.
fresh
→ 최신으로 간주
stale
→ 캐시는 있지만 다시 확인할 수 있는 상태
garbage collected
→ 비활성 캐시가 메모리에서 제거
stale 데이터도 화면에 즉시 보여주면서 백그라운드 refetch가 일어날 수 있습니다.
gcTime
gcTime은 query가 더 이상 사용되지 않는 inactive 상태가 된 뒤 캐시를 메모리에 얼마나 보관할지 정합니다.
useQuery({
queryKey: taskKeys.detail(taskId),
queryFn: () => getTask(taskId),
gcTime: 10 * 60 * 1000,
});
현재 React Query v5의 일반적인 클라이언트 기본 gcTime은 5분이고 SSR에서는 Infinity가 기본입니다.
staleTime과 gcTime 비교
staleTime
→ 데이터 신선도 기준
→ refetch 판단에 영향
gcTime
→ 사용하지 않는 캐시의 메모리 보존 시간
→ inactive query 제거 시점에 영향
둘은 목적이 다릅니다.
데이터 특성에 따라 다르게 설정
작업 목록이 자주 바뀐다면 짧게 둘 수 있습니다.
staleTime: 15_000
상세 데이터가 거의 바뀌지 않는다면 더 길게 둘 수 있습니다.
staleTime: 60_000
“몇 초가 정답”인 것은 아닙니다.
다음 기준으로 정합니다.
- 다른 사용자가 자주 데이터를 바꾸는가
- 실시간에 가까운 정확성이 필요한가
- 재요청 비용이 큰가
- 사용자가 화면을 자주 왕복하는가
- mutation 후 명시적으로 invalidation하는가
staleTime을 너무 길게 설정하면
서버 데이터가 바뀌어도 클라이언트가 오랫동안 fresh로 판단할 수 있습니다.
따라서 데이터 변경 경로가 많은 서비스라면 mutation invalidation이나 적절한 staleTime이 필요합니다.
gcTime을 너무 짧게 설정하면
화면을 잠깐 벗어났다가 돌아왔을 때 캐시가 이미 제거되어 다시 처음부터 요청할 수 있습니다.
메모리 절약과 화면 왕복 경험 사이에서 판단합니다.
학습 완료 체크리스트
- fresh와 stale을 구분할 수 있습니다.
- staleTime이 데이터 신선도를 정한다는 것을 이해했습니다.
- gcTime이 inactive 캐시의 제거 시간을 정한다는 것을 이해했습니다.
- 캐시가 존재하는 것과 fresh 상태가 같은 뜻이 아님을 설명할 수 있습니다.
- 데이터 특성에 따라 시간을 조정할 수 있습니다.
선택 실습: 할 일 앱에 적용하기
TaskFlow는 작업을 추가·완료·삭제하는 할 일 앱입니다. 이 선택 실습에서는 서버 데이터의 조회·변경과 캐시를 관리합니다.
작업 목록과 상세의 캐시 정책을 명시합니다.
선택: 연재 프로젝트와 전체 자료
연재 프로젝트를 이어 만드는 분을 위한 자료입니다. 이 글의 개념과 예제만 찾으셨다면 필요한 파일과 실행 방법만 펼쳐보세요.
TaskFlow 프로젝트 적용
중간부터 시작하는 전체 실습 파일
기존 Next.js Route Handler TaskFlow의 /tasks 화면과 /api/tasks를 이어서 확장합니다. 인증 없는 demo 메모리 저장소를 사용하는 로컬 실습이며 계정·외부 DB·API 키가 필요 없습니다. MSW를 활성화하지 않습니다. 서버를 다시 시작하면 데이터가 초기화됩니다.
이 글의 시작본은 직전 프로젝트 단계 완료본 전체입니다. 이미 직전 단계를 마쳤다면 그 폴더를 이어 쓰셔도 됩니다. Node.js 24.x와 npm을 사용하며 패키지 버전은 package-lock.json에 고정했습니다.
시작본에서 직접 바꾸기
시작본을 실행한 뒤 아래 변경 파일을 순서대로 수정합니다. 완료본의 같은 경로 파일에는 import와 입력 타입을 포함한 전체 코드가 있습니다.
app/tasks/TaskListClient.tsx— 기존 파일의 같은 위치를 갱신합니다.
변경 이유와 결과: staleTime 30초·gcTime 5분을 명시하고 새로고침/필터 왕복의 요청 의미를 분리합니다.
완료본의 docs/changes.patch에서 시작·완료 차이를, docs/current-step.md에서 파일별 적용 위치를 확인할 수 있습니다. 아래 전체 파일을 같은 경로에 반영한 다음 실행하세요.
app/tasks/TaskListClient.tsx 전체 파일
'use client';
import { useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import type { Task } from '../lib/types';
import { getTasks, type TaskStatus } from '../lib/tasks-api';
import { taskKeys } from '../lib/task-keys';
export default function TaskListClient({ status = 'all', onSelect = () => {} }: {
status?: TaskStatus; onSelect?: (id: string) => void;
}) {
const [fail, setFail] = useState(false);
const query = useQuery({
queryKey: taskKeys.list(status),
queryFn: ({ signal }) => getTasks(signal, status, fail),
staleTime: 30_000, gcTime: 5 * 60_000,
});
return (
<section aria-label="서버 작업 목록">
<label><input type="checkbox" checked={fail} onChange={(event) => setFail(event.target.checked)} />실습 조회 실패</label>
<button onClick={() => void query.refetch()}>목록 다시 확인</button>
{query.isFetching && <p role="status">요청 중입니다.</p>}
{query.isPending ? <p>불러오는 중입니다.</p> : query.isError ? <p role="alert">{query.error.message}</p> : query.data.length === 0 ? <p>표시할 작업이 없습니다.</p> : (
<ul>
{query.data.map((task) => (
<li key={task.id}>
<button onClick={() => onSelect(task.id)}>{task.title}</button>
<span> · {task.completed ? '완료' : '진행 중'}</span>
</li>
))}
</ul>
)}
</section>
);
}
완료 확인
- 시작본은 직전 완료본과 동일한 ZIP입니다. 압축을 푼 뒤
npm ci로 잠금 파일의 의존성을 설치합니다. npm run dev실행 후http://localhost:3000/tasks를 엽니다.- staleTime 30초·gcTime 5분을 명시하고 새로고침/필터 왕복의 요청 의미를 분리합니다.
npm run typecheck와npm run build가 성공하면 다음 프로젝트 단계로 갑니다.
이 체크포인트는 TypeScript 검사·프로덕션 빌드와 실제 소스를 사용한 JSDOM/상태 시나리오 검증을 통과했습니다. 별도 실제 브라우저 화면 검수는 이 파일 검증 범위에 포함하지 않았습니다.
아래 부분 예제는 핵심 변경을 설명합니다. 실행에 필요한 전체 파일과 보조 타입·API는 위 완료본에서 확인하세요.
누적 실습은 이 절의 프로젝트 단계 링크를 따라 진행합니다. 페이지 끝 학습 순서는 기술 주제 읽기 순서입니다.
이전 프로젝트 단계: TanStack Query queryKey 설계 기준: 배열 키로 캐시 구분하기. 위 시작본 ZIP 전체를 내려받으면 이전 글을 다시 조립하지 않고 이 단계부터 실행할 수 있습니다.
목록·상세 useQuery의 staleTime과 gcTime을 각각 명시합니다. 시간을 바꾸는 위치와 화면 왕복 시 요청 여부를 함께 기록합니다.
이전 체크포인트
query-step-04-querykey
이번 목표
작업 목록과 상세의 캐시 정책을 명시합니다.
목록
useQuery({
queryKey: taskKeys.list('all'),
queryFn: ({ signal }) =>
getTasks(signal),
staleTime: 30_000,
gcTime: 5 * 60 * 1000,
});
상세
useQuery({
queryKey: taskKeys.detail(taskId),
queryFn: ({ signal }) =>
getTask(taskId, signal),
staleTime: 60_000,
gcTime: 10 * 60 * 1000,
});
위 숫자는 학습 예시입니다. 실제 서비스에서는 API 변경 빈도와 사용자 흐름을 기준으로 조정합니다.
관찰 실습
- 작업 목록 페이지를 엽니다.
- 다른 페이지로 이동합니다.
- staleTime 안에 다시 돌아옵니다.
- staleTime이 지난 뒤 다시 돌아옵니다.
- Network 요청 차이를 확인합니다.
현재 확장 단계
✓ 1~4. 조회 구조와 queryKey
→ 5. staleTime / gcTime
○ 6. mutation
○ 7. invalidation
○ 8~11. 페이지·복구·캐시 동기화
○ 12~15. SSR·Hydration
완료 체크포인트
query-step-05-cache-time
다음 학습
다음 글에서는 조회에서 변경으로 넘어가 useMutation으로 작업을 저장하고 pending/error/success 흐름과 목록 갱신을 연결합니다.
다음 프로젝트 단계: TanStack Query useMutation 저장 실습: 실패·재시도와 목록 갱신까지
공식 문서
- Important Defaults: https://tanstack.com/query/latest/docs/framework/react/guides/important-defaults
- useQuery options: https://tanstack.com/query/latest/docs/framework/react/reference/interfaces/UseQueryOptions
프로젝트 적용 후 확인: staleTime과 gcTime은 무엇을 바꿀까요?
관찰 절차 Devtools에서 staleTime을 60_000, gcTime을 120_000으로 설정하고 /tasks 목록에 들어갔다 다른 화면으로 이동한 뒤 30초 안에 복귀합니다. 다시 60초 이후 복귀한 경우와 비활성 상태로 120초를 넘긴 경우를 Network와 함께 비교합니다.
예상 결과 신선한 활성/재진입 데이터는 정책상 재사용되고 stale 데이터는 조건에 따라 백그라운드 재요청될 수 있습니다. 비활성 캐시의 gcTime 경과 뒤에는 캐시가 제거될 수 있습니다.
오류 점검 예상과 다른 요청은 remount 여부, refetchOnMount, 창 포커스와 Devtools의 fresh/stale/active 표시를 먼저 봅니다.
다음 학습 6회차에서 저장 후 캐시를 갱신합니다.
검증 범위 전체 체크포인트의 빌드·상태 동작은 로컬 검증을 통과했습니다. 실제 브라우저 시각 확인은 별도로 진행해야 합니다.
글 소개·학습 준비·전체 목차

TanStack Query · 5 / 15 · 필수 학습 · 캐시 수명
TanStack Query를 사용하면 “캐시가 있으니 요청을 다시 하지 않는다”라고 단순하게 생각하기 쉽습니다.
실제로는 캐시에 데이터가 있는지와 그 데이터가 fresh인지, stale인지, 그리고 사용하지 않는 캐시가 언제 메모리에서 제거되는지를 나눠서 봐야 합니다.
이번 글에서 배우는 내용
- fresh와 stale의 차이
- staleTime의 역할
- gcTime의 역할
- active와 inactive query
- 데이터 특성에 따른 시간 설정
- TaskFlow 목록과 상세 캐시 정책
목차
이 글이 도움이 되었나요?
읽음 기록
읽음 — / 전체 15개 · 필수 14개
새 글 받아보기
RSS 리더에서 BlogFlow의 새 글을 확인할 수 있습니다.