2026-09-28 · Evan · Engineering

캐시가 PASS를 FAIL로 속인다: 기술블로그 발행 직후 stale 검증 우회 실측기

발행 직후 sitemap에는 있고 /blog에는 없는 stale 캐시 구간을 가려내는 공개 검증 판정 순서를 정리한 실측기

#cache#cdn#sitemap#tech-blog#verification

# 캐시가 PASS를 FAIL로 속인다: 기술블로그 발행 직후 stale 검증 우회 실측기

매일 18:00 KST에 발행하고 sitemap과 /blog를 확인하는 루틴이 있다. 어느 날은 DB 검증이 전부 PASS인데 공개 검증만 FAIL이 뜬다. sitemap에는 당일 slug가 있고 /blog HTML에는 없다. 다시 요청하면 이번에는 있다. 본문도 DB도 건드리지 않았는데 결과가 뒤집힌다. 범인은 코드가 아니라 캐시다. 이 글은 발행 직후 검증이 stale 캐시에 속는 구조와 우회법을 정리한 기록이다.

## 1. 문제: 같은 URL인데 요청마다 답이 다르다

발행 완결의 검증 체인은 publish → get → list → 공개 확인(sitemap.xml·/blog 서버 HTML) 순서다. DB 단계가 끝나면 공개 단계에서 당일 slug가 두 곳에 직접 포함됐는지 본다. 그런데 CDN·엣지 캐시나 ISR 재생성 주기가 끼면, 같은 URL을 같은 판정 스크립트로 두 번 요청해도 결과가 달라진다. 첫 요청은 발행 전 캐시된 HTML을 받고, 두 번째 요청은 재생성된 HTML을 받는다.

이때 FAIL 판정을 그대로 믿고 재발행하면 중복 발행·slug 충돌 같은 이차 결함이 생긴다. 반대로 캐시를 의심하지 않고 FAIL을 무시하면 실제 미반영을 놓친다. 둘을 가르는 판정이 필요하다.

## 2. 원인: 세 개의 시계가 따로 돈다

발행 직후에는 세 개의 갱신 시점이 따로 돈다.

1. DB 쓰기: publish 호출 시점에 즉시 반영된다. get·list로 바로 확인된다.
2. sitemap.xml: 빌드·재생성 시점에 따라 늦게 반영된다. DB에는 있는데 sitemap에 없는 구간이 생긴다.
3. /blog 목록 HTML: 엣지 캐시 TTL 안에서는 발행 전 버전이 계속 나간다. sitemap에는 있는데 /blog에는 없는 구간이 생긴다.

과거 실측에서도 DB 14편이 sitemap 9건·서버렌더 0건으로 미반영된 적이 있었다. 그때 원인이 클라이언트 전용 렌더였듯, 이번에는 캐시가 같은 종류의 간극을 만든다. DB PASS가 공개 PASS를 증명하지 못한다는 교훈은 그대로 유효하다.

## 3. 실측: stale인지 진짜 FAIL인지 가리는 세 단계

검증된 판정 순서는 다음과 같다.

1. sitemap부터 본다. sitemap에 당일 slug가 있으면 DB→정적 생성까지는 도달한 것이다. sitemap에도 없으면 캐시 탓이 아니라 아직 재생성 전이거나 발행 자체 문제다. 기다렸다가 다시 본다.
2. /blog는 캐시 우회 조건으로 재요청한다. 쿼리스트링을 붙이거나 no-cache 헤더로 엣지 캐시를 피하고 서버 HTML에 당일 slug href가 직접 포함됐는지 확인한다. 우회 요청에서 보이면 stale이었고, 우회해도 안 보이면 진짜 미반영이다.
3. 상세 페이지로 닫는다. `/blog/<slug>/`에 직접 접근해 HTTP 200과 본문 렌더를 확인한다. 목록 캐시와 무관하게 단건 라우트가 살아 있음을 증명한다.

순서가 중요하다. /blog 일반 요청 FAIL 하나로 재발행을 결정하지 않는다.

## 4. 함정 두 가지

첫째, 우회 확인을 PASS로 착각하는 것이다. 캐시 우회로만 보이고 일반 요청에서 계속 안 보이면 독자에게는 여전히 미노출이다. 우회 PASS는 "stale 확정"이지 "완결"이 아니다. TTL 만료 후 일반 요청으로 한 번 더 확인해야 한다.

둘째, 재시도 때마다 시각을 다시 찍는 것이다. publishedAt을 `now()`로 갱신하면 같은 slug 재사용의 동일 내용 조건이 깨져 멱등 발행이 실패한다. 시각은 KST→UTC 환산 고정값(18:00 KST면 `T09:00:00.000Z`)으로 최초 시도 값을 유지한다.

## 5. 결론: 판정 한 줄

sitemap → 캐시 우회 /blog → 상세 200 순서로 보고, 우회 PASS는 stale로 기록하고 TTL 후 일반 요청으로 닫는다. 이 한 줄을 지키면 거짓 FAIL에 재발행하지도, 진짜 미반영을 놓치지도 않는다.

← 글 목록으로