1. 라이브러리
  2. HTTP와 웹
  3. HTTP 헤더

업데이트됨 1개월 전

브라우저가 리소스를 가져올 때마다, 항상 같은 질문을 합니다: 이미 이걸 갖고 있나?

있다면 네트워크를 완전히 건너뛸 수 있습니다—지연 없이, 대역폭 소모 없이, 즉각적인 응답. 없다면 기다려야 합니다. 페이지 로딩이 100밀리초냐 3초냐의 차이는, 그 질문에 얼마나 잘 미리 대답해 두었느냐에 달려 있는 경우가 많습니다.

Cache-Control은 그 대답을 하는 방법입니다.

핵심 개념

캐싱은 시간에 거는 내기입니다. max-age=3600을 설정하면 이렇게 말하는 것입니다: "이 응답은 앞으로 한 시간 동안 유효합니다. 믿어도 됩니다." max-age=31536000을 설정하면: "이건 일 년 동안 유효합니다."

모든 max-age 값은 하나의 내기입니다: 이 사실이 얼마나 오래 유효할까?

맞게 설정하면 애플리케이션이 즉각적으로 느껴집니다. 틀리게 설정하면 사용자는 오래된 데이터를 보게 됩니다—더 나쁜 경우, 변경되지도 않은 콘텐츠를 다시 받아오느라 대역폭을 낭비하게 됩니다.

문법

Cache-Control은 HTTP 응답(그리고 가끔은 요청)에 포함됩니다. 여러 지시어는 쉼표로 구분합니다:

Cache-Control: public, max-age=86400, immutable

각 지시어는 캐싱 방법에 관한 서로 다른 질문에 답합니다.

누가 캐시할 수 있나? (public vs. private)

public: 누구나 캐시할 수 있습니다—브라우저, CDN, 기업 프록시, 인터넷 서비스 제공업체.

Cache-Control: public, max-age=86400

모든 사용자에게 동일한 콘텐츠에 사용하세요: 이미지, 스타일시트, JavaScript, 폰트.

private: 최종 사용자의 브라우저만 캐시할 수 있습니다. 공유 캐시는 저장해서는 안 됩니다.

Cache-Control: private, max-age=3600

사용자별 콘텐츠에 사용하세요: 대시보드, 계정 페이지, 개인화된 API 응답.

보안상 이 구분은 중요합니다. CDN이 사용자의 개인 데이터를 캐시해서 다른 사람에게 제공하면 보안 사고가 됩니다. private는 이를 방지합니다.

얼마나 오래? (max-age와 s-maxage)

max-age는 초 단위로 유효 기간을 설정합니다:

Cache-Control: max-age=3600

자주 사용되는 값:

  • 60 — 1분 (빠르게 변하는 콘텐츠)
  • 3600 — 1시간 (적당히 동적인 콘텐츠)
  • 86400 — 1일 (매일 업데이트되는 콘텐츠)
  • 604800 — 1주일
  • 31536000 — 1년 (버전이 지정된 정적 자산)

s-maxage는 공유 캐시에 한해 max-age를 재정의합니다:

Cache-Control: max-age=60, s-maxage=3600

브라우저는 1분, CDN은 1시간 캐시합니다. CDN 효율성과 브라우저 최신성을 모두 원할 때 유용합니다.

헷갈리는 지시어들

no-cache는 "캐시하지 마라"는 뜻이 아닙니다. "캐시는 하되, 사용하기 전에 나한테 확인해라"는 뜻입니다.

Cache-Control: no-cache

캐시는 응답을 저장하지만, 제공하기 전에 서버에 유효성 검사를 요청합니다. 변경되지 않았다면 서버는 "304 Not Modified"를 반환하고 캐시는 저장된 사본을 제공합니다. 변경되었다면 새로운 콘텐츠가 도착합니다.

이를 통해 아무것도 변경되지 않은 경우에는 캐싱의 이점을 누리면서도 항상 최신 콘텐츠를 보장받을 수 있습니다.

no-store는 정말로 캐시하지 말라는 뜻입니다:

Cache-Control: no-store

아무것도 저장되지 않습니다. 모든 요청이 원본 서버에 도달합니다. 민감한 데이터에 사용하세요: 은행 거래, 의료 기록, 비밀번호.

재검증 지시어

must-revalidate: 유효 기간이 지나면 반드시 재검증합니다. 서버에 연결할 수 없더라도 만료된 콘텐츠를 제공하지 않습니다.

Cache-Control: max-age=3600, must-revalidate

이 지시어가 없으면 일부 캐시는 원본 서버에 접근하지 못할 때 만료된 콘텐츠를 그냥 제공할 수도 있습니다. 있으면 대신 오류를 반환합니다.

immutable: 이 콘텐츠는 절대 변경되지 않습니다. 재검증은 필요 없습니다.

Cache-Control: max-age=31536000, immutable

app.v2.4.1.js처럼 버전이 지정된 자산에 딱 맞습니다. 콘텐츠가 변경되면 파일 이름이 바뀌므로 URL이 사실상 콘텐츠 주소가 됩니다. 브라우저는 새로 고침 시에도 재검증을 완전히 건너뛸 수 있습니다.

검증 작동 방식

캐시된 콘텐츠의 유효 기간이 지나면, 브라우저는 물어봅니다: "이게 아직 유효한가요?"

두 가지 메커니즘이 이를 효율적으로 만듭니다:

ETag는 콘텐츠의 지문입니다:

# 응답
ETag: "a1b2c3d4"
Cache-Control: max-age=3600

