프론트 웹캐시 가이드 (Part 1) - HTTP 캐싱

개발.log·2025년 12월 4일
post-thumbnail

HTTP 캐싱 가이드 (Part 1): 웹 성능의 핵심

"왜 수정한 게 반영이 안 되지?" — 모든 웹 개발자가 한 번쯤 겪는 악몽이다.
이 글에서는 HTTP 캐시의 원리를 근본부터 파헤쳐, 캐시를 제대로 이해하고 활용하는 방법을 알아본다.


1. 캐시란 무엇인가?

캐시는 "한번 가져온 것을 저장해두고 다시 쓰는 것"이다.

편의점에 물을 사러 간다고 생각해보자.

캐시 없이:
집 → 편의점 → 집 (물 1병)
집 → 편의점 → 집 (물 1병)
집 → 편의점 → 집 (물 1병)
매번 왕복 10분...

캐시 사용:
집 → 편의점 → 집 (물 10병 사서 냉장고에 저장)
냉장고에서 꺼냄 (10초)
냉장고에서 꺼냄 (10초)

웹에서도 똑같다.

캐시 없이:
브라우저 → 서버 요청 → 응답 (500ms)
브라우저 → 서버 요청 → 응답 (500ms)

캐시 사용:
브라우저 → 서버 요청 → 응답 → 저장 (500ms)
로컬 캐시에서 읽음 (5ms)  ← 100배 빠름!🔥

1-1. 웹에서 캐시의 대상

HTTP에서 리소스(Resource)란 브라우저가 요청할 수 있는 모든 파일이다.

리소스 종류예시캐시 중요도
HTMLindex.html, about.html높음
CSSstyle.css, main.css높음
JavaScriptapp.js, bundle.js높음
이미지logo.png, hero.jpg높음
폰트Pretendard.woff2중간
API 응답/api/users, /api/products중간

1-2. 캐시가 왜 중요한가?

측면캐시 없음캐시 있음
속도매번 네트워크 왕복즉시 로드
서버 부하모든 요청 처리대부분 요청 안 옴
트래픽 비용매번 데이터 전송거의 전송 없음
사용자 경험느리고 답답함빠르고 쾌적함

실제 수치로 보면 이렇다.

  • 캐시 미스: 59KB 다운로드, 500ms 소요
  • 캐시 히트: 0KB 다운로드, 5ms 소요

2. 브라우저는 언제 서버에 요청을 보내는가?

2-1. 하나의 페이지 로드 = 수십 개의 요청

https://example.com에 접속하면, 브라우저는 HTML 하나만 요청하는 게 아니다.

1. 사용자가 URL 입력 후 엔터 (HTML 요청)
   └─ GET /index.html

2. HTML 파싱 중 발견된 리소스들 (자동 요청)
   └─ <link href="style.css">      → GET /style.css
   └─ <script src="app.js">        → GET /app.js
   └─ <img src="logo.png">         → GET /logo.png

3. JavaScript 실행 중 동적 요청
   └─ fetch('/api/users')          → GET /api/users

4. 브라우저 자동 요청
   └─ favicon.ico (자동)

Chrome DevTools Network 탭에서 확인하면 이런 식이다.

Name              Status    Type        Size      Time
───────────────────────────────────────────────────────
localhost         200       document    15.2 KB   45ms    ← HTML
style.a1b2c3.css  200       stylesheet  8.5 KB    23ms    ← CSS
app.d4e5f6.js     200       script      125 KB    89ms    ← JS
logo.png          200       png         12.3 KB   34ms    ← 이미지
api/users         200       fetch       2.1 KB    156ms   ← API

중요한 점: 각 요청은 독립적으로 캐시된다. 같은 페이지의 리소스라도 각각 다른 캐시 정책을 가질 수 있다.


3. 캐시는 어디에 저장되는가?

3-1. 캐시 저장소의 종류

사용자 컴퓨터                    인터넷                     서버
──────────────                  ──────                    ──────

