Reese-log
  • About
  • Blog

© 2025 Reese. All rights reserved.

2025년 2월 23일

useQuery에 staleTime 구현하기: stale-while-revalidate 패턴 톺아보기

#TanStackQuery

클라이언트에서 데이터를 가져올 때 매번 새 요청을 보내는 것은 불필요한 네트워크 트래픽을 증가시키고, 사용자 경험(UX)을 저하시킬 수 있습니다. 이를 개선하기 위해, 우리는 이전 글에서 useQuery를 만들면서 캐시를 활용해 불필요한 요청을 줄이는 방법을 살펴봤습니다.

하지만, 단순히 cacheTime만을 적용하는 것만으로는 부족한 점이 있습니다. 캐시가 만료되기 전까지는 데이터를 계속 사용할 수 있지만, 캐시가 만료되는 순간 새로운 데이터를 가져올 때까지 로딩 상태가 발생하기 때문이죠.

이 문제를 해결하기 위해, 이번 글에서는 staleTime을 추가하여 데이터를 더 효율적으로 관리하는 방법을 살펴보겠습니다. 또한, 이를 제대로 이해하기 위해 staleTime이 활용하는 SWR(stale-while-revalidate) 패턴이 무엇인지도 함께 알아보겠습니다.

이번 글에서 다룰 내용


✔️ useQuery 훅을 직접 구현하며 staleTime을 추가하는 과정

✔️ cacheTime과 staleTime의 차이점 및 역할

✔️ SWR 패턴을 활용해 최신 데이터를 유지하면서도 빠른 응답을 제공하는 방법

이제 staleTime이 어떻게 동작하는지 하나씩 살펴보겠습니다.

1. cacheTime만 쓰면 어떤 점이 아쉬운가요?

1.1 cacheTime 은 어떻게 구현되었더라

typescript

const isExpired = cachedData ? now - cachedData.timestamp > cacheTime : true;

→ 캐시된 데이터는 cacheTime이 지나기 전까지 유지되며, 시간이 초과되면 삭제됩니다. 이후 새로운 요청이 발생하면 다시 데이터를 가져오죠.

1.2 cacheTime만 쓰면 아쉬운 점

