목록으로
개발/

브라우저 캐시 완벽 가이드 - HTTP 캐싱의 모든 것

HTTP 캐싱의 동작 원리를 정리합니다. 강력 캐시와 협상 캐시의 차이, Cache-Control 디렉티브, ETag, 리소스 유형별 전략까지 실무에 필요한 캐시 지식을 다룹니다.

guideintermediate
20 min read
브라우저 캐시
그림 1. Browser Cache

캐시가 필요한 이유

웹 페이지를 열 때마다 HTML, CSS, JavaScript, 이미지 등 수많은 리소스를 서버에서 내려받습니다. 매번 동일한 리소스를 반복해서 받는 것은 네트워크 트래픽 낭비이자 사용자 경험 저하의 원인이 됩니다.

HTTP 캐싱은 이전에 가져온 리소스를 재사용하여 이 문제를 해결합니다. 캐시의 핵심은 저장(Store)재사용(Reuse) 두 단계로 구성되며, 브라우저는 이 과정을 강력 캐시협상 캐시라는 두 가지 전략으로 처리합니다.

구분강력 캐시협상 캐시
서버 요청하지 않음조건부 요청
응답 코드200 (from cache)304 Not Modified
판단 기준Cache-Control, ExpiresETag, Last-Modified
데이터 전송없음 (로컬 캐시 사용)헤더만 전송 (본문 없음)
속도가장 빠름빠름 (헤더만 주고받음)

이 글에서는 캐시의 종류부터 동작 흐름, 각 전략의 세부 동작, 리소스별 캐시 설정, 무효화 방법까지 순서대로 정리합니다.


캐시의 종류

저장 위치에 따른 분류

브라우저 캐시는 저장 위치에 따라 Memory CacheDisk Cache로 나뉩니다.

기준Memory CacheDisk Cache
저장 위치브라우저 탭의 메모리(RAM)하드디스크/SSD
속도가장 빠름빠름 (메모리보다 느림)
수명탭을 닫으면 소멸브라우저를 닫아도 유지
대상작은 파일, 자주 사용되는 리소스큰 파일, 일반 리소스
DevTools 표시from memory cachefrom disk cache

브라우저가 리소스를 Memory Cache에 넣을지 Disk Cache에 넣을지는 자체적으로 판단합니다. 개발자가 직접 제어할 수는 없습니다.

캐시 계층에 따른 분류

캐시는 브라우저에만 존재하는 것이 아닙니다. 클라이언트에서 서버까지의 경로에 여러 계층의 캐시가 존재합니다.

plaintext
클라이언트 (브라우저)
  └── Private Cache (개인 캐시)
        ├── 특정 사용자 전용
        └── Cache-Control: private
 
중간 서버
  └── Shared Cache (공유 캐시)
        ├── Proxy Cache ─ ISP, 기업 프록시 서버
        └── Managed Cache ─ CDN, Reverse Proxy, Service Worker
 
원본 서버 (Origin Server)
  • Private Cache: 브라우저에만 저장되는 캐시입니다. Cache-Control: private으로 설정하면 CDN이나 프록시 서버에서는 캐시하지 않습니다.
  • Shared Cache: 여러 사용자가 공유하는 캐시입니다. CDN(CloudFront, Cloudflare 등)이나 Reverse Proxy(Nginx, Varnish)가 대표적입니다.

캐시 동작 흐름

브라우저 캐시 동작 흐름브라우저 캐시 동작 흐름
그림 2. 브라우저, 캐시 스토리지, 웹 서버 간의 리소스 흐름

브라우저가 리소스를 요청할 때, 캐시를 확인하는 과정은 다음과 같습니다.

캐시 동작 흐름도캐시 동작 흐름도
그림 3. 브라우저 캐시 동작 흐름 - 강력 캐시 확인 후 협상 캐시로 이어지는 과정