┌─────────────┐            ┌─────────────┐          ┌─────────────┐
│   Memory    │            │             │          │             │
│   Cache     │            │     CDN     │          │    원본      │
│  (브라우저)   │◄──────────►│    Cache    │◄────────►│    서버      │
├─────────────┤            │             │          │             │
│    Disk     │            │             │          │             │
│   Cache     │            │             │          │             │
│  (브라우저)   │            │             │          │             │
└─────────────┘            └─────────────┘          └─────────────┘

   Private Cache             Shared Cache                Origin
   (이 사용자만)              (모든 사용자 공유)           (원본 데이터)

3-2. 브라우저 캐시: Memory vs Disk

Memory CacheDisk Cache
저장 위치RAMSSD/HDD
속도가장 빠름 (~0ms)빠름 (~5ms)
수명탭 닫으면 사라짐수 주 ~ 수 개월
대상현재 페이지 리소스큰 파일, 자주 방문
DevTools(memory cache)(disk cache)

3-3. CDN 캐시

CDN(Content Delivery Network)은 전세계에 분산된 서버 네트워크다.

  • 여러 사용자가 공유 (Shared Cache)
  • 원본 서버 부하 감소
  • 사용자와 가까운 서버에서 응답
  • 개인정보 저장 금지 (다른 사용자에게 노출 위험)

3-4. 핵심: CDN 무효화 ≠ 브라우저 캐시 삭제

상황: 버그 수정 후 배포

1. CDN Invalidation 실행
   → CDN 캐시 삭제됨

2. 하지만 사용자 A의 브라우저에는?
   → 여전히 이전 버전이 Disk Cache에 있음
   → max-age 만료 전까지 서버에 요청조차 안 보냄
   → 사용자 A는 계속 버그 버전을 봄 😱

결론: 브라우저 캐시는 서버에서 제어할 수 없다. 처음부터 신중하게 설정해야 한다.


4. 브라우저는 무엇을 보고 캐시를 결정하는가?

브라우저는 HTTP 응답의 Cache-Control 헤더를 보고 캐시 여부를 결정한다.

HTTP/1.1 200 OK
Content-Type: application/javascript
Cache-Control: public, max-age=31536000
ETag: "abc123"

4-1. 브라우저의 캐시 저장 판단 흐름

HTTP 응답 수신
      │
      ▼
┌─────────────────────────────────┐
│ Cache-Control: no-store 있음?    │
└─────────────┬───────────────────┘
              │
     Yes      │      No
      │       │       │
      ▼       │       ▼
  저장 안 함  │  Cache-Control 또는 Expires 있음?
              │       │
              │  Yes  │      No
              │   │   │       │
              │   ▼   │       ▼
              │ 해당 규칙대로 │  브라우저 휴리스틱
              │ 캐시 저장    │  (기본 캐시 적용)
              │       │       │
              │       ▼       │
              │  private 있음? │
              │       │       │
              │  Yes  │  No   │
              │   │   │   │   │
              │   ▼   │   ▼   │
              │ 브라우저만 │ 브라우저 + CDN
              │ 저장      │ 모두 저장 가능

5. Cache-Control 헤더 완벽 정리

5-1. max-age: 유효 기간

Cache-Control: max-age=3600

의미: "이 리소스는 3600초(1시간) 동안 신선하다"

시간용도
max-age=00초매번 재검증
max-age=601분빈번히 바뀌는 데이터
max-age=36001시간적당히 바뀌는 데이터
max-age=864001일하루 단위 갱신
max-age=315360001년영구 캐시

immutable: immutable을 추가하면 브라우저가 max-age 기간 동안 재검증 시도조차 하지 않는다. 파일명에 해시가 포함된 정적 파일(app.a1b2c3.js)에 사용하면, 사용자가 새로고침해도 불필요한 304 요청을 방지할 수 있다.

5-2. no-cache vs no-store

이 두 가지는 이름만 비슷하고 완전히 다르다.

no-cache: "캐시해도 되지만, 사용 전에 반드시 확인해"

Cache-Control: no-cache

동작 과정:

1단계: 저장
─────────────────────────────────────────────────────────
• 브라우저 Disk Cache에 저장함
• CDN 캐시에도 저장 가능 (public인 경우)

