캐시 문제를 디버깅하다가 Disable cache 옵션이 켜져 있었다는 걸 뒤늦게 발견했다.
캐시를 이해하려고 개발자 도구를 열었는데, 오히려 그 도구가 캐시 동작을 바꾸고 있었던 것이다.
이 글은 그 과정에서 정리한 브라우저 캐시의 동작 원리다.
1. Request Headers vs Response Headers — 누가 보내는 것인가
개발자 도구 Network 탭에서 요청을 클릭하면 Headers 탭에 두 가지가 보인다.
Request Headers → 브라우저가 서버에게 보내는 것
Response Headers → 서버가 브라우저에게 보내는 것
요청/응답 흐름으로 보면 이렇다.
브라우저 서버 (CloudFront / S3)
│ │
│ ── Request ──────────────────────> │
│ Cache-Control: no-cache │ ← 브라우저가 작성
│ Accept: text/html │
│ If-None-Match: "abc123" │
│ │
│ <── Response ───────────────────── │
│ Cache-Control: public, max-age=0 │ ← 서버가 작성
│ ETag: "abc123" │
│ Content-Type: text/html │
│ │
캐시 정책의 실권은 Response Headers에 있다
Request Header에서 브라우저가 아무리 no-cache를 보내도, 다음 번 요청부터의 캐시 동작은 Response Header의 지시에 따라 결정된다.
Response Header: Cache-Control: max-age=3600
↓
브라우저: "1시간 동안은 서버에 요청 안 해도 되겠네"
↓
1시간 이내 재요청 → 서버에 요청 자체를 보내지 않음
(Request Header 자체가 생성되지 않음)
Request Header의 Cache-Control: no-cache는 이번 요청에 한해서만 캐시를 무시하는 것이고, 브라우저의 캐시 정책 자체를 바꾸지는 않는다.
2. Cache-Control 주요 값 정리
Response Header 기준
max-age=3600 → 3600초(1시간) 동안 캐시 유효, 서버 요청 안 함
public, max-age=0 → 즉시 만료, 매번 서버에 재검증 요청
no-cache → 저장은 하되, 사용 전 반드시 서버에 재검증
no-store → 캐시 저장 자체를 금지
immutable → 파일이 절대 바뀌지 않음, 재검증 불필요
no-cache vs no-store — 헷갈리기 쉬운 차이
no-cache |
no-store |
|
|---|---|---|
| 캐시 저장 | ✅ 저장함 | ❌ 저장 안 함 |
| 서버 재확인 | ✅ 매번 확인 | ✅ 매번 새로 받음 |
| 304 응답 가능 | ✅ 가능 (빠름) | ❌ 항상 200 |
| 네트워크 비용 | 낮음 | 높음 |
no-cache는 "캐시 사용 금지"가 아니라 "캐시 사용 전 반드시 확인"이다.
public, max-age=0 이 의미하는 것
캐시를 사용하지 않겠다 ❌
캐시는 저장하되, 매번 서버에 확인하겠다 ✅
3. ETag — 서버가 발급하는 파일의 지문
ETag(Entity Tag)는 서버가 리소스에 부여하는 고유 식별자다. S3/CloudFront는 파일의 MD5 해시값을 ETag로 자동 생성한다.
파일 내용이 바뀌면 → ETag 값도 바뀜
파일 내용이 같으면 → ETag 값도 같음
ETag 검증 흐름
첫 번째 요청
브라우저 → 서버: GET /page.html
서버 → 브라우저: 200 OK
ETag: "abc123"
Cache-Control: max-age=0
브라우저: ETag와 파일을 함께 캐시에 저장
두 번째 요청 (max-age=0 이므로 즉시 만료)
브라우저 → 서버: GET /page.html
If-None-Match: "abc123" ← "이 지문이랑 같아?"
서버: 파일 그대로 → 304 Not Modified ← "같아, 캐시 써도 돼"
서버: 파일 바뀜 → 200 OK + 새 파일 ← "달라, 새로 줄게"
ETag 검증은 요청-응답 사이클 안에서 일어나는 것이다. 요청 자체가 발생하지 않으면 ETag 검증도 없고, 그냥 캐시를 그대로 사용한다.
4. 전체 요청 프로세스
브라우저가 리소스를 요청할 때 내부적으로 이런 판단을 거친다.
브라우저가 리소스 요청
↓
캐시에 해당 URL이 있는가?
├── 없음 → 서버에 새로 요청 → 200 OK + 리소스 저장
└── 있음
↓
max-age가 남아있는가?
├── 남아있음 → 서버 요청 없이 캐시 바로 사용 → 200 (disk/memory cache)
└── 만료됨
↓
ETag / Last-Modified 가 있는가?
├── 있음 → If-None-Match 포함해서 서버 재검증 요청
│ ├── 변경 없음 → 304 Not Modified → 캐시 재사용
│ └── 변경 있음 → 200 OK + 새 리소스
└── 없음 → 서버에 새로 요청 → 200 OK
5. 개발자 도구 Network 탭에서 캐시 구분하기
Status Code가 200이어도 실제 서버에서 받아온 것과 캐시에서 꺼낸 것이 다르다.
Size 컬럼으로 구분
Name Status Size
page.html 200 1.2 kB ← 실제 서버 응답
polling-manager.js 200 (memory cache) ← 메모리 캐시
style.css 200 (disk cache) ← 디스크 캐시
api/data 304 -- ← ETag 검증, 변경 없음
세 가지 200의 차이
| 표시 | 서버 통신 | 의미 |
|---|---|---|
200 + 파일 크기 |
✅ 있음 | 실제 서버에서 받아옴 |
200 (memory cache) |
❌ 없음 | 메모리에서 즉시 반환 (같은 탭) |
200 (disk cache) |
❌ 없음 | 디스크에서 반환 (탭 닫아도 유지) |
304 |
✅ 있음 | 서버 확인 후 변경 없음 |
200 (disk cache)는 서버와 통신이 전혀 없었음에도 200으로 표시된다. Remote Address 컬럼이 비어있으면 캐시에서 반환된 것이다.
6. Disable cache 옵션
개발자 도구 Network 탭 상단의 Disable cache 체크박스.
실제 동작
Disable cache 체크 ✅
→ 개발자 도구가 열려있는 동안 모든 요청에 강제로 no-cache 적용
→ 캐시를 완전히 무시하고 항상 서버에 재요청
→ 모든 응답이 200으로 표시됨 (304 없음)
Disable cache 체크 해제 ⬜
→ 브라우저 정상 캐시 동작
→ ETag 검증 정상 동작
→ 304 응답 정상 반환
강력 새로고침과의 차이
강력 새로고침 (Ctrl+Shift+R)
→ 현재 요청에 한해서만 no-cache 적용
→ 일회성, 이후 요청은 정상 캐시 동작
→ iframe은 적용 안 됨
Disable cache + 일반 새로고침
→ 개발자 도구가 열려있는 동안 지속적으로 적용
→ 페이지 내 모든 리소스에 적용
→ iframe 요청도 포함 ✅
| 일회성 | 지속성 | iframe 포함 | |
|---|---|---|---|
| 강력 새로고침 | ✅ | ❌ | ❌ |
| Disable cache | ❌ | ✅ | ✅ |
언제 켜고 끄는가
✅ 켜야 할 때
JS / CSS 수정 후 즉시 반영 확인
Response Header 캐시 설정 검증
배포 직후 변경사항 즉시 확인
❌ 끄고 테스트해야 할 때
실제 사용자 경험 재현
캐시 동작 자체를 검증 (304, ETag, max-age 등)
성능 측정 (캐시 히트 없으면 실제보다 느리게 측정됨)
캐시 관련 디버깅을 할 때 Disable cache가 켜져 있으면 오히려 원인 파악이 더 어려워지는 아이러니한 상황이 생긴다.
7. 새로고침 방식별 동작 비교
| 동작 | 캐시 삭제 | 서버 재요청 | iframe 포함 |
|---|---|---|---|
| 링크 클릭 / 주소창 입력 | ❌ | 조건부 (max-age 확인) | ❌ |
| 일반 새로고침 (F5) | ❌ | 조건부 (ETag 검증) | ❌ |
| 강력 새로고침 (Ctrl+Shift+R) | ❌ | ✅ (부모 페이지만) | ❌ |
| 저장 데이터 전체 삭제 | ✅ | ✅ (모든 리소스) | ✅ |
| Disable cache + 새로고침 | ❌ | ✅ (모든 리소스) | ✅ |
강력 새로고침은 캐시를 삭제하는 것이 아니다. 캐시를 무시하고 서버에 재요청한 뒤, 받아온 응답으로 캐시를 덮어쓸 뿐이다. 기존 캐시 데이터는 그대로 남아있다.
마치며
캐시는 성능을 위한 도구지만, 동작 원리를 정확히 알지 못하면 디버깅에 많은 시간을 쓰게 된다. 이번에 정리하면서 깨달은 핵심은 세 가지다.
1. Request Header의 no-cache와 Response Header의 no-cache는 의미가 다르다.
2. 강력 새로고침은 캐시를 삭제하는 게 아니라 무시하는 것이다.
3. 캐시 디버깅을 할 때는 Disable cache를 꺼야 실제 동작을 볼 수 있다.
캐시 문제를 만나면 가장 먼저 할 질문은 이것이다.
"지금 어느 레이어의 캐시 문제인가? — 브라우저인가, CDN인가?"