클라이언트에서 데이터를 가져올 때 매번 새 요청을 보내는 것은 불필요한 네트워크 트래픽을 증가시키고, 사용자 경험(UX)을 저하시킬 수 있습니다. 이를 개선하기 위해, 우리는 이전 글에서 useQuery를 만들면서 캐시를 활용해 불필요한 요청을 줄이는 방법을 살펴봤습니다.
하지만, 단순히 cacheTime만을 적용하는 것만으로는 부족한 점이 있습니다. 캐시가 만료되기 전까지는 데이터를 계속 사용할 수 있지만, 캐시가 만료되는 순간 새로운 데이터를 가져올 때까지 로딩 상태가 발생하기 때문이죠.
이 문제를 해결하기 위해, 이번 글에서는 staleTime을 추가하여 데이터를 더 효율적으로 관리하는 방법을 살펴보겠습니다. 또한, 이를 제대로 이해하기 위해 staleTime이 활용하는 SWR(stale-while-revalidate) 패턴이 무엇인지도 함께 알아보겠습니다.
✔️ useQuery 훅을 직접 구현하며 staleTime을 추가하는 과정
✔️ cacheTime과 staleTime의 차이점 및 역할
✔️ SWR 패턴을 활용해 최신 데이터를 유지하면서도 빠른 응답을 제공하는 방법
이제 staleTime이 어떻게 동작하는지 하나씩 살펴보겠습니다.
cacheTime만 쓰면 어떤 점이 아쉬운가요?cacheTime 은 어떻게 구현되었더라
const isExpired = cachedData ? now - cachedData.timestamp > cacheTime : true;→ 캐시된 데이터는 cacheTime이 지나기 전까지 유지되며, 시간이 초과되면 삭제됩니다. 이후 새로운 요청이 발생하면 다시 데이터를 가져오죠.
cacheTime만 쓰면 아쉬운 점1. 최신 데이터가 필요한데 갱신되지 않을 수 있음
cacheTime이 만료되기 전까지는 이전 데이터를 계속 사용하므로
→ 사용자가 최신 데이터를 확인할 방법이 없음.cacheTime을 길게 설정하면? → 최신성이 떨어짐 😕2. cacheTime이 지나면 데이터가 삭제되므로, 새 요청이 오기까지 로딩이 발생함
cacheTime이 초과되면 캐시된 데이터를 즉시 삭제함.staleTime 추가 staleTime을 도입하면?
✔️ 이전 데이터를 유지하면서도, 백그라운드에서 최신 데이터를 요청 가능
✔️ staleTime이 지나기 전까지는 기존 데이터를 그대로 표시 → 로딩 없이 즉시 응답
✔️ staleTime이 지나면, 데이터를 stale 상태로 간주하고 백그라운드에서 새 요청을 보내 최신 데이터로 갱신
staleTime 이해하기staleTime이란?staleTime은 데이터가 "신선한(fresh)" 상태로 유지되는 시간을 의미합니다.staleTime이 지나면 데이터가 "stale(오래된)" 상태로 간주되며
→ 화면에는 기존 데이터를 유지하되, 백그라운드에서 새로운 데이터를 요청staleTime vs cacheTime 무슨 차이점이 있나요?staleTime을 적용하면 어떻게 달라질까?✔️ 캐시된 데이터를 즉시 제공하여 빠른 응답 가능
✔️ staleTime 이후에는 백그라운드에서 새 데이터를 가져와 UI 업데이트
✔️ 최신 데이터 요청 시에도 로딩 상태가 발생하지 않음
cacheTime와 staleTime이 동작하는 흐름 살펴보기
useQuery는 먼저 캐시에서 확인cacheTime이 지나지 않았으면 기존 데이터를 그대로 반환staleTime이 지나면 백그라운드에서 새 데이터를 요청하여 캐시 갱신cacheTime이 지나면 캐시 삭제 후, API에서 새 데이터를 받아옴staleTime 옵션 직접 추가하기staleTimestamp 추가이제 데이터가 stale한지 판단하기 위한 staleTimestamp를 추가합니다.
const queryCache = new Map<string, CacheData<unknown>>();→ 기존 queryCache에 staleTimestamp 값을 추가하여 관리
staleTime이 지나면 데이터가 stale 상태로 변경
const isStale = cachedData ? now - cachedData.staleTimestamp > staleTime : true;→ staleTime이 지나면 isStale을 true로 설정
if (cache && cachedData && !isExpired && isStale) {
setData(cachedData.data); // 기존 데이터 유지
setIsPending(false);
}→ 오래된 데이터(stale)라도 화면에는 표시하지만, 새 데이터를 백그라운드에서 요청
if (isStale || !cachedData) {
if (!cachedData) setIsPending(true);
executeQuery(); // 새 데이터 요청
}→ staleTime이 지나면 새 요청을 백그라운드에서 실행하여 최신 데이터로 업데이트
실제 코드가 궁금하다면 ⇒ GitHub가서 코드 보기
화면으로 직접 보고싶다면 ⇒ 이동하기 *console창을 켜고 직접 확인해보세요!
stale-while-revalidate 패턴과의 연관성지금까지 살펴본 staleTime을 적용한 데이터 패칭 흐름을 보면, 어디선가 본 듯한 익숙한 패턴이 떠오르지 않나요? 바로 SWR(stale-while-revalidate) 패턴입니다.

