CDN 뒤에 사이트를 올리고 나서 실제로 당한 캐시 함정 네 가지

배포는 성공했는데 의도한 대로 서빙되지 않는 일이 반복됐습니다. 검증 파일이 사라지고, 기능 플래그를 켜도 404가 남고, 지운 파일이 계속 응답했습니다. 각각의 원인과 확인 방법입니다.

해봄·

사이트를 CDN(CloudFront) 뒤에 올리고 몇 주 운영하면서, 배포가 성공했는데 의도한 대로 동작하지 않는 일을 네 번 겪었습니다. 넷 다 원인이 달랐고, 넷 다 처음에는 원인을 잘못 짚었습니다.

공통점이 하나 있습니다. 응답 코드만 보면 전부 정상이었습니다. 200이 나오는데 틀린 것이 나오고 있었습니다.

1. 검증 파일이 하루 종일 사라진다

광고 네트워크 콘솔에서 ads.txt 상태가 "승인됨"에서 "찾을 수 없음"으로 바뀌었습니다. 파일을 확인하니 멀쩡했습니다. 요구되는 문자열과 바이트 단위로 일치했고, 크롤러 UA로 요청해도 200이었습니다.

원인 후보로 캐시를 의심했습니다. 정적 파일 기본 캐시가 이렇게 잡혀 있었습니다.

cache-control: public, max-age=0, s-maxage=86400, stale-while-revalidate=86400

s-maxage=86400은 CDN 엣지가 24시간 들고 있겠다는 뜻입니다. 배포 중에 어느 엣지가 순간적으로 404를 한 번 캐시하면, 그 엣지로 오는 요청은 하루 종일 404를 받습니다. 제 요청은 정상 캐시를 가진 엣지에 걸려서 계속 200으로 보였고요.

확인 방법은 캐시 우회입니다. 쿼리스트링을 붙이면 캐시 키가 달라져서 오리진까지 갑니다.

curl -s -o /dev/null -w '%{http_code}' "https://example.com/ads.txt?cb=$RANDOM"

일반 요청은 200인데 이게 404나 403이면 캐시가 진실을 가리고 있는 겁니다.

덧붙이면, 이 건은 결국 캐시가 원인이 아니었습니다. 심사가 끝나자 저절로 정상으로 돌아왔습니다. 다만 검증 파일의 캐시를 24시간에서 5분으로 줄인 건 남겨뒀습니다. 같은 일이 생겨도 하루가 아니라 5분 안에 자가 복구되니까요.

2. 기능 플래그를 켰는데 목록 페이지만 404

기능을 데이터베이스 플래그로 가려뒀다가 켰습니다. 개별 페이지는 전부 200이 됐는데 목록 페이지만 404가 유지됐습니다.

/games/worktype   200   ← 개별은 됨
/games            404   ← 목록만 안 됨

플래그 캐시(60초)를 의심하고 기다렸는데 몇 분이 지나도 그대로였습니다.

원인은 캐시가 아니라 빌드였습니다. 빌드 산출물을 보니 목록 페이지가 정적으로 표시돼 있었습니다.

├ ○ /games      ← 정적
├ ● /games/[game]

빌드 시점에는 데이터베이스에 접근할 수 없습니다. 그러니 플래그가 전부 꺼진 값으로 평가되고, "표시할 항목이 0개니까 404"라는 결과가 HTML로 구워져 배포됩니다. 런타임에 플래그를 켜도 그 페이지는 이미 정해진 답을 들고 있습니다.

같은 이유로 사이트맵도 비어 있었습니다. 이걸 못 잡았으면 플래그를 켜도 검색엔진에는 아무것도 안 올라갔을 겁니다.

고치는 법은 해당 페이지를 동적으로 강제하는 것입니다.

export const dynamic = "force-dynamic";

교훈: 런타임 설정을 읽는 페이지는 정적으로 구우면 안 됩니다. 그리고 빌드 산출물의 정적/동적 표시를 한 번은 확인하세요.