2단계: 다음에 이 리소스가 필요할 때
─────────────────────────────────────────────────────────

"다음에 필요할 때"란?
• 페이지 새로고침
• 다른 페이지에서 같은 리소스 요청
• fetch()로 같은 URL 호출
• 뒤로가기/앞으로가기

   브라우저 → 원본 서버
       "이거 바뀌었어?"
       (If-None-Match: "abc123")
              │
              ▼
   ┌─────────────────────────────────────┐
   │ 서버가 ETag 또는 Last-Modified 비교     │
   └─────────────────────────────────────┘
             │
     안 바뀜   │  바뀜
       │     │    │
       ▼     │    ▼
 304 Not     │  200 OK
 Modified    │  + 새 파일
 (본문 없음)   │
       │     │    │
       ▼     │    ▼
 캐시된 버전 │  새 버전으로
 그대로 사용 │  캐시 갱신

304 응답의 효율성: 59KB 파일을 324바이트로 검증 → 99.5% 절약

no-store: "절대 저장하지 마"

Cache-Control: no-store
  • 브라우저 Memory Cache에도 저장 안 함
  • 브라우저 Disk Cache에도 저장 안 함
  • CDN/Edge Cache에도 저장 안 함
  • 매번 원본 서버에서 전체 응답
  • 가장 느리지만 가장 안전

사용 케이스: 은행 계좌 잔액, 의료 기록, 결제 정보

비교 정리

no-cacheno-store
캐시 저장저장함 ✅저장 안 함 ❌
재검증매번 필요 ✅해당 없음 ❌
304 응답가능 ✅불가능 ❌
보안 수준중간최고

5-3. public vs private

Cache-Control: public, max-age=3600   # CDN도 저장 가능
Cache-Control: private, max-age=3600  # 브라우저에만 저장

public: 모든 캐시(브라우저, CDN, 프록시)에 저장 가능
private: 브라우저 캐시에만 저장, CDN/프록시는 저장 금지

5-4. s-maxage: CDN 전용 설정

Cache-Control: s-maxage=31536000, max-age=0
┌─────────┐         ┌─────────┐         ┌─────────┐
│ 브라우저  │         │   CDN   │         │  원본    │
│         │         │         │         │  서버    │
└────┬────┘         └────┬────┘         └────┬────┘
     │                   │                   │
     │  max-age=0        │  s-maxage=1년     │
     │  (매번 확인)        │  (1년 캐시)         │
     │                   │                   │
     ▼                   ▼                   ▼
저장하되 매번        1년 동안 저장         원본 데이터
CDN에 재검증         배포 시 무효화

결과:

  • 사용자는 매번 CDN에 "최신이야?" 확인
  • CDN은 캐시된 버전 즉시 응답 (원본 서버 안 감)
  • 배포 시 CDN Invalidation → CDN이 원본에서 새로 받음

왜 브라우저는 캐시 안 하고 CDN만 캐시할까?

핵심: 서버에서 제어 가능한가?

브라우저 캐시:
├─ 서버에서 제어 불가능
├─ 사용자 컴퓨터에 저장됨
├─ 문제 시: "고객님, 브라우저 캐시 지워주세요..."

CDN 캐시:
├─ 서버에서 제어 가능
├─ Invalidation으로 즉시 삭제
├─ 문제 시: aws cloudfront create-invalidation 실행

이 전략은 프로덕션 배포에서 가장 많이 쓰이는 패턴이다.

5-5. stale-while-revalidate

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

의미:

  • 60초 동안은 신선한 캐시 사용
  • 60~360초(60+300) 사이에는 캐시를 반환하면서 백그라운드에서 갱신
  • 360초 이후에는 반드시 새로 요청

6. private과 Authorization 헤더

6-1. "Authorization 있으면 CDN이 알아서 캐시 안 하는 거 아니야?"

맞다, 절반은. HTTP 스펙상 Authorization 헤더가 있는 요청의 응답은 기본적으로 CDN에 캐시되지 않는다. 하지만 서버가 명시적으로 public이나 s-maxage를 설정하면 CDN이 캐시한다.

