Cache-Control
사용법
오리진, 중간 노드, 클라이언트 모두 저장된 응답을 언제까지 계속 쓸 수 있고 언제 새 사본이 필요한지 합의하기 위해 Cache-Control에 의존합니다. 하나의 응답은 완전한 캐싱 정책을 표현하기 위해 여러 디렉티브를 조합하는 경우가 많습니다. 얼마나 오래 저장할지, 누가 저장할 수 있는지, 신선도가 만료되면 어떻게 할지를 담습니다.
아래 표는 각 디렉티브가 요청에 적용되는지, 응답에 적용되는지, 아니면 둘 다인지 보여 줍니다.
여러 디렉티브는 하나의 헤더 값 안에서 쉼표로 구분하거나 여러 Cache-Control 헤더로 나눕니다. 서로 충돌하는 디렉티브가 나타나면 가장 제한적인 조합이 적용됩니다.
참고
인식되지 않는 디렉티브는 캐시가 무시합니다. 덕분에 기존 구현을 망가뜨리지 않고 새 디렉티브를 도입할 수 있습니다.
참고
Cache-Control 헤더가 없으면 캐시는 휴리스틱 신선도를 적용합니다. 흔한 휴리스틱은 Last-Modified 이후 경과한 시간의 10%입니다. 즉 Cache-Control이 없는 응답도 브라우저와 CDN이 캐시합니다. Cache-Control을 명시적으로 설정하면 예기치 않은 캐싱 동작을 막을 수 있습니다.
참고
Cache-Control은 언제 재검증할지(신선도 수명)를 관장합니다. ETag는 어떻게 재검증할지(콘텐츠 동일성)를 관장합니다. 둘은 함께 작동합니다. Cache-Control은 재검증이 필요한지를 결정하고, ETag는 콘텐츠가 바뀌었는지를 결정합니다. Pragma 헤더는 Cache-Control: no-cache에 대응하는 HTTP/1.0 방식이며 사실상 더 이상 사용되지 않습니다.
max-age
max-age 디렉티브는 응답이 지정된 초 수보다 오래되지 않았을 때만 저장된 응답을 반환하라고 캐시에 알립니다. 음수 같은 유효하지 않은 값은 0으로 취급됩니다.
Cache-Control: max-age=<seconds>
max-age=0으로 설정하면 종단 간 재검증이 강제됩니다. 이 패턴은 no-cache를 지원하지 않던 HTTP/1.0 구현에서 비롯되었습니다.
max-stale
max-stale 디렉티브는 Age가 신선도 수명을 주어진 초 수만큼까지 넘긴 응답도 받아들이겠다는 신호입니다. 허용 구간은 max-age가 만료된 시점부터 시작됩니다.
Cache-Control: max-stale=<seconds>
이 디렉티브는 오리진 서버에 일시적으로 접근할 수 없고 약간 오래된 응답이라도 괜찮을 때 유용합니다.
min-fresh
min-fresh 디렉티브는 남은 신선도 수명이 최소한 지정된 초 수 이상인 저장된 응답을 요청합니다. 캐시는 응답이 그 추가 기간 동안 계속 신선할 때만 저장된 사본을 반환합니다.
Cache-Control: min-fresh=<seconds>
no-cache
no-cache를 담은 요청은 캐시가 사본을 제공하기 전에 오리진과 함께 저장된 응답을 검증하도록 요구합니다. 이는 저장된 항목을 버리지 않으면서 재검증을 강제합니다.
Cache-Control: no-cache
참고
캐시가 응답을 아예 저장하지 못하게 하려면 대신 no-store를 사용하십시오.
no-store
no-store 디렉티브는 캐시에 요청이나 그에 대응하는 응답을 저장하지 말 것을 요청합니다.
Cache-Control: no-store
no-transform
no-transform 디렉티브는 중간 노드가 이미지를 재압축하거나 미디어 형식을 변환하는 등 응답 본문을 수정하는 것을 금지합니다. 이 제한은 중간 노드가 캐싱 프록시이든 전달 게이트웨이이든 똑같이 적용됩니다.
Cache-Control: no-transform
only-if-cached
only-if-cached 디렉티브는 오리진에 연락하지 말고 저장된 응답을 반환하라고 캐시에 알립니다. 적합한 저장 응답이 없으면 캐시는 504 상태를 반환합니다.
Cache-Control: only-if-cached
max-age
max-age 디렉티브는 응답이 생성된 뒤 신선한 상태로 유지되는 초 수를 선언합니다. 타이머는 오리진이 응답을 만든 순간부터 시작되므로, 전송 시간과 중간 캐시에서 머문 시간이 그 예산에서 차감됩니다.
Cache-Control: max-age=<seconds>
max-age=3600인 응답은 한 시간 동안 신선한 상태를 유지합니다. 한 시간이 지나면 캐시는 그 응답을 오래된 것으로 취급하고, 함께 있는 다른 디렉티브에 따라 재검증하거나 새 사본을 가져옵니다.
s-maxage
s-maxage(공유 max-age) 디렉티브는 CDN과 프록시 서버 같은 공유 캐시에서 max-age를 덮어씁니다. 개인 브라우저 캐시는 s-maxage를 무시하고 max-age로 되돌아갑니다.
Cache-Control: s-maxage=<seconds>
no-cache
응답의 no-cache 디렉티브는 캐시가 재사용할 때마다 오리진과 함께 저장된 응답을 재검증하도록 요구합니다. 캐시는 여전히 응답을 저장하므로 ETag나 Last-Modified를 이용한 조건부 요청이 가능합니다.
Cache-Control: no-cache
no-cache에 필드 이름 목록이 포함되면 그 특정 헤더들만 재검증이 필요합니다. 저장된 응답의 나머지는 오리진에 연락하지 않고 제공됩니다.
Cache-Control: no-cache="Set-Cookie"
참고
저장 자체를 막으려면 no-store를 사용하십시오.
no-store
no-store 디렉티브는 어떤 캐시도 응답을 저장하지 못하게 합니다. 이후의 모든 요청은 오리진으로 갑니다.
Cache-Control: no-store
참고
no-cache는 저장은 허용하되 사용할 때마다 재검증을 요구합니다. 캐시는 If-None-Match나 If-Modified-Since로 오리진에 확인하고 콘텐츠가 바뀌지 않았으면 304를 받습니다. no-store는 저장 자체를 막으므로 사본이 전혀 남지 않고 모든 요청이 전체 응답을 가져옵니다. 금융이나 의료 기록 같은 민감한 데이터에 no-store를 의도하면서 no-cache를 쓰는 것이 흔한 실수입니다. no-cache는 자주 갱신되는 콘텐츠에 효율적입니다. no-store는 디스크에 결코 남아서는 안 되는 콘텐츠를 위한 것입니다.
no-transform
응답의 no-transform 디렉티브는 중간 노드가 콘텐츠를 캐시하든 하지 않든, 전달 전에 응답 본문을 변경하지 못하게 막습니다.
Cache-Control: no-transform
must-revalidate
must-revalidate 디렉티브는 응답이 신선한 동안에는 캐시가 그대로 제공하도록 허용합니다. 일단 오래되면 캐시는 다시 제공하기 전에 오리진에 연락해 재검증합니다. 오리진에 접근할 수 없으면 캐시는 오래된 콘텐츠를 제공하는 대신 504를 반환합니다.
Cache-Control: must-revalidate
proxy-revalidate
proxy-revalidate 디렉티브는 must-revalidate와 동일하게 작동하되, 그 요건이 공유 캐시에만 적용됩니다. 개인 브라우저 캐시는 영향을 받지 않습니다.
Cache-Control: proxy-revalidate
must-understand
must-understand 디렉티브는 캐시가 상태 코드를 인식하고 그에 연관된 캐싱 요건을 이해할 때만 응답을 저장하도록 지시합니다.
Cache-Control: must-understand, no-store
must-understand를 no-store와 짝지으면 대비책이 생깁니다. must-understand를 지원하지 않는 캐시는 알 수 없는 디렉티브를 무시하고 대신 no-store를 따릅니다.
private
private 디렉티브는 저장을 개인 캐시, 보통 최종 사용자의 브라우저로 제한합니다. CDN과 프록시 서버 같은 공유 캐시는 응답을 버립니다.
Cache-Control: private
public
public 디렉티브는 응답이 공유 캐시에 저장될 수 있음을 표시합니다. Authorization 헤더를 담은 응답은 public 디렉티브가 없으면 공유 캐시가 저장하지 않습니다.
Cache-Control: public
참고
must-revalidate나 s-maxage가 이미 있으면 public 디렉티브는 불필요합니다.
immutable
immutable 디렉티브는 신선도 수명 동안 응답 본문이 바뀌지 않음을 보장합니다. 캐시는 immutable 리소스에 대해 조건부 요청을 건너뛰므로, 버전이 붙은 JavaScript 번들이나 핑거프린트가 찍힌 이미지 같은 애셋에서 재검증 왕복이 사라집니다.
Cache-Control: public, max-age=31536000, immutable
stale-while-revalidate
stale-while-revalidate 디렉티브는 오래된 응답을 쓸 수 있는 구간을 연장합니다. 신선도가 만료된 뒤에도 캐시는 백그라운드에서 재검증하면서 오래된 사본을 제공합니다. 매개변수는 그 오래된 응답을 몇 초 더 받아들일 수 있는지 정의합니다.
Cache-Control: max-age=600, stale-while-revalidate=30
백그라운드 재검증이 끝나면 캐시는 오래된 항목을 새 응답으로 교체합니다. 이 패턴은 재검증 지연을 최종 사용자에게서 감춥니다.
stale-if-error
stale-if-error 디렉티브는 오리진이 오류 상태(500, 502, 503, 504)를 반환하거나 접근 불가일 때 캐시가 오래된 응답을 제공하도록 허용합니다. 매개변수는 신선도 수명을 넘어 그 오래된 응답을 대비책으로 쓸 수 있는 초 수를 설정합니다.
Cache-Control: max-age=600, stale-if-error=86400
예시
긴 신선도 수명과 immutable 플래그로 제공되는 정적 애셋입니다. 브라우저와 모든 공유 캐시는 이 응답을 재검증 없이 1년 동안 저장합니다.
Cache-Control: public, max-age=31536000, immutable
요청한 브라우저만을 위한 API 응답입니다. 캐시는 응답을 5분 동안 저장하고, 오래되면 재사용 전에 재검증합니다.
Cache-Control: private, max-age=300, must-revalidate
공유 캐시에는 10분, 브라우저 캐시에는 1분의 구간을 주는 CDN 대상 정책입니다. CDN이 백그라운드에서 재검증하는 동안 오래된 응답을 최대 30초까지 제공합니다.
Cache-Control: s-maxage=600, max-age=60, stale-while-revalidate=30
Googlebot과 max-age
Google의 크롤링 인프라는 휴리스틱 HTTP 캐싱을 구현합니다. max-age 디렉티브는 Googlebot이 재크롤 빈도를 정하는 데 도움을 줍니다. max-age가 긴 페이지는 덜 자주 다시 가져오고, no-cache는 콘텐츠가 자주 바뀐다는 신호여서 더 잦은 크롤링을 부릅니다. 캐시 헤더가 전혀 없는 응답도 특정 상태 코드에서는 기본적으로 휴리스틱하게 캐시될 수 있으므로, 캐싱을 완전히 막으려는 오리진은 no-store를 명시적으로 보내야 합니다. Bingbot은 요청에 Cache-Control: no-cache를 보내고, 크롤 속도 조절에는 robots.txt의 crawl-delay와 IndexNow에 의존합니다.
참고
SEO 및 캐싱 관련 도움이 필요하면 전 Google SEO 컨설턴트인 Search Brothers에 문의하십시오.
문제 해결
예기치 않은 캐싱 동작은 디렉티브에 대한 오해, CDN의 덮어쓰기, 또는 서로 충돌하는 헤더에서 비롯됩니다.
no-cache 디렉티브가 있는데도 오래된 콘텐츠가 제공됩니다. no-cache 디렉티브는 저장을 막지 않습니다. 캐시는 응답을 저장하고 재사용할 때마다 재검증합니다. 저장 자체를 막으려면 no-store를 사용하십시오. 민감한 데이터가 관련되었다면 둘을 함께 쓰십시오. Cache-Control: no-cache, no-store 처럼 씁니다.
CDN이 Cache-Control 디렉티브를 무시합니다. 많은 CDN이 오리진의 Cache-Control을 자신의 정책으로 덮어씁니다. Cloudflare는 기본적으로 오리진 헤더를 존중하지만 Page Rule이나 Cache Rule이 활성화되면 이를 덮어씁니다. AWS CloudFront는 사용자 정의 TTL이 설정된 캐시 정책이 붙어 있지 않은 한 오리진 헤더를 사용합니다. 오리진 설정을 디버깅하기 전에 CDN 대시보드에서 덮어쓰기 규칙을 확인하십시오.
max-age를 낮춘 뒤에도 오래된 콘텐츠가 남아 있습니다. CDN 엣지 노드와 브라우저 캐시는 원래의 max-age가 만료될 때까지 응답을 유지합니다. 오리진에서 값을 낮춰도 이미 캐시된 사본은 비워지지 않습니다. 캐시 수명을 바꾼 뒤에는 CDN 퍼지를 실행하십시오. Cloudflare에서는 URL 단위로 퍼지하거나 전체 영역 퍼지를 사용하십시오. CloudFront에서는 해당 경로에 대한 무효화를 생성하십시오.
no-store인데도 뒤로/앞으로 이동 시 브라우저가 옛 페이지를 보여 줍니다. Chrome, Firefox, Safari의 뒤로-앞으로 캐시(bfcache)는 메모리에서 전체 페이지 상태를 복원하므로 no-store를 우회합니다. 이는 의도된 동작이며 캐시 위반이 아닙니다. DevTools의 Network 탭에서 “Disable cache”를 해제한 상태로 Cache-Control 헤더를 살펴 헤더가 브라우저에 도달하는지 확인하십시오. unload 이벤트 리스너를 추가하면 일부 브라우저에서 bfcache가 비활성화되지만 성능이 저하됩니다.
Expires 헤더가 Cache-Control과 충돌합니다. 두 헤더가 모두 있으면 Cache-Control: max-age가 Expires보다 우선합니다. 혼란을 피하려면 Expires 헤더를 제거하십시오. nginx에서는:
expires off;
add_header Cache-Control "max-age=3600";
Apache에서는:
Header unset Expires
Header set Cache-Control "max-age=3600"
curl과 DevTools로 캐시 헤더 진단하기. curl -I https://example.re 를 실행하여 오리진의 응답 헤더를 살펴보십시오. -H “Cache-Control: no-cache” 를 추가하면 CDN 캐시를 우회해 오리진에 직접 도달합니다. 브라우저 DevTools에서는 Network 탭을 열고 요청을 선택한 뒤 Response Headers 섹션을 확인하십시오. 응답이 CDN 엣지 노드에서 왔는지 오리진에서 왔는지 판단하려면 Age, X-Cache, CF-Cache-Status 헤더를 살펴보십시오.