핵심은 강력 캐시가 먼저, 만료되면 협상 캐시가 동작한다는 점입니다. 이제 각 전략을 자세히 살펴보겠습니다.


강력 캐시 (Strong Cache)

강력 캐시는 서버에 요청하지 않고 로컬 캐시에서 즉시 리소스를 반환하는 방식입니다. 네트워크 통신이 전혀 없으므로 가장 빠릅니다.

Cache-Control 헤더

Cache-Control은 HTTP/1.1에서 도입된 캐시 제어 헤더입니다.

http
Cache-Control: max-age=31536000

주요 디렉티브를 표로 정리하면 다음과 같습니다.

디렉티브의미예시
max-age=<초>캐시 유효 시간 (초 단위)max-age=3600 (1시간)
no-cache캐시 저장은 하되, 사용 전 반드시 서버 검증자주 바뀌는 HTML
no-store캐시 저장 자체를 금지민감한 개인정보
must-revalidate만료 후 반드시 서버 검증중요 데이터
public중간 서버(CDN, 프록시)도 캐시 가능공개 이미지, JS
private브라우저만 캐시 가능개인화 페이지
s-maxage=<초>공유 캐시(CDN)에서만 적용되는 max-ageCDN 전략 분리
immutable리소스가 절대 변하지 않음해시 포함 URL
stale-while-revalidate=<초>만료 후에도 캐시를 먼저 반환하고 백그라운드에서 갱신UX 최적화

no-cache vs no-store

이름이 혼동을 주는 대표적인 디렉티브입니다.

plaintext
no-cache
├── 캐시에 저장한다
├── 사용할 때마다 서버에 검증 요청을 보낸다
├── 서버가 304 응답하면 캐시 재사용 (본문 전송 없음)
└── "캐시하되, 항상 확인하고 써라"
 
no-store
├── 캐시에 저장하지 않는다
├── 매번 서버에서 전체 리소스를 다시 받는다
└── "절대 캐시하지 마라"

no-cache는 캐시를 금지하지 않습니다.
이름과 달리 리소스를 캐시에 저장하되, 사용 전에 항상 서버에 확인하는 것입니다. 캐시 자체를 금지하려면 no-store를 사용해야 합니다.

must-revalidate의 역할

max-age가 만료된 후의 동작에 차이가 생깁니다.

상황must-revalidate 없음must-revalidate 있음
네트워크 정상서버에 검증 요청서버에 검증 요청
네트워크 불가만료된 캐시라도 사용 가능504 Gateway Timeout 반환

네트워크가 불안정한 환경에서 오래된 데이터가 표시되는 것을 방지할 때 유용합니다.

Expires와 Pragma (레거시)

HTTP/1.0에서 사용하던 헤더입니다.

http
Expires: Wed, 21 Oct 2026 07:28:00 GMT
Pragma: no-cache
  • Expires: 절대 시간으로 만료일을 지정합니다. 클라이언트와 서버의 시계가 다르면 문제가 발생할 수 있습니다.
  • Pragma: Cache-Control: no-cache와 동일한 역할입니다. HTTP/1.0 하위 호환용으로만 사용합니다.

헤더 우선순위는 Cache-Control > Expires > Pragma 순입니다. Cache-Control: max-ageExpires가 함께 있으면 max-age가 우선됩니다.


협상 캐시 (Negotiated Cache)

강력 캐시가 만료(Stale)된 후, 서버에 "리소스가 변경되었는지" 확인하는 방식입니다. 변경이 없으면 본문 없이 304 Not Modified만 응답하므로, 전체 리소스를 다시 받는 것보다 훨씬 효율적입니다.

ETag / If-None-Match (권장)

ETag는 리소스의 내용을 기반으로 생성된 고유 식별자(해시값)입니다.

ETag 협상 캐시 흐름ETag 협상 캐시 흐름
그림 4. ETag / If-None-Match 협상 캐시 흐름

