TanStack Query staleTime gcTime 차이: 캐시 시간 기준

2026.04.27·수정 2026.09.14·약 32분·작성: 해비·블로그 소개

먼저 읽기: TanStack Query Provider와 첫 useQuery 상태 처리 실습 가이드 · TanStack Query queryKey 설계 기준: 배열 키로 캐시 구분하기

이 글에서 정리하는 내용

TanStack Query v5 기준으로 staleTimegcTime의 차이를 정리합니다. 캐시가 남아 있는데도 요청이 다시 나가는 이유, v4의 cacheTime이 v5에서 gcTime으로 바뀐 이유, 실제 React 화면에서 두 옵션을 나눠 설정하는 기준을 함께 봅니다.

타이머 만료, 무효화, 수동 요청을 구분하기

staleTime 만료 자체가 요청을 실행하는 타이머는 아닙니다. 만료 후 mount·focus·reconnect 같은 조건이 생겨야 해당 정책에 따라 다시 읽습니다. 주기 조회는 refetchInterval이라는 별도 기능입니다. gcTime도 freshness를 연장하지 않습니다. fresh 데이터라도 마지막 observer가 해제된 뒤 gcTime이 지나면 제거될 수 있습니다.

설정 시간 경과 명시적 invalidation hook의 refetch()
숫자 만료 후 stale 일치한 active 쿼리 기본 재조회 요청 가능
Infinity 시간으로 stale이 되지 않음 무효화 및 기본 active 재조회 가능 요청 가능
‘static’ 시간으로 stale이 되지 않음 해당 static observer가 있는 쿼리는 client 일괄 refetch 대상에서 제외 직접 호출은 가능

이 표는 enabled 상태의 단일 observer 기준입니다. static은 모든 요청을 영원히 금지하는 보안 장치가 아닙니다. setQueryData도 캐시를 바꿀 수 있습니다. Infinity는 refetchOnMount 등의 ‘always’나 별도 polling까지 금지하는 값도 아닙니다. 반면 static은 mount·focus·reconnect의 ‘always’도 막습니다. 서로 다른 옵션의 observer가 같은 key를 공유하면 observer별 freshness와 query 단위 invalidation 결과를 함께 봐야 합니다.

브라우저 기본 gcTime은 5분, SSR 기본값은 Infinity입니다. 메모리 캐시는 페이지 새로고침 뒤 영속 보관되지 않습니다. 여러 사용처에서 같은 쿼리에 서로 다른 gcTime을 지정하면 더 긴 값이 적용됩니다. enabled:false인 observer는 자동 조회를 하지 않는 상태이지 반드시 observer 자체가 없는 상태는 아니므로 “inactive이면 무조건 삭제 타이머가 돈다”로 일반화하지 않습니다. 보통 화면을 떠나 마지막 구독이 해제되는 상황으로 gcTime을 이해하면 됩니다.

캐시가 있는데도 다시 요청되는 상황

TanStack Query staleTime gcTime 차이: 캐시 시간 기준 핵심 개념을 설명하는 첫 번째 본문 이미지

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분 동안 캐시에 남겨둡니다. 여기서 staleTimegcTime은 서로 대신할 수 없습니다. 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의 보관 시간을 줄여야 할 수 있음

화면 성격에 맞게 설정 기준 잡기

TanStack Query staleTime gcTime 차이: 캐시 시간 기준 적용 흐름을 설명하는 두 번째 본문 이미지

실제 프로젝트에서는 모든 쿼리에 같은 시간을 넣기보다 화면 성격에 맞게 나누는 쪽이 더 안정적입니다. 먼저 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가 자주 보일 수 있습니다.

staleTimegcTime을 안정적으로 쓰려면 먼저 캐시를 나누는 기준인 TanStack Query queryKey 배열 설계도 함께 확인하는 것이 좋습니다.

정리