Authorization 헤더와 캐시의 관계
─────────────────────────────────────────────────────────

요청:
GET /api/my-profile
Authorization: Bearer eyJhbGciOiJIUzI1...

CDN 판단:
┌─────────────────────────────────────┐
│ Authorization 헤더 있음?              │
└──────────────┬──────────────────────┘
         Yes   │
          │    │
          ▼    │
┌─────────────────────────────────────┐
│ 응답에 public 또는 s-maxage 있음?       │
└──────────────┬──────────────────────┘
         Yes   │   No
          │    │    │
          ▼    │    ▼
    CDN 캐시   │   CDN 캐시 안 함 (기본 동작, 안전)
    (위험!)    │

즉, Authorization이 있어도 서버가 public을 명시하면
CDN이 캐시해버릴 수 있음!

6-2. 그럼 private은 왜 필요한가?

  1. 명시적 의도 표현: "이건 개인 데이터야"라고 확실히 선언
  2. 실수 방지: 누군가 나중에 s-maxage 추가해도 private이 우선
  3. Authorization 없는 요청: 쿠키 기반 인증은 Authorization 헤더가 없음

결론: 개인 데이터에는 항상 private을 명시하는 것이 안전하다.


7. 캐시 재검증 (Revalidation)

7-1. ETag와 Last-Modified

서버는 파일이 변경됐는지 판단하기 위해 두 가지 방법을 사용한다.

ETag (권장): 파일 내용의 해시값

# 첫 응답
ETag: "abc123def456"

# 재검증 요청
If-None-Match: "abc123def456"

Last-Modified: 파일 수정 시간

# 첫 응답
Last-Modified: Wed, 21 Oct 2024 07:28:00 GMT

# 재검증 요청
If-Modified-Since: Wed, 21 Oct 2024 07:28:00 GMT
ETagLast-Modified
정확도높음 (내용 기반)낮음 (시간 기반, 1초 단위)
권장우선 사용보조적 사용

7-2. 재검증 전체 흐름

재검증 상세 흐름
─────────────────────────────────────────────────────────

브라우저          CDN              원본 서버
   │              │                   │
   │  1. 재검증 요청 (캐시 만료됨)          │
   │─────────────►│                   │
   │              │                   │
   │              │  2. CDN도 재검증 필요?
   │              │──────────────────►│
   │              │                   │
   │              │                   │  3. ETag 비교
   │              │                   │
   │              │  4-A. 304 Not Modified (안 바뀜)
   │              │◄──────────────────│
   │              │                   │
   │              │  4-B. 200 OK + 새 파일 (바뀜)
   │              │◄──────────────────│
   │              │                   │
   │  5. 결과 전달 │                    │
   │◄─────────────│                   │

8. 실제 캐시 보안 사고 사례

8-1. ChatGPT 타인 대화 노출 사고 (2023년 3월)

2023년 3월, ChatGPT에서 다른 사용자의 채팅 기록이 보이는 심각한 버그가 발생했다.

문제 상황:
• 사용자들이 ChatGPT 사이드바에서 자신의 채팅 기록 대신
  다른 사람의 채팅 제목이 보이는 현상 발생
• 일부 사용자는 타인의 결제 정보 일부까지 노출됨 😱

원인 (OpenAI 공식 발표):
• Redis 캐시 라이브러리의 버그
• 특정 조건에서 캐시된 데이터가 잘못된 사용자에게 반환
• 개인 데이터에 대한 캐시 설정 문제

교훈:
• 개인 데이터는 반드시 private 또는 no-store 설정
• 캐시 키에 사용자 식별자 포함 필수

8-2. CDN 캐시 키 문제

문제 시나리오:

요청 A:
  GET /api/profile
  Authorization: Bearer token_user_A
  → 응답: {"name": "김철수", "balance": 1000000}
  → CDN: 캐시 키 = "GET /api/profile"로 저장

요청 B (5분 후):
  GET /api/profile
  Authorization: Bearer token_user_B  ← 다른 토큰!
  → CDN: "GET /api/profile" 캐시 있네? 반환!
  → B에게 A의 프로필이 반환됨!
  