ETag는 Strong ETag("33a64df5")와 Weak ETag(W/"33a64df5")로 나뉩니다. Strong ETag는 바이트 단위 완전 일치를 요구하고, Weak ETag는 의미적 동등성만 확인합니다.

Last-Modified / If-Modified-Since

파일의 마지막 수정 시간을 기준으로 비교하는 방식입니다.

Last-Modified 협상 캐시 흐름Last-Modified 협상 캐시 흐름
그림 5. Last-Modified / If-Modified-Since 협상 캐시 흐름
Last-Modified의 한계
  • 초 단위 정밀도: 1초 내 여러 번 수정되면 변경을 감지하지 못합니다.
  • 내용 무관 변경: 파일을 열었다 저장만 해도 수정 시간이 바뀝니다. 내용이 같아도 변경으로 인식됩니다.
  • 분산 서버 환경: 서버마다 파일의 수정 시간이 다를 수 있습니다.

이러한 한계 때문에 ETag를 기본으로 사용하고, Last-Modified는 보조 수단으로 함께 제공하는 것이 권장됩니다.

ETag vs Last-Modified 비교

항목ETagLast-Modified
정밀도내용 기반 (정확함)시간 기반 (초 단위)
우선순위높음 (먼저 확인)낮음 (ETag 없을 때 사용)
계산 비용해시 계산 필요파일시스템에서 즉시 확인

베스트 프랙티스
서버는 ETag와 Last-Modified를 둘 다 응답에 포함하는 것이 좋습니다. 브라우저는 If-None-Match(ETag)를 우선 확인하고, 없으면 If-Modified-Since를 사용합니다.


휴리스틱 캐싱

휴리스틱 캐싱Cache-Control이나 Expires가 없을 때, 브라우저가 자체적으로 캐시 기간을 추정하는 방식입니다.

동작 조건

  • Cache-Control 헤더 없음
  • Expires 헤더 없음
  • Last-Modified 헤더 있음

캐시 기간 계산

plaintext
캐시 기간 = (현재 시간 - Last-Modified) × 10%

예를 들어, 마지막 수정 후 1년(365일)이 경과했다면 캐시 유효 기간은 약 36.5일로 추정됩니다.

http
HTTP/1.1 200 OK
Last-Modified: Tue, 22 Feb 2025 22:22:22 GMT
Date: Tue, 22 Feb 2026 22:22:22 GMT

휴리스틱 캐싱은 예측 불가능한 동작을 유발할 수 있습니다. 모든 응답에 명시적으로 Cache-Control을 설정하여 휴리스틱 캐싱을 방지하는 것이 권장됩니다.


GET vs POST 캐시 동작

HTTP 메서드에 따라 캐시 동작이 다릅니다.

항목GETPOST
캐시 여부캐시됨캐시 안 됨
캐시 키URL + 쿼리 스트링해당 없음
HTTP 스펙안전한 메서드 (Safe)부수 효과 있음 (Unsafe)

GET 요청은 URL이 같으면 캐시가 적중합니다. 쿼리 파라미터가 다르면 별도의 캐시 엔트리로 저장됩니다.

plaintext
GET /api/servers?keyword=web     → 서버 요청 → 캐시 저장 [A]
GET /api/servers?keyword=db      → 서버 요청 → 캐시 저장 [B]
GET /api/servers?keyword=web     → 캐시 히트 [A]
GET /api/servers?keyword=web&page=2  → 서버 요청 → 캐시 저장 [C]

POST는 HTTP 스펙(RFC 7231)에서 부수 효과(side effect)가 있는 메서드로 정의됩니다. 같은 요청을 보내도 서버 상태가 변경될 수 있으므로, 브라우저는 POST 응답을 캐시하지 않습니다.

AJAX 요청도 동일한가?

동일합니다. $.ajax(), fetch() 등의 AJAX 요청도 일반 HTTP 요청과 같은 캐싱 규칙을 따릅니다.

