두 개의 층, 두 종류의 ‘오래됨’
변경이 “안 보인다”고 할 때 질문은 두 개로 나뉘고, 답도 고치는 방법도 서로 다릅니다.
- 이름이 어느 서버로 안내하는가? DNS의 문제입니다. DNS가 오래됐다는 것은 방문자의 리졸버가 예전 레코드를 캐시하고 있어서 예전 서버로 보낸다는 뜻입니다.
- 그 서버가 무엇을 돌려주는가? HTTP 캐시의 문제입니다. 캐시가 오래됐다는 것은 방문자가 제대로 된 곳(CDN 엣지나 리버스 프록시)에 도착했는데, 그곳이 변경 전에 저장해 둔 사본을 내준다는 뜻입니다.
사람들이 잊는 세 번째 층도 있습니다. 방문자의 브라우저 자체입니다. HTTP 캐시, 서비스 워커, 기억해 둔 리다이렉트가 여기에 있습니다.
해결책은 서로 통하지 않습니다. CDN을 purge해도 사람들을 예전 호스트로 보내는 리졸버에는 아무 효과가 없습니다. DNS TTL을 기다려도 max-age=86400으로 캐시된 객체에는 아무 효과가 없습니다. 그러니 어느 층인지부터 찾아야 합니다.
하나 짚어 둘 것이 있습니다. 사이트가 CDN 프록시 뒤에 있다면 DNS는 아예 바뀌지 않는 경우가 많습니다. Cloudflare 프록시를 켠 레코드라면 원 서버가 어느 데이터센터에 있든 공개 응답은 Cloudflare 주소이고, 원 서버 변경은 엣지에 바로 적용됩니다. 그때 ‘CDN 전파’라고 부르는 것은 둘 중 하나입니다. CDN 엣지로 아직 배포 중인 설정 변경, 또는 아직 만료되지 않은 캐시 객체입니다.
1단계: DNS는 정리됐나
권한 네임서버가 말하는 값과 공개 리졸버가 캐시한 값을 비교합니다.
$ dig @ns1.dns-provider.example www.example.com A +norecurse +short
198.51.100.20
$ dig @1.1.1.1 www.example.com A +short
198.51.100.20
$ dig @8.8.8.8 www.example.com A +short
203.0.113.5
Google 리졸버에 아직 예전 주소가 남아 있습니다. 이것은 DNS이고, 그 캐시의 TTL이 끝나면 맞춰집니다(+short 없이 dig @8.8.8.8 www.example.com A를 하면 남은 초가 보입니다). 전파 점검은 같은 비교를 공개 리졸버 여섯 곳에 한 번에 해 주고, 얼마나 걸리는지는 전파 가이드에 정리돼 있습니다.
CDN 뒤에서는 IP 비교가 오해를 낳을 수 있습니다. CDN은 일부러 위치마다 다른 주소로 답하므로 리졸버끼리 답이 다른 것이 정상입니다. 대신 CNAME 대상을 비교하거나(dig www.example.com CNAME +short), 받은 IP가 모두 CDN 소유인지 확인하세요. 주소의 소유자는 ASN 조회로 알 수 있습니다. 모든 답이 CDN을 가리킨다면 DNS는 원인이 아닙니다.
2단계: DNS를 빼고 테스트하기
curl --resolve는 내가 고른 IP로 연결하면서도 올바른 호스트명과 SNI를 보내므로, 특정 서버에 직접 물어볼 수 있습니다.
# 새 원 서버에 직접, DNS와 CDN을 거치지 않고
curl -s --resolve www.example.com:443:198.51.100.20 https://www.example.com/ | grep -o '<title>.*</title>'
# 일반 방문자가 받는 것과 실제로 연결되는 IP
curl -sv -o /dev/null https://www.example.com/ 2>&1 | grep -iE 'connected to|^< (age|cache-control|cf-cache-status|x-cache)'
새 원 서버에 고정한 요청은 새 콘텐츠를, 일반 요청은 예전 콘텐츠를 준다면 차이는 그 사이에 있습니다. 일반 요청이 예전 IP로 연결됐다면 DNS이고, CDN으로 연결됐다면 캐시입니다. 새 원 서버에 고정한 요청도 예전 콘텐츠를 준다면 오래된 것은 아무것도 없습니다. 배포가 안 됐거나, 다른 서버로 갔거나, 애플리케이션 자체 캐시가 있는 것입니다.
원 서버가 CDN IP 대역의 연결만 받는다면, 원 서버 안에서 127.0.0.1을 대상으로 고정 요청을 해 보세요.
3단계: 캐시 헤더 읽기
캐시된 응답은 어디를 봐야 하는지만 알면 스스로 그렇다고 말해 줍니다.
$ curl -sI https://www.example.com/
HTTP/2 200
date: Wed, 07 Oct 2026 08:12:40 GMT
content-type: text/html; charset=utf-8
cache-control: public, max-age=3600
age: 2841
cf-cache-status: HIT
last-modified: Wed, 07 Oct 2026 07:20:02 GMT
Age는 표준 헤더로, 응답이 캐시에 머문 시간(초)입니다. 이 응답은 47분째 캐시돼 있고 max-age가 한 시간이므로, purge하지 않으면 이 엣지는 13분쯤 더 이 사본을 내줍니다. 다른 헤더가 무엇이라고 하든 Age가 0보다 크면 캐시에서 나온 사본입니다.
CDN마다 적중 여부를 자기 헤더로도 알려 줍니다.
| CDN | 헤더 | 볼 값 |
|---|---|---|
| Cloudflare | cf-cache-status | HIT, MISS, EXPIRED, STALE, UPDATING, REVALIDATED, BYPASS, DYNAMIC |
| Amazon CloudFront | x-cache | Hit from cloudfront, Miss from cloudfront, RefreshHit from cloudfront |
| Fastly | x-cache, x-served-by | HIT 또는 MISS, 캐시 계층마다 하나씩(MISS, HIT) |
| Vercel | x-vercel-cache | HIT, MISS, STALE, PRERENDER |
| RFC 9211을 따르는 캐시 | cache-status | 예: ExampleCDN; hit |
Akamai는 기본적으로 캐시 상태를 보여 주지 않습니다. 요청에 akamai-x-cache-on 같은 Akamai의 Pragma 디버그 값을 넣으면 디버그 헤더를 붙여 줍니다.
진단을 바꾸는 값이 두 가지 있습니다.
- **
cf-cache-status: DYNAMIC**은 Cloudflare가 응답을 캐시 대상으로 보지 않고 원 서버에서 가져왔다는 뜻입니다. Cloudflare는 기본적으로 HTML을 캐시하지 않으므로 대부분의 페이지에서 이 값이 보입니다. 콘텐츠는 예전인데 상태가DYNAMIC이라면 CDN은 원인이 아닙니다. 원 서버나 브라우저를 보세요. STALE(넓게는stale-while-revalidate,stale-if-error동작)은 CDN이 만료된 사본을 일부러 내줬다는 뜻이고, 원 서버가 응답하지 못해서인 경우가 많습니다. “변경이 배포되지 않았다”처럼 보이지만 실제로는 “원 서버가 죽었다”입니다. 원 서버를 직접 테스트하세요.
HTTP 점검은 내 네트워크 밖에서 URL을 가져와 모든 응답 헤더를 보여 주므로, 터미널 없이 Age, cache-control, CDN 상태 헤더를 볼 수 있습니다. 다만 관측 지점은 한 곳입니다. CDN에는 엣지가 많고, 다른 엣지에는 더 오래되거나 더 새로운 사본이 있을 수 있습니다.
빠른 추가 테스트도 하나 있습니다. 아무도 쓴 적 없는 쿼리 문자열을 붙여 보세요. ?nocache=8231 같은 것입니다. 이것으로 새 콘텐츠가 나온다면 전체 URL을 키로 쓰는 캐시가 예전 사본을 주고 있던 것입니다. 그래도 예전 콘텐츠라면 캐시가 쿼리 문자열을 무시하도록 설정돼 있거나(CDN에서 흔한 설정입니다) 캐시가 관여하지 않는 것입니다.
4단계: 브라우저 배제하기
curl은 새 버전을 받는데 브라우저는 예전 화면이라면, 오래된 사본은 내 컴퓨터에 있습니다.
- HTTP 캐시. 개발자 도구의 Network 탭에서 “Disable cache”를 켜고 새로고침하세요. 브라우저 밖으로 나가지 않은 응답은 Size 열에
(memory cache)나(disk cache)로 표시됩니다. - 서비스 워커. 오프라인 지원이나 PWA 기능이 있는 사이트는 서비스 워커의 캐시에서 페이지를 내줄 수 있습니다. Size 열에
(ServiceWorker)로 나옵니다. 개발자 도구 → Application → Service Workers에서 등록을 해제하거나 네트워크로 우회하게 할 수 있습니다. - 캐시된 리다이렉트. 브라우저는
301리다이렉트를 오래 기억합니다. 리다이렉트를 없앴는데 브라우저가 계속 따라간다면 이것입니다. 시크릿 창은 지금의 실제 동작을 보여 줍니다.
세 가지 모두 시크릿 창이 가장 빠른 확인 방법입니다. 일반 프로필의 캐시, 쿠키, 서비스 워커 없이 시작하기 때문입니다.
한 표로 정리
| 권한 DNS | 공개 리졸버 | 새 원 서버 고정 요청 | 일반 요청 | 오래된 층 |
|---|---|---|---|---|
| 새 값 | 일부 예전 값 | 새 콘텐츠 | 일부 사용자에게 예전 콘텐츠 | DNS. 예전 TTL만큼 기다립니다. |
| 새 값 | 전부 새 값(또는 전부 CDN) | 새 콘텐츠 | 예전 콘텐츠, Age > 0, 캐시 HIT | CDN이나 프록시 캐시. purge합니다. |
| 새 값 | 전부 새 값(또는 전부 CDN) | 예전 콘텐츠 | 예전 콘텐츠 | 오래된 것은 없음. 원 서버가 예전 콘텐츠를 줍니다. 배포를 확인하세요. |
| 새 값 | 전부 새 값(또는 전부 CDN) | 새 콘텐츠 | curl은 새 콘텐츠, 브라우저는 예전 | 브라우저 캐시, 서비스 워커, 캐시된 리다이렉트. |
| 예전 값 | 예전 값 | 해당 없음 | 예전 콘텐츠 | 변경이 게시되지 않았거나, 실제로 쓰이지 않는 zone에서 수정했습니다. 네임서버 불일치 가이드를 보세요. |
purge할 것인가, 기다릴 것인가
DNS: 기다립니다. 남의 리졸버는 비울 수 없습니다. Cloudflare와 Google은 1.1.1.1과 8.8.8.8에서 이름 하나를 비우는 페이지를 제공하지만, 그 두 리졸버 사용자에게만 도움이 됩니다. 실제 한계는 예전 레코드의 TTL이고, 그래서 TTL 가이드는 계획된 변경 하루 전에 TTL을 낮추라고 권합니다.
CDN: purge하되 좁게 합니다. CDN은 객체의 신선도 기간이 끝나기 전에는 갱신하지 않고, 그 기간이 며칠일 수도 있습니다. 전체를 비우기보다 바꾼 URL, 경로 접두사, 캐시 태그만 purge하세요. ‘전체 purge’는 모든 요청을 한꺼번에 원 서버로 보내므로, 이미 힘든 원 서버에 할 일이 아닙니다.
# Cloudflare: 특정 URL purge
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://www.example.com/","https://www.example.com/app.css"]}'
# CloudFront: 경로 무효화
aws cloudfront create-invalidation --distribution-id E1EXAMPLE --paths "/" "/app.css"
purge가 모든 엣지에 닿기까지는 조금 걸리고, CloudFront는 매달 무료 한도를 넘는 무효화 경로에 요금을 받습니다. 끝나면 응답 헤더를 다시 보세요. 첫 요청은 MISS, 다음 요청은 작은 Age와 함께 HIT여야 합니다.
브라우저: purge할 수 없습니다. 내가 보낸 Cache-Control을 방문자의 브라우저가 그대로 따릅니다. app.js를 max-age=31536000으로 내보냈다면 재방문자는 1년 동안 그것을 씁니다. 오래가는 해결책은 파일 이름과 캐시 정책에 있습니다.
- 정적 파일 이름에 내용 해시를 넣고(
app.3f9a1c.js) 오래 캐시하세요. 파일이 바뀌면 이름도 바뀝니다. - HTML은 짧은
max-age나Cache-Control: no-cache로 내보내세요.no-cache는 “저장하지 말라”가 아니라 “쓰기 전에 다시 확인하라”는 뜻입니다. 그러면 새 배포가 다음 페이지 로드에 바로 보입니다.
헤더는 캐시·압축 점검 가이드에서 더 자세히 다룹니다.
DNS와 CDN이 함께 바뀐 경우
사이트를 CDN 뒤로 옮기거나, CDN 뒤의 원 서버를 옮기면 두 층이 한꺼번에 바뀝니다. 나눠서 보세요.
- CDN 뒤로 옮기기는 DNS 레코드를 바꿉니다(CNAME이나 CDN IP로). 이 부분은 예전 레코드의 TTL을 따릅니다. 트래픽이 들어오기 시작하면 CDN 캐시는 비어 있으므로 처음에는
MISS가 나오고 원 서버 부하가 늘어날 것을 예상하세요. - 같은 CDN 뒤에서 원 서버 옮기기는 대개 공개 DNS를 바꾸지 않습니다. CDN이 원 서버에 IP로 접근한다면(예: Cloudflare 프록시 A 레코드) 전환은 즉시입니다. 호스트명으로 접근한다면 CDN이 그 호스트명을 직접 조회해 TTL 동안 캐시하므로, 옮기기 전에 그 TTL을 낮추세요.
- CDN 설정 변경(규칙, 원 서버, behavior)은 엣지로 따로 배포됩니다. CloudFront는 그동안 배포를 “Deploying”으로 표시합니다. 이 사이에는 엣지마다 다르게 동작하므로 전파처럼 보이지만 DNS는 아닙니다.
무엇을 purge할지, 얼마나 기다릴지 정하기 전에 위 명령으로 한 층씩 테스트하세요.
관련 가이드
- DNS 전파 지연: DNS 변경이 얼마나 걸리고 리졸버별로 어떻게 확인하는지.
- TTL 설정 가이드: TTL 고르기와 변경 전 낮추기.
- 네임서버 불일치: 권한 응답 자체가 틀렸을 때.
내 도메인에서 바로 확인
무료, 가입 불필요. 이 가이드가 다루는 항목을 바로 검사하고 조치 방법을 알려드립니다.