CDN 입장에서는 Authorization 헤더를 안 보기 때문에
두 요청이 "같은 요청"으로 보임

해결 방법: 개인 데이터에는 항상 private 또는 no-store를 명시하자.


9. 파일명 해시 전략 (Cache Busting)

9-1. 문제: 캐시가 너무 잘 되는 문제

Day 1: 첫 배포
• style.css 배포
• Cache-Control: max-age=31536000 (1년)
• 사용자 브라우저에 캐시됨

Day 2: 버그 수정 후 재배포
• style.css 내용 수정 후 배포
• 서버에는 새 버전이 있음

하지만...
• 사용자 브라우저: "max-age 아직 안 지났네? 캐시 쓸게~"
• 서버에 요청조차 안 보냄
• 사용자는 364일 동안 버그 버전을 봄

9-2. 해결: 파일명에 해시 붙이기

원리:
• 파일 내용이 바뀌면 → 파일명도 바뀜
• 파일명이 바뀌면 → 브라우저 입장에서 "새로운 파일"
• 새로운 파일이므로 → 캐시 없이 새로 다운로드

Day 1:
파일명: style.a1b2c3d4.css
내용: .button { color: red; }
            └───── 이 내용의 해시 = a1b2c3d4

Day 2: 버그 수정
파일명: style.x7y8z9w0.css  ← 파일명 자체가 바뀜!
내용: .button { color: blue; }
            └───── 이 내용의 해시 = x7y8z9w0

9-3. 해시는 언제, 누가 붙이는가?

정답: npm run build 시점에 빌드 도구가 붙인다.

1. 소스 코드
   src/
   ├── styles/main.css
   └── scripts/app.js

2. npm run build 실행  ← 여기서 해시가 붙음! 🔥

3. 빌드 결과물
   dist/
   ├── index.html  (해시된 파일명을 참조)
   └── assets/
       ├── main.a1b2c3d4.css
       └── app.e5f6g7h8.js

4. 배포 (Docker, S3, Vercel 등)
   • 이미 해시가 붙은 파일들을 업로드

5. 사용자 요청
   • 해시가 포함된 URL로 요청
   • 예: GET /assets/style.a1b2c3d4.css

9-4. HTML은 해시를 붙이면 안 됨

HTML에 해시를 붙이면 안 되는 이유
─────────────────────────────────────────────────────────

HTML에 해시를 붙이면?
─────────────────────────────────────────────────────────

index.a1b2c3d4.html

문제 1: 사용자가 어떤 URL로 접속?
• https://example.com/ → index.html을 찾음
• https://example.com/index.a1b2c3d4.html ???

문제 2: 배포할 때마다 URL이 바뀜
• v1: https://example.com/index.a1b2c3d4.html
• v2: https://example.com/index.x7y8z9w0.html
• 북마크, 공유 링크가 다 깨짐!

문제 3: SEO 문제
• 검색엔진이 인덱싱한 URL이 무효화됨

올바른 전략:

HTML: 고정 URL + 캐시 안 함
├── index.html (URL 고정)
├── Cache-Control: no-cache
└── 항상 최신 버전 확인

JS/CSS: 해시 포함 + 영구 캐시
├── app.a1b2c3d4.js (해시 포함)
├── Cache-Control: max-age=31536000
└── 내용 바뀌면 URL도 바뀌므로 안전

흐름:
사용자 → index.html (매번 최신 확인)
            └─ <script src="app.a1b2c3d4.js">  ← 캐시 있으면 사용
            └─ <link href="style.e5f6g7h8.css"> ← 캐시 있으면 사용

배포 후:
사용자 → index.html (새 버전)
            └─ <script src="app.NEW_HASH.js">  ← 새 URL이므로 다운로드

10. 캐시와 URL의 관계: 리소스 캐싱의 핵심

캐시를 이해하는 데 가장 중요한 원칙 하나가 있다.

캐시는 URL을 키로 저장된다.

