브라우저가 느려지는 이유는 대개 네트워크다. 같은 파일을 매번 내려받는 건 돈(트래픽)과 시간(레이턴시)을 같이 태운다. HTTP 캐시는 이걸 “이미 받은 건 재사용하자”로 바꿔서 UX를 확 끌어올린다. 다만 “캐시한다”는 말 안에는 만료(freshness), 검증(validation), 공유(proxy), 무효화(invalidation)까지 꽤 많은 규칙이 숨어 있다.
1. 캐시가 없을 때 vs 있을 때
캐시가 없을 때
리소스가 변하지 않았더라도 매 요청마다 서버로부터 다시 내려받는다.
- 네트워크 비용 증가 (대역폭/트래픽)
- 초기 로딩, 재방문 로딩 모두 느림
- 결과적으로 사용자 경험이 나빠짐
캐시가 있을 때
응답을 로컬(브라우저 캐시)에 저장하고, 유효 기간 동안은 네트워크를 타지 않는다.
대표적인 지시어는 이거 하나로 시작한다.
- Cache-Control: max-age=60
→ “이 응답은 60초 동안 fresh(신선)하다. 그동안은 캐시에서 바로 써라.”
중요 포인트
- max-age가 살아있는 동안은 “재요청” 자체가 안 나간다.
- max-age가 끝나면 그때부터 “검증(조건부 요청)”이 개입한다.
2. 캐시 만료 이후의 현실: “진짜 바뀌었나?”를 확인해야 한다
max-age가 끝났다고 해서 매번 1MB짜리 파일을 다시 내려받는 건 낭비다.
캐시 만료 이후에도 서버 데이터가 그대로일 수 있기 때문이다.
이때 필요한 게 검증 헤더(validator) 와 조건부 요청(conditional request) 이다.
핵심 목표는 하나다:
“서버 데이터가 그대로라면, 바디(body) 말고 ‘바뀌지 않았다’는 사실만 알려줘.”
그래서 등장하는 응답이:
- 304 Not Modified
→ 바디 없음. 대신 헤더(메타 정보)만 갱신하고, 클라이언트는 자기 캐시를 재사용한다.
3. 검증 헤더와 조건부 요청: Last-Modified vs ETag
캐시 검증은 크게 두 가지 축으로 돌아간다.
Last-Modified / If-Modified-Since (시간 기반)
서버가 “이 리소스 마지막 수정 시각”을 알려준다.
- 응답: Last-Modified: Tue, 10 Nov 2020 10:00:00 GMT
- 재요청(조건부): If-Modified-Since: Tue, 10 Nov 2020 10:00:00 GMT
동작은 직관적이다.
- 서버가 그 이후로 수정 안 했으면 → 304 Not Modified
- 수정했으면 → 200 OK + 새 바디
장점
단점(실무에서 걸리는 포인트)
- 1초 미만 단위 제어가 어렵다.
- “내용은 동일한데 저장만 다시 해서 수정 시간이 바뀐 경우” 같은 케이스에 취약하다.
- 서버가 “의도적으로 캐시 정책을 통제”하기 어렵다.
(Ex. 의미 없는 변경은 캐시 유지하고 싶은데 시간은 바뀜)
ETag / If-None-Match (버전 기반)
서버가 “이 표현의 버전(태그)”를 준다.
- 응답: ETag: "a2jiodwjekjl3"
- 재요청: If-None-Match: "a2jiodwjekjl3"
판단은 더 명확해진다.
- ETag 동일 → 304 Not Modified
- ETag 다름 → 200 OK + 새 바디
ETag의 본질
- 클라이언트가 캐시 메커니즘을 “이해”할 필요가 없다.
- 서버가 “이 버전은 유지/갱신”을 완전히 통제한다.
예를 들어 서버가 이렇게도 할 수 있다:
- “베타 오픈 3일간은 파일이 조금 바뀌어도 ETag 유지”
- “배포 주기에 맞춰 ETag 전부 갱신”
4. 캐시 제어 헤더 한 장 요약
Cache-Control (현대 표준)
- max-age=초 : fresh 기간
- no-cache : 저장해도 되지만, 사용 전 항상 원 서버에 검증해야 한다.
(이름이 헷갈리는 대표 케이스)
- no-store : 저장 금지 (민감 데이터)
하위 호환
- Pragma: no-cache : HTTP/1.0 대응
- Expires: 날짜 : 만료 시각(HTTP/1.0). 요즘은 max-age 선호
- 둘 다 있으면 Cache-Control이 우선 (Expires는 무시)
Validator / Conditional 헤더 정리
- Validator: Last-Modified, ETag
- Conditional:
- 시간 기반: If-Modified-Since, If-Unmodified-Since
- 태그 기반: If-None-Match, If-Match
5. 프록시 캐시: “나만 빠른 게 아니라, 모두가 빠르게”
브라우저 캐시는 개인(private) 캐시다.
하지만 동일 리소스를 많은 사용자가 요청한다면, 중간에 공용(public) 캐시를 두는 게 더 효율적이다.
Ex.
브라우저(private) → 한국 프록시 캐시(public) → 해외 오리진(origin)
프록시 캐시에서 자주 쓰는 헤더:
- Cache-Control: public : 공용 캐시에 저장 가능
- Cache-Control: private : 개인 캐시에만 저장(기본 성격)
- Cache-Control: s-maxage=초 : 프록시 캐시 전용 max-age
- Age: 60 : 프록시에 저장된 지 얼마나 됐는지(초)
실무에서는 “브라우저는 빨리 갱신해야 하는데 CDN은 좀 더 오래 들고 있어도 됨” 같은 요구가 많아서 s-maxage가 꽤 유용하다.
6. 캐시 무효화: “확실하게” 막고 싶을 때
민감한 데이터(개인정보, 결제, 관리자 화면)나
“절대 오래된 값을 보여주면 안 되는” 응답은 캐시 전략이 달라진다.
자주 쓰는 조합:
- Cache-Control: no-store
→ 아예 저장 금지 (가장 강함)
- Cache-Control: no-cache
→ 저장은 가능, 매번 원 서버 검증 후 사용
- Cache-Control: must-revalidate
→ 만료 후 첫 조회는 반드시 원 서버 검증
→ 원 서버 접근 실패 시 반드시 오류 (보통 504 Gateway Timeout)
no-cache vs must-revalidate 차이 (실무 포인트)
둘 다 “검증하고 써라” 느낌인데, 장애 상황에서 갈린다.
no-cache
- 원 서버가 잠깐 죽었을 때
- 프록시 캐시 설정에 따라 그냥 오래된 캐시를 200으로 내줄 수도 있음
- (운영 철학: “에러보단 구버전이라도 보여주자”)
must-revalidate
- 원 서버가 죽었으면
- 오래된 캐시를 주면 안 됨 → 504로 실패해야 함
- (운영 철학: “틀린 정보는 못 보여준다”)
7. 정리
캐시 설계는 “성능”이 아니라 “정합성 정책”이다
캐시는 단순히 빠르게 만드는 기능이 아니라,
- 얼마나 오래 믿을 것인지(max-age)
- 만료 후엔 어떻게 확인할 것인지(ETag/Last-Modified)
- 누구와 공유할 것인지(public/private, s-maxage)
- 장애 시 구버전을 허용할 것인지(no-cache vs must-revalidate)
이런 정책 결정에 더 가깝다.
출처
모든 개발자를 위한 HTTP 웹 기본 지식 (김영한, 인프런, 2020)