라이브와 빌드본이 한 줄 달랐다 — CDN이 끼워 넣은 통계 스크립트와 배포 검증

베리체·2026년 10월 20일
post-thumbnail

정적 사이트를 하나 운영하면서 지키는 규칙이 있다. 배포가 끝나면 라이브 파일을 받아서 내 빌드 결과와 바이트 단위로 비교하고, 같을 때만 검색엔진에 "새 글이 있다"고 알린다. 덜 올라간 파일이나 캐시에 남은 옛 버전을 알리는 실수를 막으려는 장치다.

어느 날 이 비교가 실패했다. 파일을 다시 올려도 마찬가지였다. 차이는 딱 한 줄이었다.

1. 끼어든 한 줄

라이브 HTML의 </body> 바로 앞에 내가 쓴 적 없는 <script>가 있었다. 호스팅하는 CDN의 웹 통계 기능이 응답을 내보낼 때 자동으로 넣는 비콘 스크립트다. 내 파일에는 없고, 서버가 내보내는 순간에만 붙는다.

크기로 보면 이렇다. 같은 주소를 두 번 받았다.

요청받은 크기
Accept: text/html 헤더 있음25,494바이트
헤더 없음(curl 기본)25,127바이트
내 빌드 결과25,127바이트

367바이트, 스크립트 태그 한 줄이 그 차이였다.

2. 처음엔 틀리게 이해했다

처음 이 현상을 봤을 때는 "브라우저처럼 보이는 요청(User-Agent)에만 붙인다"고 메모해 뒀다. 이 글을 쓰려고 다시 재 보니 아니었다. User-Agent를 크롬 문자열로 바꿔도 헤더가 없으면 25,127바이트 그대로였고, User-Agent가 짧아도 Accept: text/html을 붙이면 25,494바이트가 됐다. 기준은 "HTML을 원하는 요청인가"였다.

3. 고친 코드

선택지는 두 가지였다. CDN 쪽 통계 기능을 끄거나, 비교에서 그 한 줄만 빼거나. 통계는 계속 보고 싶었으니 두 번째로 갔다.

BEACON = re.compile(
    rb'<script[^>]*static[.]cloudflareinsights[.]com'
    rb'[^>]*></script>\s?')

def same_as_build(slug):
    local = open('dist/%s.html' % slug, 'rb').read()
    st, live = get('https://%s/%s?nc=%d' % (HOST, slug, os.getpid()))
    live = BEACON.sub(b'', live)
    return st == 200 and live == local

몇 가지를 일부러 이렇게 했다.

  • 지우는 범위를 좁게 잡았다. <script ...> 전체를 지우면 내가 넣은 스크립트가 빠져도 통과해 버린다. 도메인까지 맞는 태그 하나만 뺀다.
  • 바이트로 비교한다. 문자열로 디코딩하면 줄바꿈이나 인코딩 차이가 조용히 묻힌다.
  • 캐시를 피하려고 매번 다른 쿼리 문자열을 붙인다. 같은 주소를 연달아 받으면 CDN이 옛 응답을 줄 수 있다.

4. 남은 고민

이 방식은 CDN이 넣는 내용이 바뀌면 다시 깨진다. 그래서 정규식을 넓히지 않고, 깨지면 그때 눈으로 확인하고 고치기로 했다. 검증이 너무 관대하면 검증이 아니게 되니까.

그리고 측정은 다시 해 보는 게 맞았다. "UA 때문"이라는 첫 메모를 그대로 믿었다면, 헤더 구성이 바뀌는 날 이유도 모른 채 또 실패했을 것이다.

이 검증을 거쳐 배포하는 사이트는 환불·위약금 기준을 고시와 표준약관 원문에 대조해 정리하는 곳이다. 글마다 대조한 판과 열람일을 적고, 분야별 기준은 한 페이지에 모아 두었다.

profile
공개 데이터로 온라인 거래 피해를 기록합니다. 크롤링·DNS·데이터 정리 이야기를 씁니다.

0개의 댓글