React Native에서의 네이티브 HTTP 캐싱 레이어

eeennsu·2026년 7월 6일

React Native

목록 보기
62/88

개요

React Native 앱에는 JS 캐시(TanStack Query) 외에 OS 레벨 HTTP 캐시가 추가로 존재한다. 이 글은 그 레이어가 왜 생기는지, 어떤 문제를 일으키는지, 그리고 실무에서 보통 어떻게 대응하는지를 정리한다.



1. 웹 vs 앱: HTTP 캐시 레이어가 더 깊은 이유

여기서 말하는 HTTP 캐시 계층이란 클라이언트(브라우저/네이티브) → 프록시/CDN → 서버 순으로 이어지는 다단 캐시 구조를 말한다. 웹 브라우저는 이 캐시를 단일 레이어로 통합 관리한다. 브라우저가 표준 캐시 헤더(Cache-Control, ETag, Expires)를 해석하고 DevTools에서 직접 제어할 수 있다.

React Native 앱은 구조가 다르다. JS의 fetch/XHR이 네이티브 네트워크 스택을 경유하기 때문에 OS 레벨 HTTP 캐시가 별도로 끼어든다. axios도 RN 환경에선 XMLHttpRequest 폴리필 위에서 돌기 때문에 결국 같은 네이티브 스택을 거친다.

레이어RN 앱
애플리케이션 캐시-TanStack Query (JS 메모리)
네이티브 HTTP 캐시브라우저 통합OkHttp Cache (Android) / NSURLCache (iOS)
프록시 / CDNOO
서버OO

RN 앱의 전체 캐시 흐름:

TanStack Query (JS) ↔ 네이티브 HTTP 캐시 (OkHttp / NSURLCache) ↔ 프록시/CDN ↔ 서버
  • Android : React Native가 네트워크를 OkHttp(안드로이드에서 RN이 사용하는 HTTP 클라이언트)로 처리한다. RN 기본 설정에서 약 10MB 디스크 캐시가 구성된다.
  • iOS : URLSession이 사용하는 OS 레벨 HTTP 캐시인 NSURLCache가 응답을 메모리·디스크에 캐시한다.


2. 네이티브 HTTP 캐시의 양면

유리한 경우

  • 오프라인 / 약전파 환경에서 이전 응답을 재사용해 회복력을 높인다.
  • 동일 리소스에 대한 중복 네트워크 요청을 줄인다.
  • 변경 빈도가 낮은 마스터 데이터(카테고리, 배너 등)를 빠르게 재표시한다.

불리한 경우

  • 서버/관리자에서 데이터를 바꿨는데 네이티브가 캐싱된 이전 응답을 반환한다. 사용자는 옛 데이터를 계속 보게 된다.
  • JS 레벨에서는 이 캐시 동작이 보이지 않아 디버깅이 어렵다. TanStack Query가 서버에 요청을 보냈다고 생각해도 실제로는 네이티브 캐시 응답이 반환된다.
  • stale 응답이 TanStack Query 캐시로 들어오면 invalidateQueries로 무효화해도 다시 같은 stale 응답을 받는다.

예시 시나리오: 관리자가 주요 데이터 정보를 수정했다. 앱은 TanStack Query의 staleTime이 지나 리페치를 시도하지만, OkHttp 또는 NSURLCache가 이전 응답을 그대로 반환한다. 사용자는 수정 전 데이터를 계속 본다.



3. iOS NSURLCache 함정

iOS NSURLCache는 cachePolicy(NSURLRequest의 캐시 사용 정책) 기반으로 동작한다. request 헤더에 Cache-Control: no-cache를 보내도 NSURLCache가 이를 무시하고 디스크 캐시 응답을 반환할 수 있다.

이유는 캐시 사용 여부를 결정하는 것이 HTTP 헤더가 아니라 request의 cachePolicy 속성이기 때문이다. 기본 정책인 useProtocolCachePolicy서버 응답 헤더를 기준으로 캐시 여부를 결정하며, 클라이언트가 보낸 request 헤더를 그대로 따르지는 않는다.

따라서 iOS에서는 Cache-Control: no-cache 헤더만으로는 stale 응답을 막을 수 없다. (Cache-Control: no-cache는 "캐시에 저장은 하되, 사용 전 원본 서버에 재검증을 요청"하라는 의미의 헤더다.)



4. 해결 방법 비교

방법설명장점단점
서버 응답 헤더 제어Cache-Control: no-store, ETag + If-None-Match 조건부 요청표준 정석, 프록시/CDN까지 제어서버 주도 — 클라이언트 단독으론 적용 불가
클라이언트 request Cache-Control: no-cache프록시/CDN에 재검증 요청구현 간단iOS NSURLCache 무시 가능 — 단독으론 불충분
캐시 버스팅 (_ts 쿼리 파라미터)매 요청마다 URL(캐시 키)을 바꿔 캐시 미스 강제iOS 우회에 효과적CDN/프록시 캐시 효율 저하
네이티브 캐시 자체 비활성화Android: OkHttp cache(null), iOS: urlCache/requestCachePolicy 조정근본적 차단오프라인 회복력 소실
애플리케이션 레이어 캐싱 위임TanStack Query staleTime/gcTime으로 제어JS 레벨에서 세밀한 제어 가능네이티브 캐시가 살아있으면 TanStack Query 무효화가 무력화됨

