정적 파일 CDN 캐시가 배포 후에도 안 바뀌는 이유와 헤더 해결법

정적 파일 CDN

결론부터 말씀드리면, 정적 파일 CDN 캐시가 배포 후에도 그대로 남아있는 문제는 대부분 응답 헤더의 max-age 값과 CDN 엣지 캐시가 아직 만료되지 않았기 때문에 생깁니다. 파일 성격에 맞춰 Cache-Control을 다르게 설정하고, 배포 스크립트에 CDN 캐시 무효화 호출을 넣으면 대부분 해결됩니다. 이 글에서는 캐시가 안 바뀌는 구조적 원인부터, 실제로 쓸 수 있는 헤더·무효화 설정, 그리고 설정을 고쳐도 반영이 안 될 때 점검하는 순서까지 정리했습니다.

정적 파일 CDN 캐시가 안 바뀌는 근본 원인

정적 파일이 사용자에게 전달되기까지는 브라우저 캐시, CDN 엣지 캐시, 오리진 서버 캐시라는 세 단계를 거칩니다. 이런 분산 전달 구조를 콘텐츠 전송 네트워크라고 부르는데, 배포 후에도 파일이 안 바뀐다고 느껴지는 경우 대부분 이 중 두 번째 단계인 엣지 캐시가 원인입니다.

URL이 그대로인 채로 새 파일만 올리면, CDN은 max-age가 끝나기 전까지 오리진에 재요청할 이유가 없습니다. 실제로 헤더를 확인해 보면 원인이 바로 보입니다.

curl -I https://example.com/assets/app.js

HTTP/2 200
content-type: application/javascript
cache-control: public, max-age=31536000
etag: "5d8c72a5edd0d1d5"
age: 205300
x-cache: Hit from cloudfront

여기서 age 값 205300초는 약 57시간 전에 캐시된 응답이 계속 재사용되고 있다는 뜻입니다. x-cache가 Hit로 찍혀 있으면 요청이 오리진까지 가지도 않고 엣지에서 바로 응답한 것이므로, 오리진에 새 파일을 올려도 변화가 없는 게 정상적인 동작입니다.

Cache-Control 헤더, 이렇게 설정하면 됩니다

모든 정적 파일에 같은 캐시 정책을 쓰면 문제가 생깁니다. 해시가 붙은 자산 파일은 길게 캐시해도 되지만, 진입점 역할을 하는 HTML 파일은 배포 시점마다 최신 상태를 보장해야 합니다. Cache-Control 헤더의 max-age, s-maxage, no-cache, immutable 같은 지시자를 파일 종류에 따라 나눠 쓰는 방식이 일반적입니다.

location /assets/ {
    add_header Cache-Control "public, max-age=31536000, immutable";
}

location = /index.html {
    add_header Cache-Control "no-cache";
}

immutable 지시자는 브라우저가 새로고침을 해도 재검증 요청조차 보내지 않게 만듭니다. 파일명이 해시로 고정돼 있어 내용이 바뀌면 URL 자체가 바뀐다는 전제가 있을 때만 써야 안전합니다. index.html처럼 내용이 그때그때 바뀌는 파일에 immutable을 걸면, 배포해도 계속 예전 HTML이 나오는 사고로 이어집니다.

글로벌 CDN 엣지 서버 네트워크 인프라 구성도

배포 후 즉시 반영이 필요할 때는 어떻게 하나요?

헤더를 아무리 잘 나눠도, 이미 캐시된 max-age 31536000짜리 파일이 남아있으면 다음 배포까지 기다릴 수 없습니다. 이럴 때는 CDN 무효화 API를 배포 파이프라인 마지막 단계에 넣습니다.

CloudFront는 AWS CLI로 특정 경로만 지정해 무효화할 수 있습니다.

aws cloudfront create-invalidation \
  --distribution-id EDFDVBD6EXAMPLE \
  --paths "/index.html" "/assets/*"