웹 애플리케이션에서 데이터를 가져올 때, 항상 새로운 데이터를 요청하면 불필요한 네트워크 트래픽이 발생하고,
반대로 오래된 캐시 데이터를 그대로 사용하면 최신성이 보장되지 않는 문제가 생깁니다.
이를 해결하기 위한 전략이 바로 SWR(stale-while-revalidate) 패턴입니다.
SWR 패턴의 핵심 원리
즉, 사용자는 즉각적인 응답을 받을 수 있고, 백그라운드에서 새로운 데이터를 가져오면서 최신 상태를 유지할 수 있습니다.
우리가 만든 useQuery 훅의 동작 방식도 SWR 패턴과 거의 동일합니다.
SWR 개념을 코드로 표현하면 다음과 같습니다.
if (cache && cachedData && !isExpired) {
setData(cachedData.data); // Stale 데이터 즉시 반환
if (isStale) executeQuery(); // 백그라운드에서 최신 데이터 요청 (Revalidate 진행)
}✔️ cacheTime이 지나지 않았다면? → 캐시된 데이터를 즉시 반환 (빠른 응답)
✔️ staleTime이 지나지 않았다면? → 추가 요청 없이 기존 데이터를 유지
✔️ staleTime이 지났다면? → 기존 데이터를 유지하면서 백그라운드에서 최신 데이터를 요청
이제 SWR 패턴이 어디에서 왔는지, 그리고 프런트엔드 생태계에서 어떻게 쓰이고 있는지 살펴보겠습니다.
SWR(stale-while-revalidate) 전략은 원래 HTTP Cache-Control 헤더에서 유래한 개념입니다.
이 개념은 RFC 5861(HTTP Cache-Control 확장)에서 처음 등장했으며, 이를 기반으로 프런트엔드 생태계에서도 SWR 패턴이 발전하게 되었습니다.
RFC(Request for Comments, 의견 요청 문서) 는 인터넷 프로토콜 및 표준을 문서화한 공식 문서입니다.
IETF(Internet Engineering Task Force, 인터넷 엔지니어링 태스크 포스)에서 관리하며,
인터넷 기술 및 표준이 되는 프로토콜(TCP/IP, HTTP 등)에 대한 세부 명세를 정의합니다.
→ 즉, RFC는 인터넷 기술과 프로토콜의 공식적인 가이드라인 문서라고 볼 수 있습니다.
문서는 IETF의 공식 웹사이트인 Datatracker에서 관리되며, 일반적으로 인터넷 및 웹 기술의 발전 과정에서 표준으로 자리 잡는 경우가 많습니다.
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 확장 디렉티브를 정의합니다.
→ 캐시된 데이터가 stale(오래됨) 상태라도 즉시 반환하고, 백그라운드에서 새로운 데이터를 요청하는 방식
HTTP 응답 헤더에 확장된 옵션
Cache-Control: max-age=60, stale-while-revalidate=30이 설정의 의미
max-age=60 → 60초 동안 fresh 상태 유지stale-while-revalidate=30 → 60초가 지나도 30초 동안 기존 캐시 데이터를 즉시 제공하며, 백그라운드에서 새 데이터를 요청동작 방식
max-age=60이 지나면 stale 상태가 되지만,stale-while-revalidate=30 동안에는 기존 데이터를 반환하면서도 백그라운드에서 새로운 요청을 보냄즉, 사용자는 로딩 없이 데이터를 받아볼 수 있고, 최신 데이터가 준비되면 업데이트됨
캐시가 만료되었더라도, 네트워크 요청이 실패하면 기존 캐시를 반환하는 방식
사용 예시 (HTTP 응답 헤더)
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-if-error=120이 끝나기 전까지는 네트워크 오류 시 캐시 데이터를 유지→ 즉, 서버 오류나 네트워크 장애가 발생해도, 사용자는 기존 데이터를 유지하면서 서비스 이용 가능!
TanStack Query - useQuery의 retry 옵션처럼, 요청이 실패했을 때 캐시 데이터를 사용하는 개념과 유사합니다.
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를 고민하는 과정이 모여 더 나은 서비스를 만든다는 사실을 다시금 되새기며,
저 역시 이런 고민을 이어가야겠다는 다짐을 해봅니다. 😊