브라우저 캐시 저장소 (개념적):
┌──────────────────────────────────────┬─────────────────┐
│            URL (캐시 키)               │    저장된 파일     │
├──────────────────────────────────────┼─────────────────┤
│ /images/cat.jpg                      │ 고양이 사진        │
│ /images/cat.jpg?v=2                  │ 다른 고양이 사진    │  ← 다른 캐시!
│ /images/dog.jpg                      │ 강아지 사진        │
└──────────────────────────────────────┴─────────────────┘

10-1. 케이스별 동작 분석

이 부분이 많이 헷갈리니까 케이스별로 정리해보자.

상황URL 변경캐시 동작결과
백엔드 API에서 다른 이미지 URL 반환O캐시 미스새 이미지 다운로드
백엔드 API에서 같은 이미지 URL 반환X캐시 히트캐시된 이미지 사용
public 이미지 파일 교체 (같은 경로)X캐시 히트이전 이미지가 보임
public 이미지 삭제X캐시 히트삭제 전 이미지가 보임

케이스 1: 백엔드 API에서 다른 URL 반환

// Before: 백엔드 API 응답
{ "avatar": "/images/profile-old.jpg" }

// After: 백엔드 API 응답 변경
{ "avatar": "/images/profile-new.jpg" }

// 결과: URL이 다름 → 캐시 미스 → 새 이미지 다운로드
// 정상 동작한다!

케이스 2: 파일만 교체 (같은 URL)

// public/logo.png 파일을 다른 이미지로 교체
<img src="/logo.png" />

// 결과: URL이 같음 → 캐시 히트 → 이전 이미지가 보임
// max-age 만료 전까지 새 이미지가 안 보인다!

케이스 3: 파일 삭제

// public/banner.png 파일 삭제
<img src="/banner.png" />

// 결과:
// - 캐시에 있는 사용자: 삭제 전 이미지가 보임
// - 캐시에 없는 사용자: 404 에러
// 사용자마다 다른 결과가 나온다!

10-2. 왜 이렇게 동작하는가?

브라우저의 관점에서 생각해보면 명확하다.

브라우저가 <img src="/logo.png">를 만났을 때:
─────────────────────────────────────────────────────────

1. "내 캐시에 /logo.png 있어?" 확인
   └─ 있고, max-age 안 지남 → 캐시 사용! (서버에 안 물어봄)
   └─ 있고, max-age 지남 → 서버에 재검증 요청
   └─ 없음 → 서버에 요청

2. 서버에 요청하지 않았다면?
   └─ 서버의 파일이 바뀌든, 삭제되든 모름
   └─ 브라우저 입장에서는 "캐시에 있으니까 그거 쓸게~"

핵심: 브라우저는 URL만 보고 캐시를 찾는다.
     서버의 파일 상태는 요청을 보내야만 알 수 있다.

10-3. 해결 전략

전략 1: 파일명에 버전/해시 포함 (권장)

<img src="/logo-v2.png" />
<img src="/logo.a1b2c3.png" />

// 장점: 확실한 캐시 무효화
// 단점: 파일명 관리 필요

전략 2: 쿼리스트링 사용

// 버전 번호
<img src="/logo.png?v=2" />

// 타임스탬프 (매번 새로 받음)
<img src={`/logo.png?t=${Date.now()}`} />

// 장점: 파일명 안 바꿔도 됨
// 단점: 일부 CDN에서 무시될 수 있음

전략 3: Next.js Image 최적화 사용

import Image from 'next/image'

// Next.js가 자동으로 최적화 + 캐시 관리
<Image src="/logo.png" width={200} height={100} alt="Logo" />

// _next/image?url=... 형태로 변환됨

10-4. 실전 예시: 프로필 이미지 변경

// 나쁜 예: 파일명 고정
const avatarUrl = "/users/123/avatar.jpg"
// 문제: 사용자가 프로필 사진 바꿔도 캐시된 이전 사진이 보임

// 좋은 예 1: 업데이트 시간 포함
const avatarUrl = `/users/123/avatar.jpg?updated=${user.updatedAt}`

// 좋은 예 2: 해시 포함
const avatarUrl = `/users/123/avatar-${user.avatarHash}.jpg`

// 좋은 예 3: CDN URL에 버전 포함
const avatarUrl = `https://cdn.example.com/users/123/${user.avatarVersion}/avatar.jpg`