javascript
// GET → 캐시 대상
fetch('/api/list.do?page=1');
 
// POST → 캐시 안 됨
fetch('/api/save.do', { method: 'POST', body: formData });

Vary 헤더

같은 URL이라도 요청 헤더에 따라 다른 응답을 제공해야 할 때 사용합니다.

http
Vary: Accept-Language

이 경우 캐시 키가 URL + Accept-Language 값으로 구성됩니다.

plaintext
GET /index.html  (Accept-Language: ko)  → 한국어 HTML 캐시
GET /index.html  (Accept-Language: en)  → 영어 HTML 캐시 (별도 저장)

Vary 사용 시 주의사항

설정권장 여부이유
Vary: Accept-Encoding권장gzip/br 구분에 적절
Vary: Accept-Language권장다국어 지원에 적절
Vary: User-Agent비권장변형이 너무 많아 캐시 적중률 급감
Vary: Cookie비권장사용자마다 다른 캐시가 생성되어 의미 없음

리소스 유형별 캐시 전략

실무에서 리소스 유형에 따라 어떤 캐시 전략을 적용하면 좋은지 정리합니다.

리소스 유형Cache-Control이유
HTML (메인 페이지)no-cache항상 최신 확인 필요, 304로 빠르게 검증
JS/CSS (해시 포함 URL)max-age=31536000, immutableURL이 곧 버전, 영구 캐시 안전
JS/CSS (해시 없음)no-cache 또는 max-age=0변경 감지 불가, 매번 검증 필요
이미지/폰트max-age=2592000 (30일)자주 변경되지 않음
API 응답no-store 또는 no-cache, private동적 데이터, 개인정보 포함 가능
민감 데이터no-store절대 캐시 금지

실제 사례: 토스의 캐시 전략

http
HTML 파일:
  Cache-Control: max-age=0, s-maxage=31536000
  → 브라우저: 매번 서버 검증 (ETag/304)
  → CDN: 1년 캐시 (배포 시 CDN Invalidation)
 
JS/CSS 파일 (해시 포함):
  Cache-Control: max-age=31536000
  URL: /static/bundle.a1b2c3d4.js
  → URL에 콘텐츠 해시가 포함되어 영구 캐시 안전
  → 파일 변경 시 해시가 바뀌어 새 URL로 요청됨

max-age=0s-maxage=31536000을 조합하면, 브라우저는 매번 서버에 검증하면서도 CDN에서는 1년간 캐시를 유지할 수 있습니다. 배포할 때 CDN Invalidation만 수행하면 됩니다.


캐시 무효화 방법

클라이언트 측

방법동작
일반 새로고침 (F5)Cache-Control: max-age=0 + 조건부 요청
강력 새로고침 (Ctrl+F5)캐시 무시, 서버에서 전체 리소스를 새로 받음
캐시 삭제브라우저 설정에서 수동 삭제

서버 측

방법동작
Cache BustingURL에 버전/해시 포함 (가장 효과적)
CDN InvalidationCDN 캐시 삭제 (브라우저 캐시는 영향 없음)
Clear-Site-DataClear-Site-Data: "cache" 헤더로 브라우저 캐시 삭제
짧은 max-age캐시 유효 기간 단축

Cache Busting 전략

캐시 버스팅은 URL을 변경하여 브라우저가 새 리소스로 인식하게 만드는 기법입니다.

plaintext
방법 1: 쿼리 스트링 (간단하지만 일부 프록시에서 무시될 수 있음)
  /js/common.js?v=1.2.3
  /js/common.js?v=1.2.4
 
방법 2: 파일명에 해시 삽입 (권장)
  /js/common-a1b2c3d4.js
  /js/common-e5f6g7h8.js
 
방법 3: 경로에 버전 포함
  /v1.2.3/js/common.js
  /v1.2.4/js/common.js