staleTimegcTime을 헷갈리는 이유는 둘 다 캐시와 관련된 시간처럼 보이기 때문입니다. 하지만 역할은 분명히 다릅니다. staleTime은 데이터가 fresh로 취급되는 시간이고, 이 시간이 지나 stale 상태가 되면 새 인스턴스 마운트, 창 포커스, 네트워크 재연결 같은 조건에서 백그라운드 refetch 대상이 될 수 있습니다.

gcTime은 쿼리가 사용 중일 때가 아니라 unused 또는 inactive 상태가 된 뒤 캐시를 언제 제거할지 정하는 옵션입니다. v5에서 cacheTime이라는 이름이 gcTime으로 바뀐 것도 이 동작을 더 정확히 드러내기 위한 변경으로 볼 수 있습니다.

요청이 너무 자주 나가는 문제를 만나면 먼저 staleTime을 확인합니다. 화면을 떠났다가 돌아왔을 때 이전 데이터가 남아 있지 않다면 gcTime을 확인합니다. 이 기준을 나눠두면 TanStack Query 캐시 설정을 단순히 숫자 외우기가 아니라 화면 흐름에 맞춰 판단할 수 있습니다.

함께 읽으면 좋은 글

공식 문서

목록을 닫았다 열며 freshness와 메모리 제거를 구분하는 실습

전체 실습 ZIP 다운로드

3000 모드로 첫 요청 횟수를 보고 3초를 기다립니다. 자동 증가가 없는지 보고 목록을 닫았다 다시 엽니다. 이번에는 닫은 뒤 5초가 지난 후 열어 캐시 제거를 관찰합니다. Infinity와 static 각각에서 무효화 버튼과 직접 refetch 버튼을 비교합니다.

Node.js 22.12 이상(또는 20.19 이상)에서 압축을 푼 폴더로 이동합니다. 외부 API 키는 필요 없습니다. fixture 값은 새로고침·프로세스 종료 후 보존되지 않습니다.

npm install
npm test
npm run build
npm run dev

HTML 진입점·CSS·JavaScript 파일을 분리했습니다. 본문의 기존 API 코드가 설명하는 실제 서버와 ZIP의 메모리 fixture는 다른 실행 범위입니다.

App.jsx

실습 파일

파일 경로를 확인하고 같은 프로젝트 안에 저장하세요. 이미지 등 소스 목록에 없는 파일과 실행 안내는 실습 ZIP에 포함되어 있습니다.

전체 코드

App.jsx

import React, { useState } from 'react';
import {
  QueryClient,
  QueryClientProvider,
  useQuery,
  useQueryClient,
} from '@tanstack/react-query';
const client = new QueryClient();
let reads = 0;
function Sample({ mode }) {
  const query = useQuery({
    queryKey: ['clock', mode],
    queryFn: async () => ({ read: ++reads }),
    staleTime: mode === 'Infinity' ? Infinity : mode === 'static' ? 'static' : 3000,
    gcTime: 5000,
  });
  return (
    <section>
      <p>요청 횟수: {query.data?.read ?? '대기'}</p>
      <p>
        {query.isStale ? 'stale' : 'fresh'} / {query.isFetching ? 'fetching' : 'idle'}
      </p>
      <button onClick={() => query.refetch()}>직접 refetch</button>
    </section>
  );
}
function Lab() {
  const [shown, setShown] = useState(true);
  const [mode, setMode] = useState('3000');
  const qc = useQueryClient();
  return (
    <main>
      <h1>캐시 시간 실험</h1>
      <label>
        staleTime{' '}
        <select value={mode} onChange={(e) => setMode(e.target.value)}>
          <option>3000</option>
          <option>Infinity</option>
          <option>static</option>
        </select>
      </label>
      <button onClick={() => setShown(!shown)}>
        {shown ? '목록 닫기' : '목록 열기'}
      </button>
      <button onClick={() => qc.invalidateQueries({ queryKey: ['clock', mode] })}>
        현재 키 무효화
      </button>
      {shown && <Sample mode={mode} />}
      <p>
        3초 freshness, 마지막 구독 종료 후 5초 gcTime을 비교하세요. 타이머만 만료되어도
        요청 횟수가 증가하는지 관찰하세요.
      </p>
    </main>
  );
}
export default function App() {
  return (
    <QueryClientProvider client={client}>
      <Lab />
    </QueryClientProvider>
  );
}

