
정적 사이트를 하나 운영하면서 지키는 규칙이 있다. 배포가 끝나면 라이브 파일을 받아서 내 빌드 결과와 바이트 단위로 비교하고, 같을 때만 검색엔진에 "새 글이 있다"고 알린다. 덜 올라간 파일이나 캐시에 남은 옛 버전을 알리는 실수를 막으려는 장치다.
어느 날 이 비교가 실패했다. 파일을 다시 올려도 마찬가지였다. 차이는 딱 한 줄이었다.
라이브 HTML의 </body> 바로 앞에 내가 쓴 적 없는 <script>가 있었다. 호스팅하는 CDN의 웹 통계 기능이 응답을 내보낼 때 자동으로 넣는 비콘 스크립트다. 내 파일에는 없고, 서버가 내보내는 순간에만 붙는다.
크기로 보면 이렇다. 같은 주소를 두 번 받았다.
| 요청 | 받은 크기 |
|---|---|
Accept: text/html 헤더 있음 | 25,494바이트 |
| 헤더 없음(curl 기본) | 25,127바이트 |
| 내 빌드 결과 | 25,127바이트 |
367바이트, 스크립트 태그 한 줄이 그 차이였다.
처음 이 현상을 봤을 때는 "브라우저처럼 보이는 요청(User-Agent)에만 붙인다"고 메모해 뒀다. 이 글을 쓰려고 다시 재 보니 아니었다. User-Agent를 크롬 문자열로 바꿔도 헤더가 없으면 25,127바이트 그대로였고, User-Agent가 짧아도 Accept: text/html을 붙이면 25,494바이트가 됐다. 기준은 "HTML을 원하는 요청인가"였다.
선택지는 두 가지였다. 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이 넣는 내용이 바뀌면 다시 깨진다. 그래서 정규식을 넓히지 않고, 깨지면 그때 눈으로 확인하고 고치기로 했다. 검증이 너무 관대하면 검증이 아니게 되니까.
그리고 측정은 다시 해 보는 게 맞았다. "UA 때문"이라는 첫 메모를 그대로 믿었다면, 헤더 구성이 바뀌는 날 이유도 모른 채 또 실패했을 것이다.
이 검증을 거쳐 배포하는 사이트는 환불·위약금 기준을 고시와 표준약관 원문에 대조해 정리하는 곳이다. 글마다 대조한 판과 열람일을 적고, 분야별 기준은 한 페이지에 모아 두었다.