배포했는데 반영이 안 된다? 강력 새로고침(Ctrl+Shift+R)으로도 안 풀린다? 웹 캐시가 어디서 어떻게 동작하는지 전체 그림을 알면, 원인을 빠르게 찾을 수 있다.
요청이 서버에 도달하기까지 거치는 캐시 계층
사용자가 URL을 입력하고 Enter를 누르면, 요청은 여러 캐시 계층을 순서대로 거친다. 어딘가에서 캐시 HIT가 발생하면 그 아래 계층은 실행되지 않는다.
[ 클라이언트 영역 ]
| 순서 | 캐시 | 설명 |
| 1 | Memory Cache | 가장 빠름 (RAM) |
| 2 | Disk Cache (HTTP Cache) | 가장 흔한 캐시 |
| 3 | Service Worker Cache | 개발자가 직접 제어 |
| 4 | Back-Forward Cache | 뒤로가기 전용 |
| 5 | Application Cache | JS 런타임 (프레임워크, Storage) |
[ 네트워크 연결/정책 캐시 ]
| 순서 | 캐시 | 설명 |
| 6 | DNS Cache | 도메인 → IP 변환 결과 캐시 |
| 7 | TLS Session Cache | HTTPS 핸드셰이크 결과 재사용 |
| 8 | CORS Preflight Cache | OPTIONS 요청 결과 캐시 |
| 9 | HSTS Cache | HTTP → HTTPS 강제 전환 정책 캐시 |
[ 서버 / 인프라 영역 ]
| 순서 | 캐시 | 설명 |
| 10 | CDN Cache | Edge 서버 캐시 |
| 11 | Reverse Proxy Cache | Origin 앞단 캐시 |
| 12 | Server Application Cache | Redis, Memcached |
| 13 | Database Cache | Buffer Pool, Query Cache |
1. 브라우저 캐시
1-1. Memory Cache
탭 프로세스의 RAM에 저장되는 캐시. 같은 페이지에서 이미 로드한 리소스를 다시 요청하면 네트워크 없이 즉시 반환한다.
- 저장 위치: 탭 프로세스 메모리 (RAM)
- 생명주기: 탭을 닫으면 소멸
- 대상: 이미 로드된 이미지, JS, CSS, preload 리소스
- 개발자 제어: 불가 (브라우저 자동 관리)
개발자 도구 Network 탭의 Size 열에 (memory cache)로 표시된다.
1-2. Disk Cache (HTTP Cache)
가장 핵심적인 캐시. 서버 응답을 디스크에 저장하고, Cache-Control 헤더에 따라 재사용한다.
- 저장 위치: 디스크 (파일 시스템)
- 생명주기: Cache-Control, Expires 헤더에 따라 결정
- 대상: HTTP 응답 전체 (HTML, JS, CSS, 이미지, 폰트, API 응답)
Cache-Control 주요 디렉티브
| 디렉티브 | 의미 |
| max-age=31536000 | 1년간 캐시 사용 (재검증 없이) |
| no-cache | 캐시 저장은 하지만, 사용 전 매번 서버에 재검증 |
| no-store | 절대 캐시하지 않음 |
| private | 브라우저만 캐시 가능 (CDN은 캐시 금지) |
| public | CDN 포함 어디서든 캐시 가능 |
| must-revalidate | 만료 후 반드시 서버에 검증 |
| s-maxage=3600 | CDN/프록시 전용 캐시 시간 (브라우저는 무시) |
| stale-while-revalidate=60 | 만료된 캐시를 일단 사용하면서 백그라운드 갱신 |
| immutable | max-age 기간 내 재검증 자체를 안 함 |
no-cache vs no-store 차이가 중요하다.
- no-cache: "저장해도 되지만, 쓰기 전에 서버한테 물어봐" → 304 가능
- no-store: "아예 저장하지 마" → 매번 전체 응답을 받아야 함
ETag를 이용한 재검증 흐름
첫 요청:
- 브라우저 → GET /api/data
- 서버 ← 200 OK + ETag: "abc123" + 본문
재요청 (캐시 만료 후):
- 브라우저 → GET /api/data + If-None-Match: "abc123"
- 변경 없으면 → 서버 ← 304 Not Modified (본문 없음, 빠름!)
- 변경 있으면 → 서버 ← 200 OK + ETag: "def456" + 새 본문
304 응답은 본문이 없어서 수 바이트만 전송된다. no-store와 비교하면 대역폭 차이가 크다.
주의: 강력 새로고침은 만능이 아니다
강력 새로고침(Ctrl+Shift+R)은 브라우저가 직접 로드하는 리소스만 캐시를 우회한다.
- O 우회됨 — HTML 문서, <script>, <link>, <img> 등
- X 안 됨 — JavaScript 코드 내 fetch() 호출
- X 안 됨 — JavaScript 코드 내 dynamic import()
React, Next.js 같은 SPA/SSR 프레임워크는 페이지 로드 후 JS에서 fetch()로 데이터를 가져오는 경우가 많다. 이 요청들은 강력 새로고침의 영향을 받지 않아서, 디스크 캐시에 남은 이전 응답을 그대로 사용할 수 있다.
1-3. Service Worker Cache
브라우저와 서버 사이에 끼어드는 프록시. 개발자가 JS 코드로 캐시를 완전히 제어한다.
- 저장 위치: Cache Storage API (디스크)
- 생명주기: SW 코드가 명시적으로 삭제할 때까지 영구
- 제어: caches.open(), cache.put(), cache.delete()
캐시 전략 패턴
| 전략 | 동작 | 적 합한 대상 |
| Cache First | 캐시 있으면 사용, 없으면 네트워크 | 정적 리소스 (JS, CSS, 이미지) |
| Network First | 네트워크 우선, 실패 시 캐시 | API 데이터, 실시간 콘텐츠 |
| Stale While Revalidate | 캐시 즉시 반환 + 백그라운드 갱신 | 뉴스, 피드, 준실시간 데이터 |
| Cache Only | 캐시만 사용 | 완전 오프라인 앱 |
| Network Only | 네트워크만 사용 | 결제, 인증 등 |
강력 새로고침으로도 안 풀리는 이유
| 상황요청 | 흐름 |
| 일반 요청 | 브라우저 → SW 가로챔 → Cache Storage → 응답 |
| 강력 새로고침 | 브라우저 → SW 가로챔 → Cache Storage → 응답 (여전히 SW 경유!) |
| 기록 삭제 후 | 브라우저 → (SW 없음) → 서버 → 응답 |
SW는 브라우저와 서버 사이의 프록시처럼 동작하기 때문에, 강력 새로고침이 HTTP 캐시를 우회해도 SW 레이어는 여전히 통과한다. "인터넷 기록 삭제"를 해야 SW 등록 자체가 해제된다.
1-4. Back-Forward Cache (bfcache)
뒤로가기/앞으로가기 시 페이지를 즉시 복원하는 캐시. 네트워크 요청 없이 JS 상태, DOM, 스크롤 위치까지 모두 보존한다.
페이지 A에서 페이지 B로 이동한 후 뒤로가기를 누르면, bfcache에서 페이지 A를 즉시 복원한다. JS 변수값, 폼 입력값, 스크롤 위치가 모두 유지된다.
bfcache가 비활성화되는 조건:
- Cache-Control: no-store 헤더가 있는 페이지
- unload 이벤트 리스너가 등록된 페이지
- 열린 WebSocket/WebRTC 연결이 있는 페이지
1-5. Application Cache (JS 런타임)
JavaScript 코드 레벨에서 관리하는 캐시들이다.
Framework 캐시
| 프레임워크 | 캐시 종류 | 저장 위치 | 제어 방법 |
| Next.js App Router | Router Cache | 인메모리 | staleTimes, router.refresh() |
| Next.js | Full Route Cache | 서버 (빌드 타임) | revalidate, dynamic |
| Next.js | Data Cache | 서버 | revalidateTag(), revalidatePath() |
| React Query | Query Cache | 인메모리 | staleTime, gcTime |
| SWR | Cache | 인메모리 | revalidateOnFocus |
| Apollo Client | Normalized Cache | 인메모리 | fetchPolicy |
Next.js App Router의 4가지 캐시 계층
- Request Memoization — 같은 렌더링 사이클 내 동일 fetch() 자동 중복 제거
- Data Cache — fetch() 응답을 서버에 영구 캐시, revalidate로 갱신
- Full Route Cache — 빌드 타임에 정적 렌더링된 HTML + RSC payload
- Router Cache — 방문한 경로의 RSC payload를 브라우저 인메모리에 캐시
Web Storage
| 종류 | 용량 | 생명주기 | 범위 |
| localStorage | ~5-10MB | 영구 (명시적 삭제 전까지) | 동일 origin 전체 |
| sessionStorage | ~5-10MB | 탭 닫으면 소멸 | 해당 탭만 |
| IndexedDB | 수백 MB ~ GB | 영구 | 동일 origin |
| Cookie | ~4KB/개 | Expires/Max-Age 또는 세션 | 도메인+경로 |
2. 네트워크 캐시
2-1. DNS Cache
도메인 이름 → IP 주소 변환 결과를 캐시한다. 여러 단계에 걸쳐 캐시가 존재한다.
- 브라우저 DNS 캐시 — 수 분간 유지
- OS DNS 캐시 — 수 분 ~ 수 시간
- 공유기/라우터 DNS 캐시
- ISP DNS 서버 캐시
- 권한 DNS 서버 — TTL 기반
- 제어: DNS 레코드의 TTL 값
- 확인: Chrome chrome://net-internals/#dns
- 초기화: ipconfig /flushdns (Windows) / dscacheutil -flushcache (macOS)
- 주의: TTL을 짧게 설정해도 ISP가 무시할 수 있음
DNS 변경(예: 서버 이전) 후 "반영이 안 된다"는 문제의 대부분은 DNS 캐시 때문이다.
2-2. TLS Session Cache
HTTPS 연결의 핸드셰이크 비용을 줄이기 위한 캐시다.
| 연결 | 상황흐름 | 비용 |
| 첫 연결 | TCP 3-way → TLS Full Handshake → 데이터 | 2-RTT |
| 재연결 | TCP 3-way → TLS Session Resume → 데이터 | 1-RTT |
| TLS 1.3 재연결 | TCP 3-way → 0-RTT Resume → 데이터 | 첫 패킷에 데이터 포함 |
2-3. CORS Preflight Cache
Cross-Origin 요청 시 브라우저가 보내는 OPTIONS 요청(Preflight)의 결과를 캐시한다.
첫 Cross-Origin 요청:
- 브라우저 → OPTIONS /api (Preflight)
- 서버 ← 200 OK + Access-Control-Max-Age: 86400
이후 24시간 동안:
- 브라우저 → POST /api (Preflight 생략, 캐시 사용)
Access-Control-Max-Age 헤더로 캐시 시간을 제어한다. 설정하지 않으면 Chrome은 기본 2시간(7200초) 캐시한다.
2-4. HSTS Cache
Strict-Transport-Security 헤더를 받은 도메인에 대해, 이후 요청을 브라우저 레벨에서 자동으로 HTTPS로 업그레이드한다. 네트워크 요청 없이 브라우저가 직접 처리한다.
헤더 예시: Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
- 1년간 이 도메인 + 서브도메인 모두 HTTPS 강제
- preload 옵션을 설정하면 브라우저 사전 목록에 등록되어 첫 방문부터 HTTPS 적용
확인: Chrome chrome://net-internals/#hsts
3. 서버/인프라 캐시
3-1. CDN Cache
사용자와 가까운 Edge Location에 콘텐츠를 캐시한다. 원본 서버(Origin)까지 요청이 가지 않아도 된다.
- 제어: Cache-Control 헤더 + CDN 자체 설정
- 확인: 응답 헤더 X-Cache: Hit from cloudfront
- 무효화: Invalidation 요청
| Cache-Control 값 | CDN 동작 |
| public, max-age=86400 | Edge에 24시간 캐시 |
| public, s-maxage=3600 | Edge에 1시간 캐시 (브라우저에는 max-age 적용) |
| private | Edge 캐시 안 함 |
| no-store | Edge 캐시 안 함 |
CloudFront Invalidation:
aws cloudfront create-invalidation --distribution-id E123 --paths "/*"전파에 수 분이 소요되며, 월 1,000건까지 무료다.
3-2. Reverse Proxy Cache
Origin 서버 앞단에 위치하는 프록시(Nginx, Varnish 등)가 응답을 캐시한다.
# Nginx 프록시 캐시 설정 예시
proxy_cache_path /tmp/cache levels=1:2 keys_zone=my_cache:10m max_size=1g;
location /api/ {
proxy_cache my_cache;
proxy_cache_valid 200 10m; # 200 응답 10분 캐시
proxy_cache_valid 404 1m; # 404 응답 1분 캐시
proxy_cache_bypass $http_cache_control;
add_header X-Cache-Status $upstream_cache_status;
}
CDN과 비슷하지만, 단일 위치에서 더 세밀한 캐시 규칙을 적용할 수 있다.
3-3. Server Application Cache
애플리케이션 코드에서 직접 관리하는 캐시다.
| 종류 | 특징 | 용도 |
| Redis | 인메모리, 분산 가능, TTL, 다양한 자료구조 | 세션, API 응답, 계산 결과 |
| Memcached | 인메모리, 멀티스레드, 단순 K-V | 대규모 단순 캐시 |
| In-Process (Map/LRU) | 프로세스 메모리, 재시작 시 소멸 | 프로세스 내 임시 캐시 |
3-4. Database Cache
| 종류 | 설명 |
| Buffer Pool | 자주 접근하는 데이터 페이지를 메모리에 유지 (InnoDB) |
| Query Cache | 동일 쿼리 결과 캐시 (MySQL 8.0에서 제거됨) |
| Prepared Statement Cache | 파싱된 SQL 실행 계획 캐시 |
| Connection Pool | DB 연결 재사용 (캐시는 아니지만 유사 효과) |
| Materialized View | 쿼리 결과를 테이블로 저장 (수동/주기적 갱신) |
전체 요약 비교표
| 계층 | 캐시 | Ctrl+Shift+R | 기록 삭제 | 제어 방법 |
| 브라우저 | Memory Cache | O | O | 불가 (자동) |
| 브라우저 | Disk Cache (직접 요청) | O | O | Cache-Control 헤더 |
| 브라우저 | Disk Cache (JS fetch) | X | O | fetch options, 서버 헤더 |
| 브라우저 | Service Worker | X | O | SW 코드 |
| 브라우저 | bfcache | O | O | Cache-Control: no-store |
| JS 런타임 | Framework Cache | O | O | 프레임워크 설정 |
| JS 런타임 | Web Storage | X | O | 코드에서 직접 관리 |
| 네트워크 | DNS Cache | X | X | TTL, flushdns |
| 네트워크 | TLS Session | X | X | 서버 설정 |
| 네트워크 | CORS Preflight | O | O | Access-Control-Max-Age |
| 네트워크 | HSTS | X | 부분적 | max-age |
| 인프라 | CDN | X | X | Invalidation, Cache-Control |
| 인프라 | Reverse Proxy | X | X | 서버 설정 |
| 서버 | App Cache (Redis 등) | X | X | TTL, 수동 삭제 |
| 서버 | DB Cache | X | X | DB 설정 |
핵심 포인트: 강력 새로고침(Ctrl+Shift+R)은 "브라우저가 직접 요청하는 리소스"만 우회한다. JS fetch(), Service Worker, Web Storage, 그리고 네트워크/서버 측 캐시는 전혀 영향을 받지 않는다.
캐시 문제 디버깅 가이드
브라우저 개발자 도구 (F12)
Network 탭:
- Size 열: (memory cache), (disk cache), (ServiceWorker) 표시로 어느 캐시에서 왔는지 확인
- Disable cache 체크박스: 개발 중 디스크 캐시 비활성화
Application 탭:
- Cache Storage: Service Worker 캐시 내용 확인/삭제
- Service Workers: 등록된 SW 확인/해제
- Storage: localStorage, sessionStorage, IndexedDB, Cookies 확인/삭제
서버 응답 헤더에서 확인할 것
| 헤더 | 의미 |
| Cache-Control | 캐시 정책 (가장 중요) |
| ETag | 리소스 버전 식별자 |
| Last-Modified | 마지막 수정 시각 |
| Vary | 캐시 키 분기 기준 (Accept-Encoding, Cookie 등) |
| Age | CDN에서 캐시된 후 경과 시간 (초) |
| X-Cache | CDN 캐시 HIT/MISS (CloudFront) |
| CF-Cache-Status | Cloudflare 캐시 상태 |
Chrome 내부 진단 도구
| URL | 용도 |
| chrome://net-internals/#dns | DNS 캐시 확인/초기화 |
| chrome://net-internals/#hsts | HSTS 캐시 확인/삭제 |
| chrome://serviceworker-internals | 등록된 모든 Service Worker 확인/해제 |
마무리
"캐시 때문에 안 된다"는 말을 할 때, 그 캐시가 어느 계층의 캐시인지를 특정하는 것이 문제 해결의 시작이다.
- 배포 후 반영 안 됨 → Disk Cache(JS fetch), CDN Cache, Service Worker 순서로 의심
- DNS 변경 후 안 됨 → DNS Cache (브라우저 → OS → ISP)
- 뒤로가기 시 이전 상태 → bfcache
- API 응답이 안 바뀜 → Framework Cache(React Query 등), Redis, Reverse Proxy
- 강력 새로고침으로 안 풀림 → Service Worker 또는 JS fetch() 디스크 캐시
각 캐시 계층의 특성을 이해하면 "일단 캐시 비워봐"가 아닌, 정확한 원인 진단과 해결이 가능해진다.
'WEB,WAS' 카테고리의 다른 글
| Chrome 디스크 캐시 정리 방법 — 강력 새로고침으로 안 될 때 (0) | 2026.02.12 |
|---|---|
| curl은 되는데 브라우저 접속 안되는 경우- ERR_UNSAFE_PORT (0) | 2024.01.30 |
| Angular 버전 마이그레이션 (0) | 2023.10.17 |
| angular build (0) | 2023.10.12 |
| Ubuntu apache 설치 (0) | 2023.08.11 |
댓글