cache.test.js

import { afterEach, expect, test, vi } from 'vitest';
import { QueryClient, QueryObserver } from '@tanstack/react-query';
const clients = [];
function setup(staleTime, gcTime = Infinity) {
  const client = new QueryClient({
    defaultOptions: { queries: { retry: false, gcTime } },
  });
  clients.push(client);
  const fn = vi.fn(async () => ({ count: 1 }));
  const observer = new QueryObserver(client, {
    queryKey: ['clock'],
    queryFn: fn,
    staleTime,
  });
  return { client, fn, observer };
}
afterEach(() => {
  clients.forEach((c) => c.clear());
  clients.length = 0;
  vi.useRealTimers();
});
test('freshness expiry does not itself poll, stale remount refetches', async () => {
  vi.useFakeTimers();
  const { fn, observer } = setup(1000);
  const off = observer.subscribe(() => {});
  await observer.refetch();
  await vi.advanceTimersByTimeAsync(1100);
  expect(fn).toHaveBeenCalledTimes(1);
  off();
  const off2 = observer.subscribe(() => {});
  await vi.advanceTimersByTimeAsync(0);
  expect(fn).toHaveBeenCalledTimes(2);
  off2();
});
test('gc removes only after last observer leaves, even fresh data', async () => {
  vi.useFakeTimers();
  const { client, observer } = setup(Infinity, 1000);
  const off = observer.subscribe(() => {});
  await observer.refetch();
  await vi.advanceTimersByTimeAsync(2000);
  expect(client.getQueryData(['clock'])).toBeDefined();
  off();
  await vi.advanceTimersByTimeAsync(1001);
  expect(client.getQueryData(['clock'])).toBeUndefined();
});
test.each([Infinity, 'static'])(
  'invalidation and explicit refetch differ for %s',
  async (staleTime) => {
    const { client, fn, observer } = setup(staleTime);
    const off = observer.subscribe(() => {});
    await observer.refetch();
    await client.invalidateQueries({ queryKey: ['clock'] });
    expect(fn).toHaveBeenCalledTimes(staleTime === Infinity ? 2 : 1);
    const before = fn.mock.calls.length;
    await observer.refetch();
    expect(fn).toHaveBeenCalledTimes(before + 1);
    off();
  },
);

index.html

<!doctype html>
<html lang="ko">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Query 실습</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/main.jsx"></script>
  </body>
</html>

main.jsx

import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './App.jsx';
import './style.css';
createRoot(document.getElementById('root')).render(<App />);

package.json

{
  "name": "query-lab-2412",
  "private": true,
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "test": "vitest run"
  },
  "dependencies": {
    "@tanstack/react-query": "5.102.8",
    "react": "19.3.0",
    "react-dom": "19.3.0"
  },
  "devDependencies": {
    "vite": "8.3.0",
    "vitest": "4.1.11",
    "jsdom": "30.0.1",
    "@testing-library/react": "16.3.3"
  }
}

style.css

body {
  margin: 0;
  color: #222;
  background: #fff;
  font-family: system-ui, sans-serif;
}
main {
  max-width: 760px;
  margin: 40px auto;
  padding: 24px;
}
button,
select {
  padding: 10px;
  margin: 6px;
  border: 1px solid #888;
  background: #f5f5f5;
  color: #222;
}
button:disabled {
  color: #777;
}
li {
  padding: 8px;
  border-bottom: 1px solid #ddd;
}
pre {
  padding: 16px;
  background: #eee;
  overflow: auto;
}

App.jsx 전체 코드 보기