표에 나온 헤더·기법을 짚어두면 다음과 같다.

  • Cache-Control: no-store — 캐시에 저장 자체를 하지 않는다. (no-cache가 "저장하되 재검증"이라면, no-store는 "저장 금지"다.)
  • ETag / If-None-Match — 리소스 지문 기반 조건부 요청이다. 서버가 응답에 ETag(리소스 버전 식별자)를 실어 보내면, 다음 요청 때 클라이언트가 If-None-Match로 그 값을 되돌려 보내고, 변경이 없으면 서버는 본문 없이 304 Not Modified만 반환한다.
  • 캐시 버스팅(cache busting) — URL이나 쿼리 파라미터를 변형해 캐시 키를 바꿈으로써 강제로 새 응답을 받는 기법이다.


5. 실무에서 권장되는 패턴

원칙: 네이티브 HTTP 로컬 캐시는 끈다. 캐싱은 TanStack Query(클라이언트) + 서버가 담당한다.

Android — OkHttp 디스크 캐시 자체 제거

MainApplication.onCreate에서 loadReactNative(또는 사용 중인 RN 버전의 엔트리포인트) 호출 이전에 OkHttpClientFactory를 등록해 디스크 캐시를 제거하는 방식이 흔히 쓰인다. OkHttp 클라이언트는 첫 요청 시점에 lazy하게 생성되므로, 엔트리포인트 호출 전에 팩토리를 갈아끼워 두면 안전하다.

override fun onCreate() {
  super.onCreate()
  // 네이티브 HTTP 로컬 캐시 비활성화 — 캐싱은 TanStack Query + 서버가 담당.
  // RN OkHttp 기본 10MB 디스크 캐시를 제거해 stale 응답 반환을 원천 차단한다. (loadReactNative 이전에 등록)
  OkHttpClientProvider.setOkHttpClientFactory(object : OkHttpClientFactory {
    override fun createNewNetworkModuleClient(): OkHttpClient =
        OkHttpClientProvider.createClientBuilder(applicationContext, 0)
  })
  loadReactNative(this)
}


iOS — GET 캐시 버스팅

NSURLCache는 Cache-Control: no-cache 헤더를 무시할 수 있으므로, GET 요청마다 _ts 쿼리 파라미터를 붙여 URL(캐시 키)을 매번 다르게 만든다. 캐시 키가 다르면 항상 캐시 미스가 발생한다. (더 근본적으로는 URLSessionConfigurationurlCache/requestCachePolicy를 직접 손대는 방법도 있다.)

양 플랫폼 공통 — GET 한정 request 인터셉터

axios 셋업 파일에서 request 인터셉터를 등록한다. GET 메서드에만 적용한다(POST/PUT/DELETE 쓰기 메서드는 캐시 대상이 아니다).

// GET 로컬 캐시 무력화 (캐싱은 TanStack Query + 서버 담당).
// - no-cache 헤더: 프록시/CDN 재검증 (양 플랫폼)
// - Android: OkHttp 캐시는 MainApplication에서 비활성화 → 여기선 불필요
// - iOS: NSURLCache가 no-cache 헤더를 무시하므로 _ts로 URL을 바꿔 캐시 미스 강제
function registerGetCacheControlInterceptor(): void {
  axios.interceptors.request.use(config => {
    if (config.method === 'get') {
      config.headers.set('Cache-Control', 'no-cache');

      if (Platform.OS === 'ios') {
        config.params = { ...config.params, _ts: Date.now() };
      }
    }
    return config;
  });
}


6. 최종 플로우 다이어그램

GET 요청 발생 시의 처리 흐름이다. POST/PUT/DELETE는 이 GET 전용 처리를 거치지 않는다.

┌─────────────────────────────────────────────────────────────┐
│                    GET 요청 발생                             │
└─────────────────────────────────────────────────────────────┘
                            │
                            ▼
┌─────────────────────────────────────────────────────────────┐
│           axios request 인터셉터 (공통)                      │
│  • Cache-Control: no-cache 헤더 추가 (프록시/CDN 재검증용)  │
│  • iOS 한정: _ts=Date.now() 쿼리 파라미터 추가              │
└─────────────────────────────────────────────────────────────┘
                            │
               ┌────────────┴────────────┐
               ▼                         ▼
  ┌────────────────────┐     ┌────────────────────────┐
  │      Android       │     │          iOS           │
  │                    │     │                        │
  │  OkHttp            │     │  NSURLCache            │
  │  (캐시 없음)        │     │  URL이 매번 달라        │
  │  → 항상 네트워크   │     │  → 항상 캐시 미스       │
  │    로 나감          │     │  → 네트워크로 나감      │
  └────────┬───────────┘     └──────────┬─────────────┘
           └────────────┬───────────────┘
                        ▼
┌─────────────────────────────────────────────────────────────┐
│                   프록시 / CDN (존재 시)                     │
│         no-cache 헤더 → 원본 서버에 재검증 요청             │
└─────────────────────────────────────────────────────────────┘
                            │
                            ▼
┌─────────────────────────────────────────────────────────────┐
│                       서버 응답 수신                         │
└─────────────────────────────────────────────────────────────┘
                            │
                            ▼
┌─────────────────────────────────────────────────────────────┐
│              TanStack Query JS 메모리 캐시                   │
│     staleTime / gcTime으로 캐시 유효성 관리 → UI 렌더        │
└─────────────────────────────────────────────────────────────┘

─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
  POST / PUT / DELETE: 위 인터셉터 처리 없이 직접 네트워크로
─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
profile
이력서 https://resume.eunsu.pro

0개의 댓글