3. 지운 파일이 계속 응답한다

정적 파일을 라우트로 옮기려고 public/ 에서 파일을 지우고 배포했습니다. 배포는 성공했고 응답도 200이었습니다.

그런데 응답 헤더를 보니 이랬습니다.

server: AmazonS3

라우트로 응답했다면 나올 수 없는 헤더입니다. 라우트가 호출조차 안 되고 있었습니다.

원인은 두 가지가 겹친 것이었습니다.

첫째, CDN이 확장자가 있는 경로를 정적 파일로 판단해서 스토리지로만 보냅니다. /ads.txt 처럼 점이 들어간 경로는 서버 함수까지 가지 않습니다. 그래서 같은 이름의 라우트를 만들어도 죽은 코드가 됩니다.

둘째, 배포 도구가 스토리지에 남은 객체를 지우지 않습니다. 로컬에서 파일을 지워도 이전 배포 때 올라간 객체는 그대로 남습니다. 그러니 계속 그 파일이 서빙됩니다.

더 나빴던 건 그다음입니다. 남은 객체를 직접 지웠더니 캐시 우회 요청이 403을 반환했습니다. 캐시가 만료되는 순간 검증 파일이 사라질 상태였던 겁니다. 일반 요청은 200이라 겉으로는 멀쩡해 보였습니다.

결국 원래대로 되돌렸습니다. 정적 파일을 지운 배포는 스토리지 정리를 따로 해야 하고, 확장자 경로는 라우트로 못 옮깁니다.

4. 배포 성공은 서빙 확인이 아니다

넷을 겪고 나서 정리된 게 이겁니다. 배포 로그가 초록색이고 응답이 200이어도, 의도한 경로로 서빙되고 있는지는 별개입니다.

정적/동적이 갈리는 변경을 했다면 응답 코드가 아니라 헤더를 봐야 합니다.

curl -s -D - -o /dev/null "https://example.com/경로" | grep -iE 'server|cache-control|x-cache|age'
헤더읽는 법
server: AmazonS3스토리지에서 나옴 = 정적 파일
x-cache: Hit from cloudfront캐시에서 나옴. 오리진 상태를 안 보여줌
x-cache: Miss from cloudfront오리진까지 갔음
age: 400400초 전에 캐시된 응답
s-maxage이 초만큼 엣지가 들고 있음

확인 순서

문제가 의심되면 이 순서로 봅니다.

  1. 캐시 우회로 오리진 상태 확인 — 쿼리스트링을 붙여 요청합니다. 여기서 다르면 캐시 문제입니다
  2. 응답 헤더 확인 — server와 x-cache로 어디서 나온 응답인지 봅니다
  3. 빌드 산출물 확인 — 정적으로 구워졌는지 봅니다. 런타임 설정을 읽는 페이지라면 여기가 원인일 확률이 높습니다
  4. 스토리지 직접 확인 — 지웠다고 생각한 파일이 남아 있는지 봅니다

정리

  • 캐시가 진실을 가립니다. 쿼리스트링 한 줄로 우회해서 오리진 상태를 보세요
  • 런타임 설정을 읽는 페이지를 정적으로 구우면 빌드 시점 값이 박힙니다
  • 확장자가 있는 경로는 서버 함수로 못 갑니다. 정적 파일로 두세요
  • 정적 파일을 지운 배포는 스토리지에 객체를 남깁니다
  • 배포 성공과 의도한 서빙은 다른 문제입니다. 헤더까지 보는 습관이 필요합니다

이 중 셋은 응답 코드만 봤으면 못 잡았을 것들입니다. 200이 나온다고 맞게 나오는 건 아닙니다.

이 글은 해봄 운영자가 직접 도구를 만들고 운영하며 확인한 내용을 정리한 것입니다. 사실이 아닌 부분을 발견하시면 알려 주세요.

이어서 읽을 글