cache.test.js

cache.test.js 전체 코드 보기

index.html

index.html 전체 코드 보기

main.jsx

main.jsx 전체 코드 보기

package.json

package.json 전체 코드 보기

style.css

style.css 전체 코드 보기

검증 결과와 이 실습의 경계

Vitest Node 환경 4개 테스트: staleTime 만료만으로 재요청하지 않음, stale 상태 재구독의 요청, 마지막 구독 이후 fresh 캐시 수거, Infinity/static 무효화와 직접 refetch 차이. Vite production build도 통과했습니다.

실제 브라우저나 HTTP API를 실행한 결과는 아닙니다.

v5 공식 문서 확인

2026-09-12 공식 문서와 설치된 @tanstack/react-query 5.102.8의 실행 결과를 대조했습니다. 문서의 latest 예시는 이후 바뀔 수 있으므로 ZIP의 고정 버전과 함께 확인하세요.

연결 학습: 캐시 수명을 단일 상태 순서로 외우지 않기

목표·선행 지식: 신선도, 조회 진행, 사용 여부를 서로 다른 축으로 구분합니다.

staleTime 만료 자체가 타이머 기반 재조회 명령은 아닙니다. 일반적인 stale 데이터는 마운트·포커스 복귀·재연결 등의 조건에서 갱신됩니다. refetchInterval은 staleTime과 독립적입니다. gcTime은 미사용 캐시의 제거 시점을 정하며 fresh라도 미사용 시간이 지나면 제거될 수 있습니다.

직접 확인할 과제

staleTime 30초, gcTime 5초로 설정하고 화면 이탈 후 6초 뒤 돌아오세요. 이어서 refetchInterval을 켜 fresh 기간에도 요청이 생기는지 비교합니다. 이 수치는 실습용이며 모든 서비스의 권장 기본값은 아닙니다.

공통 실습 ZIP · 다음 학습 · 라이브러리 선택 가이드

공통 ZIP은 버전을 고정한 학습 예제입니다. UI 파일은 Radix·Sonner·Embla 기반 축약 구현이며 shadcn CLI 생성물과 동일하지 않습니다. 적용 범위와 실행 방법은 ZIP의 README를 확인하세요.

이 글이 도움이 되었나요?

조회 중

TanStack Query 학습 순서

필수 11개 · 전체 12개

읽음 기록 관리

전체 과정 목차 (12개)
  1. 필수 길잡이 · TanStack Query vs Zustand: 서버 상태와 클라이언트 상태 차이
  2. 필수 학습 · TanStack Query Provider와 첫 useQuery 상태 처리 실습 가이드
  3. 필수 학습 · TanStack Query queryKey 설계 기준: 배열 키로 캐시 구분하기
  4. 필수 학습 · TanStack Query queryFn 사용법: 데이터 요청 로직 분리하기
  5. 필수 학습 · TanStack Query staleTime gcTime 차이: 캐시 시간 기준 현재 글
  6. 필수 학습 · TanStack Query useMutation 저장 실습: 실패·재시도와 목록 갱신까지
  7. 선택 참고 · TanStack Query 화면 갱신 문제 해결: 데이터 변경 후 UI가 바뀌지 않을 때
  8. 필수 학습 · TanStack Query 페이지네이션 실습: placeholderData로 이전 목록 유지하기
  9. 필수 학습 · TanStack Query 무한 스크롤 사용법: useInfiniteQuery로 목록 이어 불러오기
  10. 필수 선수 · TanStack Query Hydration 오류 해결: QueryClient 설정 기준
  11. 필수 학습 · TanStack Query Todo 실습: 서버 연결·낙관적 업데이트·실패 롤백
  12. 필수 학습 · TanStack Query 수동 캐시 정규화: 목록·상세 동기화와 선택 기준

새 글 받아보기

RSS 리더에서 BlogFlow의 새 글을 확인할 수 있습니다.

RSS 피드 구독하기

댓글 남기기