# 재검증 요청
If-None-Match: "a1b2c3d4"

# 변경되지 않은 경우: 304 Not Modified (본문 없음)
# 변경된 경우: 200 OK + 새 콘텐츠 + 새 ETag

Last-Modified는 타임스탬프를 사용합니다:

# 응답
Last-Modified: Wed, 21 Oct 2024 07:28:00 GMT

# 재검증 요청
If-Modified-Since: Wed, 21 Oct 2024 07:28:00 GMT

ETag가 더 정밀합니다—콘텐츠 해시를 기반으로 할 수 있습니다. Last-Modified는 1초 단위의 정밀도를 가지며 1초 미만의 변경은 감지하지 못합니다.

실용적인 전략

버전이 지정된 정적 자산

Cache-Control: public, max-age=31536000, immutable

파일 이름: styles.a8f3b2.css

영구적으로 캐시합니다. 콘텐츠가 변경되면 파일 이름이 바뀌고, 브라우저는 새 URL을 가져옵니다. 이것이 정적 자산 캐싱의 정석입니다.

HTML 문서

Cache-Control: no-cache

HTML은 다른 리소스를 참조합니다. 너무 오래 캐시되면 사용자가 오래된 자산을 가리키는 낡은 HTML을 볼 수도 있습니다. 최신성을 보장하려면 유효성 검사를 요구하세요.

API 응답

어느 정도의 오래됨을 허용할 수 있는 데이터의 경우:

Cache-Control: private, max-age=300

5분 캐싱은 서버 부하를 상당히 줄여줍니다.

실시간 데이터의 경우:

Cache-Control: no-cache

민감한 데이터의 경우:

Cache-Control: no-store

사용자가 업로드한 이미지

Cache-Control: public, max-age=604800

거의 변경되지 않는 콘텐츠에는 1주일 캐싱이 잘 맞습니다.

흔한 실수들

오류 응답 캐싱: 1시간 동안 캐시된 500 오류는 1시간 동안의 불량 경험을 의미합니다. 오류에는 no-store를 사용하세요.

사용자별 콘텐츠에 public 사용: 공유 캐시를 통해 개인 데이터가 유출될 수 있습니다. 개인화된 응답에는 항상 private를 사용하세요.

no-cache가 캐싱을 방지한다는 착각: 그렇지 않습니다. no-cache는 유효성 검사를 요구합니다. 캐싱 자체를 막으려면 no-store를 사용하세요.

Cache-Control 헤더 생략: 명시적인 헤더 없이는 브라우저와 프록시마다 캐싱 동작이 예측 불가능하게 달라집니다. 항상 명시적으로 설정하세요.

디버깅

브라우저 개발자 도구(Network 탭)에서 Size 열을 보면 캐시 상태를 알 수 있습니다:

  • "45.2 KB"와 같은 숫자: 네트워크에서 가져옴
  • "(disk cache)": 디스크 캐시에서 제공됨
  • "(memory cache)": 메모리 캐시에서 제공됨

어떤 요청이든 클릭하면 Cache-Control 헤더를 확인할 수 있습니다.

"Disable cache" 체크박스는 개발 중에 캐싱을 우회합니다—캐시를 직접 지우지 않고도 변경 사항을 테스트할 때 유용합니다.

Cache-Control 헤더에 관한 자주 묻는 질문

no-cache와 no-store의 차이는 무엇인가요?

no-cache는 캐싱을 허용하지만 매번 사용하기 전에 유효성 검사를 요구합니다—캐시가 제공하기 전에 "이게 아직 유효한가요?"라고 확인하는 것입니다. no-store는 캐싱 자체를 완전히 금지합니다. 최신성 보장과 함께 효율성을 원한다면 no-cache를 사용하세요. 절대 저장되어서는 안 되는 민감한 데이터에는 no-store를 사용하세요.

캐시된 파일의 새 버전을 브라우저가 가져오게 하려면 어떻게 하나요?

URL을 바꾸세요. 가장 확실한 방법은 버전이 지정된 파일 이름입니다: app.v2.jsapp.v3.js가 됩니다. 또는 쿼리 문자열을 사용할 수도 있습니다: app.js?v=2app.js?v=3이 됩니다. 캐시는 서로 다른 URL을 서로 다른 리소스로 취급합니다.

max-age=0과 no-cache 중 어떤 걸 써야 하나요?

비슷하지만 완전히 같지는 않습니다. max-age=0은 콘텐츠를 즉시 만료된 것으로 표시하여 재검증을 요구합니다. no-cache는 신선도에 관계없이 검증을 요구합니다. 실제로는 둘 다 재검증을 강제하지만, no-cache가 의도를 더 명확히 드러냅니다.

Cache-Control과 Expires 헤더가 충돌하면 어떻게 되나요?

Cache-Control이 우선합니다. 두 헤더가 모두 있을 때 max-ageExpires보다 우선합니다. Expires는 구형 HTTP/1.0 클라이언트와의 호환성을 위해서만 사용하세요.

인증된 API 응답을 안전하게 캐시하려면 어떻게 해야 하나요?

Cache-Control: private, max-age=N을 사용하세요. private 지시어는 최종 사용자의 브라우저만 응답을 캐시하도록 보장합니다—CDN과 같은 공유 캐시는 저장하지 않습니다. 인증된 콘텐츠에는 절대 public을 사용하지 마세요.

이 페이지가 도움이 되었나요?

😔
🤨
😃
Cache-Control 헤더 • 라이브러리 • Connected