React|서버 상태와 비동기 데이터

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

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

크리스TanStack Query를 도입하고 나서 고개를 갸웃거린다.

"캐싱을 해준다더니 왜 창을 클릭할 때마다 API 요청이 계속 나가는 거지?"

반대로 어떤 상황에서는, "아까 닫았던 탭을 다시 열었는데 왜 로딩 스피너가 안 뜨고 옛날 데이터가 그대로 있지?"라며 당황하기도 한다.

이 혼란의 원인은 TanStack Query의 두 가지 핵심 타이머, staleTimegcTime의 차이를 명확히 이해하지 못했기 때문이다. 이 둘은 비슷해 보이지만, 데이터를 언제 "상했다"고 판단할지언제 "메모리에서 지울지"를 결정하는 전혀 다른 역할을 한다.

오늘은 리액트 쿼리 초보자가 가장 많이 헷갈리는 이 두 개념의 라이프사이클을 완벽하게 정리한다.

1. 데이터의 신선도: Fresh vs Stale

TanStack Query에서 데이터는 크게 두 가지 상태를 가진다.

  • Fresh (신선함): 막 가져온 따끈따끈한 데이터. 이 상태일 때는 아무리 컴포넌트가 다시 렌더링 되어도 API 요청을 다시 보내지 않는다.
  • Stale (상함): 유통기한이 지난 데이터. 데이터가 있긴 하지만, 서버에 최신 데이터가 있는지 확인(Refetch)할 필요가 있는 상태다.
  • staleTime: 유통기한

  • staleTime은 데이터가 Fresh에서 Stale로 변하는 데 걸리는 시간이다.
  • 충격적인 사실은 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: 쓰레기통 비우는 시간

  • *gcTime*의 기본값은 5분(300,000ms)이다.
  • 컴포넌트 언마운트 -> 데이터는 Inactive 상태 진입.
  • 타이머 시작 (5분).
  • 5분 안에 사용자가 다시 돌아옴 -> 데이터 부활 (재사용).
  • 5분 동안 안 돌아옴 -> Garbage Collector가 메모리에서 삭제.
  • 크리스가 겪은 "데이터가 그대로 남아있는 현상"은, 비록 데이터가 상했을지라도(stale), 아직 메모리에서 삭제되지 않았기(gcTime 안쪽) 때문에 캐시 된 데이터를 먼저 보여준 것이다.

    3. 라이프사이클 한눈에 보기

    이 과정을 시각화하면 다음과 같다.

  • Fetch: 데이터를 가져온다.
  • Fresh: staleTime 동안은 캐시 된 데이터를 쓴다. (네트워크 요청 X)
  • Stale: staleTime이 지났다. 데이터는 여전히 쓸 수 있지만, 다음에 필요할 때 백그라운드에서 다시 가져온다(Refetch). (네트워크 요청 O)
  • Inactive: 컴포넌트가 모두 언마운트 되었다. 캐시는 유지된다.
  • Deleted: Inactive 상태로 gcTime이 지났다. 메모리에서 완전히 삭제된다.
  • 4. 실전 전략: 어떻게 설정해야 할까?

    상황에 따라 이 두 값을 조절하는 전략이 필요하다.

    전략 1: 기본값 사용 (staleTime: 0, gcTime: 5분)

  • 대상: 게시판 글 목록, 실시간성이 중요한 데이터.
  • 동작: 페이지에 들어올 때마다 항상 새로고침 한다. 하지만 뒤로 가기로 빨리 돌아오면 로딩 없이(캐시) 보여주고, 뒤에서 조용히 최신화한다(Stale-While-Revalidate).
  • 전략 2: 정적 데이터 (staleTime: Infinity, gcTime: Infinity)

  • 대상: 설정 정보, 지역 목록, 잘 안 변하는 콘텐츠.
  • 동작: 앱이 켜져 있는 동안 딱 한 번만 불러오고, 절대 다시 요청하지 않는다.
  • // static-data.ts
    useQuery({
      queryKey: ['settings'],
      queryFn: fetchSettings,
      staleTime: Infinity, // 영원히 신선함
      gcTime: Infinity,    // 메모리에서 절대 안 지움
    });

    전략 3: 잦은 갱신 방지 (staleTime: 30초)

  • 대상: 주식 가격, 좋아요 수.
  • 동작: 사용자가 버튼을 연타하거나 탭을 자주 이동해도, 최소 30초 동안은 네트워크 요청을 막아 서버 부하를 줄인다.
  • 핵심 정리

  • staleTime (기본값 0): "언제 다시 가져올까?"를 결정한다. 이 시간이 지나기 전까지는 네트워크 요청을 절대 보내지 않는다.
  • gcTime (기본값 5분): "언제 메모리에서 지울까?"를 결정한다. 컴포넌트가 언마운트 된 후, 이 시간이 지나면 캐시가 삭제된다.
  • Tip: v5부터 cacheTime의 이름이 gcTime으로 바뀌었다. 헷갈리지 말자.
  • 이제 캐시의 수명을 제어할 수 있게 되었다. 하지만 캐시를 저장하는 이름표(Key)를 엉망으로 관리하면 데이터가 꼬이거나 중복 호출이 발생한다. 유지보수하기 좋은 키 관리 전략은 무엇일까?

    "효율적인 Query Key 관리 전략: 팩토리 패턴으로 유지보수성 높이기" 편에서 계속된다.


    🔗 참고 링크

  • TanStack Query - Caching Examples
  • Important Defaults
  • TkDodo's Blog - Practical React Query