전체 경로(/*)를 통째로 무효화하는 것보다 실제로 바뀐 경로만 지정하는 편이 처리 속도와 비용 면에서 유리합니다. AWS 공식 문서 기준으로 월 1,000개 경로까지는 무료로 제공되고, 이를 넘어가면 경로당 과금됩니다.

Cloudflare는 REST API로 특정 파일이나 전체 캐시를 퍼지할 수 있습니다.

curl -X POST "https://api.cloudflare.com/client/v4/zones/ZONE_ID/purge_cache" \
  -H "Authorization: Bearer $CF_API_TOKEN" \
  -H "Content-Type: application/json" \
  --data '{"files":["https://example.com/assets/app.js"]}'

CDN별 캐시 무효화 방식 비교

CDN마다 무효화 방식과 전파 속도, 비용 구조가 다릅니다. 아래는 각 서비스의 공식 문서에서 설명하는 방식 기준으로 정리한 비교입니다.

CDN 무효화 방법 전파 속도(공식 문서 설명 기준) 비용
CloudFront Invalidation API 보통 수 분 내 전체 엣지 반영 월 1,000경로 무료, 초과분 경로당 과금
Cloudflare Purge Cache API 대부분 짧은 시간 내 전파(Free 플랜도 지원) Free 플랜 포함 기본 무료
Fastly Instant Purge / Surrogate Key 밀리초~초 단위로 반영된다고 설명 플랜별 상이

개발자가 브라우저에서 HTTP 응답 헤더를 확인하며 캐시 문제를 디버깅하는 모습

Fastly는 Surrogate-Key 헤더로 관련 파일을 태그로 묶어 한 번에 무효화하는 방식을 지원하는데, 개별 URL을 하나씩 지정하는 CloudFront나 Cloudflare 방식보다 대규모 사이트에서 관리가 편합니다. 다만 Surrogate-Key를 쓰려면 오리진 응답에 해당 헤더를 직접 추가하는 작업이 선행돼야 합니다.

파일명에 해시를 넣는 방법과 그 한계

무효화 요청을 매번 보내는 대신, 파일 내용이 바뀌면 URL 자체가 바뀌게 만드는 방법도 있습니다. Vite나 Webpack 같은 번들러는 기본적으로 빌드 산출물 파일명에 콘텐츠 해시를 붙여줍니다.

// vite.config.js
export default {
  build: {
    rollupOptions: {
      output: {
        entryFileNames: 'assets/[name].[hash].js',
        chunkFileNames: 'assets/[name].[hash].js',
        assetFileNames: 'assets/[name].[hash][extname]'
      }
    }
  }
}

이 방식은 app.abc123.js처럼 파일명이 바뀌므로 immutable에 가까운 긴 max-age를 걸어도 안전하고, 별도 무효화 요청 없이도 새 파일이 즉시 서비스됩니다. 다만 한계가 명확합니다. 해시 파일명은 빌드 시스템 안에서 참조되는 자산에만 적용되고, index.html처럼 그 자산들을 불러오는 진입점 파일 자체는 여전히 캐시 문제에서 자유롭지 않습니다. 또한 배포할 때마다 새 해시 파일이 CDN·스토리지에 계속 쌓이므로, 오래된 버전을 정리하는 별도 정책이 없으면 스토리지 비용이 늘어나는 트레이드오프가 있습니다.

설정을 고쳤는데도 안 바뀐다면 이 순서로 점검해 보세요

헤더도 고치고 무효화 요청도 보냈는데 여전히 예전 파일이 나온다면, 아래 순서로 원인을 좁혀가는 게 효율적입니다.

  1. curl -I로 실제 응답 헤더를 확인해 x-cache 또는 cf-cache-status가 HIT인지 MISS인지 봅니다.
  2. 브라우저 캐시 문제일 수 있으니 시크릿 창에서 다시 요청해 봅니다. CDN은 정상인데 브라우저 디스크 캐시만 남아있는 경우가 실제로 자주 있습니다.
  3. CLI나 대시보드에서 무효화 요청 상태가 Completed로 끝났는지 확인합니다. 요청만 보내고 완료 전에 테스트하면 여전히 예전 응답이 나옵니다.
  4. 회사 방화벽이나 통신사 프록시 같은 중간 캐시 서버가 껴 있는 환경이면, 오리진과 CDN 헤더를 아무리 바꿔도 그 프록시 자체 캐시까지는 건드릴 수 없습니다. 이런 경우는 Cache-Control 설정만으로 해결되지 않는, 이 방법이 통하지 않는 대표적인 예외 상황입니다.

헤더 설정과 무효화 자동화, 두 가지를 같이 해야 끝납니다

정적 파일 CDN 캐시가 배포 후에도 안 바뀌는 문제는 결국 헤더 설정과 무효화 시점, 두 가지를 같이 관리해야 완전히 풀립니다. 해시 파일명으로 자산은 길게 캐시하고, 진입점 HTML은 no-cache로 짧게 유지한 뒤, 배포 스크립트 마지막 줄에 CloudFront나 Cloudflare 무효화 API 호출을 추가하는 것을 첫 단계로 추천합니다. 그 다음에는 curl -I로 헤더를 직접 확인하는 습관을 들이면, 캐시 때문인지 다른 배포 문제인지 훨씬 빨리 구분할 수 있습니다.

이 글에서 다룬 Cache-Control 헤더의 각 지시자별 정확한 의미와 기본값이 궁금하다면 MDN 문서에서 직접 확인하실 수 있습니다. 아래 링크를 참고해 주세요: Cache-Control

Alembic 마이그레이션이 프로덕션에서 실패했을 때, 되돌리는 순서

Leave a Comment