파일명 해시 방식이 가장 안정적입니다.
쿼리 스트링 방식은 일부 CDN/프록시에서 캐시 키로 인식하지 않을 수 있습니다. Webpack, Vite 등 번들러에서 자동으로 파일명에 해시를 삽입해줍니다.


DevTools에서 캐시 확인하기

Chrome DevTools의 Network 탭에서 캐시 동작을 확인할 수 있습니다.

Status / Size 컬럼 해석

표시의미서버 요청
200 (일반 Size)서버에서 새로 받음O
200 (from memory cache)메모리 캐시 히트 (강력 캐시)X
200 (from disk cache)디스크 캐시 히트 (강력 캐시)X
304 Not Modified협상 캐시 성공O (헤더만)
  • from memory cache는 0ms에 가까운 속도로 로딩됩니다.
  • from disk cache는 수 ms 정도 소요됩니다.
  • 304는 수백 바이트의 헤더만 전송됩니다.

DevTools를 열고 "Disable cache"를 체크하면 모든 캐시가 비활성화됩니다. 개발 중에만 사용하고, 실제 사용자 환경을 테스트할 때는 반드시 해제해야 합니다.


정리

개념설명
강력 캐시Cache-Control: max-age로 서버 요청 없이 로컬에서 즉시 응답
협상 캐시ETag/Last-Modified로 변경 여부만 확인하여 304로 빠르게 응답
no-cache캐시를 금지하지 않음. "저장하되, 매번 확인하고 사용"
no-store진정한 캐시 금지. 저장 자체를 하지 않음
캐시 대상GET 요청만 캐시됨. POST는 캐시되지 않음
AJAX 캐시AJAX도 일반 HTTP 요청과 동일한 캐싱 규칙을 따름
쿼리 파라미터쿼리 파라미터가 다르면 별도의 캐시 엔트리
Cache BustingURL에 해시를 삽입하는 것이 가장 안정적인 캐시 무효화 전략
휴리스틱 캐싱모든 응답에 Cache-Control을 명시하여 방지
베스트 프랙티스ETag와 Last-Modified를 함께 제공

참고 자료

강력 캐시(Strong Cache)
Cache-Control의 max-age 또는 Expires 헤더로 유효 기간을 지정한다. 유효 기간 내에는 서버와 통신 없이 브라우저 캐시에서 리소스를 즉시 반환하므로 가장 빠르다. DevTools에서 'from memory cache' 또는 'from disk cache'로 표시된다.
협상 캐시(Negotiated Cache)
ETag/If-None-Match 또는 Last-Modified/If-Modified-Since 헤더를 사용한다. 서버가 304 Not Modified를 응답하면 본문 없이 헤더만 전송되므로, 전체 리소스를 다시 받는 것보다 훨씬 빠르다.
Cache-Control
HTTP/1.1에서 도입된 캐시 제어 헤더이다. max-age, no-cache, no-store, public, private 등의 디렉티브를 조합하여 리소스의 캐시 정책을 세밀하게 지정할 수 있다. Expires보다 우선순위가 높다.
ETag
Entity Tag의 약자. 서버가 리소스의 내용을 기반으로 생성하는 고유 식별자이다. 리소스가 변경되면 ETag 값도 변경된다. Strong ETag(바이트 단위 일치)와 Weak ETag(의미적 동등성)로 나뉜다.
캐시 버스팅(Cache Busting)
파일명에 해시를 삽입(bundle.a1b2c3.js)하거나 쿼리 스트링에 버전을 추가(?v=1.2.3)하여 브라우저가 새 리소스로 인식하게 만드는 전략이다. 파일명 해시 방식이 가장 안정적이다.
휴리스틱 캐싱(Heuristic Caching)
'(현재 시간 - Last-Modified) x 10%' 공식으로 캐시 유효 기간을 계산한다. 예측 불가능한 동작을 유발할 수 있으므로, 모든 응답에 Cache-Control을 명시적으로 설정하는 것이 권장된다.

관련 글