11. 실전 Cache-Control 전략 정리

11-1. 기본 전략표

리소스변경 빈도권장 전략Cache-Control
HTML배포마다해시 불가, 캐시 안 함no-cache
JS/CSS (빌드됨)배포마다파일명 해시public, max-age=31536000, immutable
로고, 아이콘드묾버전 쿼리스트링public, max-age=86400
사용자 프로필 사진자주URL에 timestamp 포함max-age=3600
폰트거의 없음영구 캐시public, max-age=31536000
API (공개)상황에 따라태그 기반 무효화public, max-age=60
API (개인)매 요청CDN 캐시 금지private, no-cache
민감 정보매 요청저장 자체 금지no-store

11-2. 고급 전략: 브라우저와 CDN 분리

HTML:     no-cache, no-store, must-revalidate
JS/CSS:   s-maxage=31536000, max-age=0

왜 이렇게 설정할까?

HTML에 no-cache, no-store, must-revalidate
───────────────────────────────────────────────────────────────

index.html 내용:
<script src="/static/js/main.a1b2c3.js"></script>
                              ──────
                              해시 포함!

새 버전 배포 후:
<script src="/static/js/main.x7y8z9.js"></script>
                              ──────
                              해시가 바뀜!

HTML이 캐시되면?
→ 옛날 해시(a1b2c3)를 가진 JS를 계속 요청
→ 새 JS(x7y8z9)가 있어도 못 받음!

그래서 HTML은 절대 캐시하면 안 됨!
JS/CSS에 s-maxage=31536000, max-age=0
───────────────────────────────────────────────────────────────

s-maxage=31536000 (CDN용: 1년)
• 파일명에 해시 있음 (main.a1b2c3.js)
• 같은 URL = 내용이 절대 안 바뀜
• CDN에서 1년 캐시해도 안전!

max-age=0 (브라우저용: 매번 확인)
• 브라우저는 매번 CDN에 확인 요청
• 하지만! CDN이 304로 빠르게 응답
• 실제 다운로드는 안 함 (ETag 매칭)
이 조합의 장점
───────────────────────────────────────────────────────────────

┌─────────────┐         ┌─────────────┐         ┌─────────────┐
│  브라우저     │         │    CDN      │         │  Origin     │
│             │         │             │         │  (S3)       │
└──────┬──────┘         └──────┬──────┘         └──────┬──────┘
       │                       │                       │
       │ max-age=0이니까        │                        │
       │ 확인 요청 보냄           │                        │
       │──────────────────────▶│                       │
       │                       │                       │
       │                       │ s-maxage=31536000     │
       │                       │ 아직 유효! Origin       │
       │                       │ 안 가도 됨              │
       │                       │                       │
       │◀──────────────────────│                       │
       │ 바로 응답 (또는 304)     │                       │

결과:
• Origin(S3) 부하: 거의 없음
• 배포 후 반영: 즉시 (새 해시니까)
• 브라우저 캐시 문제: 없음 (매번 확인하니까)
• 비용: CDN 캐시 덕분에 S3 요청 최소화

12. 빠른 참조

# 절대 캐시 안 함
Cache-Control: no-store

# 캐시하되 매번 확인
Cache-Control: no-cache

# 1시간 캐시
Cache-Control: max-age=3600

# 1년 캐시 (영구)
Cache-Control: max-age=31536000

# 브라우저만 캐시 (CDN 금지)
Cache-Control: private, max-age=3600

# CDN도 캐시
Cache-Control: public, max-age=31536000

# 브라우저는 확인, CDN은 캐시
Cache-Control: max-age=0, s-maxage=31536000

# 영구 캐시 + 재검증도 하지 마
Cache-Control: public, max-age=31536000, immutable

참고 자료

profile
Think Big Aim High Act Now

1개의 댓글

comment-user-thumbnail
2025년 12월 4일

정성스럽게 정리하셨네요.. 캐시에 대한 깊은 지식과 흐름도를 정확하게 배울 수 있었습니다.
좋은 글 감사합니다.

답글 달기