1. 최신 데이터가 필요한데 갱신되지 않을 수 있음

  • cacheTime이 만료되기 전까지는 이전 데이터를 계속 사용하므로 → 사용자가 최신 데이터를 확인할 방법이 없음.
  • 만약 cacheTime을 길게 설정하면? → 최신성이 떨어짐 😕
  • 반대로 짧게 설정하면? → 불필요한 네트워크 요청이 많아짐 😵
  • 2. cacheTime이 지나면 데이터가 삭제되므로, 새 요청이 오기까지 로딩이 발생함

  • cacheTime이 초과되면 캐시된 데이터를 즉시 삭제함.
  • 사용자가 페이지를 열었을 때, 캐시가 만료되었으면? → 데이터가 사라지고, 새 데이터를 가져오는 동안 로딩 상태가 발생함.
  • 즉, "이전 데이터를 유지하면서도 최신 데이터로 갱신하는 방법"이 필요함.
  • 1.3 해결 방법 → staleTime 추가

    staleTime을 도입하면?

    ✔️ 이전 데이터를 유지하면서도, 백그라운드에서 최신 데이터를 요청 가능

    ✔️ staleTime이 지나기 전까지는 기존 데이터를 그대로 표시 → 로딩 없이 즉시 응답

    ✔️ staleTime이 지나면, 데이터를 stale 상태로 간주하고 백그라운드에서 새 요청을 보내 최신 데이터로 갱신


    2. staleTime 이해하기

    2.1 staleTime이란?

  • staleTime은 데이터가 "신선한(fresh)" 상태로 유지되는 시간을 의미합니다.
  • staleTime이 지나면 데이터가 "stale(오래된)" 상태로 간주되며 → 화면에는 기존 데이터를 유지하되, 백그라운드에서 새로운 데이터를 요청
  • 2.2 staleTime vs cacheTime 무슨 차이점이 있나요?

    2.3 staleTime을 적용하면 어떻게 달라질까?

    ✔️ 캐시된 데이터를 즉시 제공하여 빠른 응답 가능

    ✔️ staleTime 이후에는 백그라운드에서 새 데이터를 가져와 UI 업데이트

    ✔️ 최신 데이터 요청 시에도 로딩 상태가 발생하지 않음

    cacheTime와 staleTime이 동작하는 흐름 살펴보기

    MERMAID
  • 사용자가 데이터를 요청하면 useQuery는 먼저 캐시에서 확인
  • cacheTime이 지나지 않았으면 기존 데이터를 그대로 반환
  • staleTime이 지나면 백그라운드에서 새 데이터를 요청하여 캐시 갱신
  • cacheTime이 지나면 캐시 삭제 후, API에서 새 데이터를 받아옴

  • 3. 자체제작 useQuery에 staleTime 옵션 직접 추가하기

    3.1 staleTimestamp 추가

    이제 데이터가 stale한지 판단하기 위한 staleTimestamp를 추가합니다.

    typescript
    const queryCache = new Map<string, CacheData<unknown>>();

    → 기존 queryCache에 staleTimestamp 값을 추가하여 관리

    3.2 staleTime이 지나면 데이터가 stale 상태로 변경

    typescript
    
    const isStale = cachedData ? now - cachedData.staleTimestamp > staleTime : true;

    → staleTime이 지나면 isStale을 true로 설정

    3.3 캐시된 데이터가 stale한 경우 백그라운드에서 새 요청

    typescript
    if (cache && cachedData && !isExpired && isStale) {
      setData(cachedData.data); // 기존 데이터 유지
      setIsPending(false);
    }

    → 오래된 데이터(stale)라도 화면에는 표시하지만, 새 데이터를 백그라운드에서 요청

    3.4 백그라운드에서 데이터 업데이트 실행

    typescript
    if (isStale || !cachedData) {
      if (!cachedData) setIsPending(true);
      executeQuery(); // 새 데이터 요청
    }

    → staleTime이 지나면 새 요청을 백그라운드에서 실행하여 최신 데이터로 업데이트

    실제 코드가 궁금하다면 ⇒ GitHub가서 코드 보기

    화면으로 직접 보고싶다면 ⇒ 이동하기 *console창을 켜고 직접 확인해보세요!


    4. stale-while-revalidate 패턴과의 연관성

    지금까지 살펴본 staleTime을 적용한 데이터 패칭 흐름을 보면, 어디선가 본 듯한 익숙한 패턴이 떠오르지 않나요? 바로 SWR(stale-while-revalidate) 패턴입니다.

    4.1 SWR 패턴이란?

    웹 애플리케이션에서 데이터를 가져올 때, 항상 새로운 데이터를 요청하면 불필요한 네트워크 트래픽이 발생하고,

    반대로 오래된 캐시 데이터를 그대로 사용하면 최신성이 보장되지 않는 문제가 생깁니다.

    이를 해결하기 위한 전략이 바로 SWR(stale-while-revalidate) 패턴입니다.

    SWR 패턴의 핵심 원리

  • 캐시된 데이터를 즉시 반환하여 빠른 응답을 제공
  • 백그라운드에서 새로운 데이터를 요청하여 최신 상태 유지
  • 새로운 데이터가 도착하면 UI를 업데이트하여 최신 데이터로 갱신
  • 즉, 사용자는 즉각적인 응답을 받을 수 있고, 백그라운드에서 새로운 데이터를 가져오면서 최신 상태를 유지할 수 있습니다.

    4.2 SWR를 코드로 이해하기

    우리가 만든 useQuery 훅의 동작 방식도 SWR 패턴과 거의 동일합니다.

    SWR 개념을 코드로 표현하면 다음과 같습니다.

    typescript
    if (cache && cachedData && !isExpired) {
      setData(cachedData.data); // Stale 데이터 즉시 반환
      if (isStale) executeQuery(); // 백그라운드에서 최신 데이터 요청 (Revalidate 진행)
    }

    ✔️ cacheTime이 지나지 않았다면? → 캐시된 데이터를 즉시 반환 (빠른 응답)

    ✔️ staleTime이 지나지 않았다면? → 추가 요청 없이 기존 데이터를 유지

    ✔️ staleTime이 지났다면? → 기존 데이터를 유지하면서 백그라운드에서 최신 데이터를 요청

    이제 SWR 패턴이 어디에서 왔는지, 그리고 프런트엔드 생태계에서 어떻게 쓰이고 있는지 살펴보겠습니다.


    5. SWR 패턴의 기원: HTTP 캐시 전략

    SWR(stale-while-revalidate) 전략은 원래 HTTP Cache-Control 헤더에서 유래한 개념입니다.

    이 개념은 RFC 5861(HTTP Cache-Control 확장)에서 처음 등장했으며, 이를 기반으로 프런트엔드 생태계에서도 SWR 패턴이 발전하게 되었습니다.

    5.1 RFC란?

    RFC(Request for Comments, 의견 요청 문서) 는 인터넷 프로토콜 및 표준을 문서화한 공식 문서입니다.

    IETF(Internet Engineering Task Force, 인터넷 엔지니어링 태스크 포스)에서 관리하며,

    인터넷 기술 및 표준이 되는 프로토콜(TCP/IP, HTTP 등)에 대한 세부 명세를 정의합니다.

    → 즉, RFC는 인터넷 기술과 프로토콜의 공식적인 가이드라인 문서라고 볼 수 있습니다.

    문서는 IETF의 공식 웹사이트인 Datatracker에서 관리되며, 일반적으로 인터넷 및 웹 기술의 발전 과정에서 표준으로 자리 잡는 경우가 많습니다.

    5.2 RFC 5861 명세와 핵심 내용

    RFC 5861 – HTTP Cache-Control Extensions for Stale Content

    발행일: 2010년 5월

    저자: Mark Nottingham (HTTP WG & W3C 기여자)

    문서: RFC 5861

    이 문서는 HTTP 캐시 시스템을 개선하기 위해 stale-while-revalidate(SWR) / stale-if-error 라는 Cache-Control 확장 디렉티브를 정의합니다.

    1. stale-while-revalidate(SWR) directive

    → 캐시된 데이터가 stale(오래됨) 상태라도 즉시 반환하고, 백그라운드에서 새로운 데이터를 요청하는 방식

    HTTP 응답 헤더에 확장된 옵션

    javascript
    Cache-Control: max-age=60, stale-while-revalidate=30

    이 설정의 의미

  • max-age=60 → 60초 동안 fresh 상태 유지
  • stale-while-revalidate=30 → 60초가 지나도 30초 동안 기존 캐시 데이터를 즉시 제공하며, 백그라운드에서 새 데이터를 요청
  • 동작 방식

  • 클라이언트가 요청을 보냈을 때, 캐시가 fresh 상태라면 즉시 반환
  • max-age=60이 지나면 stale 상태가 되지만,
  • stale-while-revalidate=30 동안에는 기존 데이터를 반환하면서도 백그라운드에서 새로운 요청을 보냄
  • 새 데이터가 도착하면 캐시를 갱신
  • 즉, 사용자는 로딩 없이 데이터를 받아볼 수 있고, 최신 데이터가 준비되면 업데이트됨


    2. stale-if-error directive

    캐시가 만료되었더라도, 네트워크 요청이 실패하면 기존 캐시를 반환하는 방식

    사용 예시 (HTTP 응답 헤더)

    javascript
    Cache-Control: max-age=60, stale-if-error=120

    이 설정의 의미

  • max-age=60 → 60초 동안 fresh 상태 유지
  • stale-if-error=120 → 60초 이후에도 네트워크 요청이 실패하면 기존 데이터를 최대 120초 동안 계속 제공
  • 동작 방식

  • 캐시된 데이터의 max-age=60이 지나면 stale 상태
  • 만약 이 상태에서 새로운 요청이 실패하면, 기존 stale 데이터를 그대로 반환
  • stale-if-error=120이 끝나기 전까지는 네트워크 오류 시 캐시 데이터를 유지
  • → 즉, 서버 오류나 네트워크 장애가 발생해도, 사용자는 기존 데이터를 유지하면서 서비스 이용 가능!

    TanStack Query - useQuery의 retry 옵션처럼, 요청이 실패했을 때 캐시 데이터를 사용하는 개념과 유사합니다.

    5.3 RFC 5861명세가 프런트엔드 생태계에서 어떻게 활용되고 있을까?

    RFC 5861에서 정의한 stale-while-revalidate(SWR) 개념은, 오늘날 SWR 패턴 (stale 데이터를 즉시 제공하면서도 백그라운드에서 최신 데이터 요청) 의 기반이 되었습니다.

    즉, 우리가 사용하는 SWR 데이터 패칭 전략은 RFC 5861의 HTTP 캐시 확장 디렉티브에서 유래한 것임을 알 수 있습니다.

    결론


    이번 글에서는 TanStack Query - useQuery의 staleTime옵션을 직접 구현하며, 이를 이해하는 핵심 개념인 SWR(stale-while-revalidate) 패턴까지 살펴봤습니다.

    프론트엔드에서 데이터를 패칭할 때 가장 중요한 고민은 최신 데이터를 유지하면서도 불필요한 API 요청을 최소화하는 것입니다.

    이 글을 통해 staleTime을 직접 구현하면서, SWR 전략의 개념과 유래, 그리고 프론트엔드 생태계에서 어떻게 활용되고 있는지 깊이 이해할 수 있었습니다.

    특히, 웹 전반에서 사용되는 HTTP 캐싱 전략이 TanStack Query, SWR 등 프론트엔드 라이브러리에서도 효과적으로 활용되고 있다는 점이 인상적이었습니다.

    하지만, 서비스마다 요구사항과 데이터 특성이 다르므로 적절한 캐싱 전략을 선택하는 것이 중요하다는 점을 기억해주세요!

    이번 글을 작성하면서 이러한 개념들이 단순한 이론이 아니라 더 나은 UX를 제공하기 위해 선배 개발자들이 오랜 시간 고민하고 발전시켜 온 결과물이라는 점을 다시 한번 느낄 수 있었습니다.

    성능 최적화와 사용자 경험 향상을 위한 노력들이 쌓여, 지금의 훌륭한 라이브러리들이 만들어졌고, 앞으로도 계속 발전해 나가겠죠.

    결국, 더 좋은 성능과 UX를 고민하는 과정이 모여 더 나은 서비스를 만든다는 사실을 다시금 되새기며,

    저 역시 이런 고민을 이어가야겠다는 다짐을 해봅니다. 😊