판별 절차
형용사 대신 남이 따라 할 수 있는 절차로 적었습니다. 각 절차마다 그것으로 실제로 무엇을 확정했는지, 그리고 그 절차가 못 잡는 것이 무엇인지 같이 적습니다.
1400과 405로 경계를 만든다
언제 쓰나
"어제까진 됐는데 오늘 안 돼요." 원인 후보가 여러 개일 때, 로그를 뒤지기 전에 범위를 반으로 자를 때 씁니다.
절차
서버가 돌려주는 상태코드는 "실패했다"만 말하지 않습니다. 어디까지 도달했는지를 말합니다.
curl -s -o /dev/null -w '%{http_code}\n' \
-X POST https://<host>/api/<경로> \
-H 'Content-Type: application/json' -d '{}'
| 응답 | 뜻 | 다음에 볼 곳 |
|---|---|---|
400 | 경로도 있고 핸들러도 돌았다. 본문이 규격에 안 맞아 거절된 것 | 요청 본문·검증 규칙 |
405 | 경로는 있는데 그 메서드가 없다. 핸들러가 배포에서 빠졌다 | 배포 산출물·번들 설정 |
404 | 경로 자체가 없다 | 라우팅·파일 배치 |
200 | 도달했고 처리됐다. 그런데도 결과가 없으면 그다음 단계 문제 | 저장·알림 |
빈 본문 {} 을 일부러 보냅니다. 정상이라면 반드시 거절당해야 하므로, 거절 방식이 곧 진단이 됩니다.
이걸로 확정한 것
운영 중인 사이트에서 문의가 안 들어온다는 신고가 있었습니다. 화면은 멀쩡했습니다. 두 배포본에 같은 요청을 던졌습니다.
2일 전 배포 POST /api/lead → 400 핸들러 살아 있음
오늘 배포 POST /api/lead → 405 핸들러 없음
두 줄로 "언제부터, 무엇 때문에"가 확정됐습니다. 정적 파일만 배포하는 과정에서 서버 함수가 통째로 빠진 것이었고, 약 4시간 동안 접수가 끊겨 있었습니다. 이후 배포 검증을 절차로 고정했습니다 — 400이면 정상, 405면 배포 실패.
이 절차가 못 잡는 것
- 커스텀 도메인으로 재면 거짓말을 합니다. CDN 캐시 때문에 이미 사라진 것도 한동안
200을 줍니다. 배포본 URL로 재거나 20~30초 뒤에 다시 봐야 합니다. 하루에 네 번 속은 적이 있습니다. 200이 성공을 뜻하지 않습니다. SPA 폴백이 켜져 있으면 없는 경로도 200입니다(soft 404)./sitemap.xml이 200인데 내용이 HTML인 경우가 여기서 나옵니다.- 상태코드는 "도달했다"까지만 말합니다. 저장이나 알림이 조용히 실패하는 건 별도로 봐야 합니다.
2해시로 정본을 판정한다
언제 쓰나
"그 파일 어디 있죠?" / "고쳤는데 배포하니 예전 걸로 돌아갔어요." 사본이 여러 개일 때, 어느 것이 지금 라이브인지 눈으로는 알 수 없습니다.
절차
라이브에서 받은 것과 로컬 후보들을 같은 방식으로 해시 냅니다. 눈으로 비교하지 않습니다.
LIVE=$(curl -s "https://<host>/?cb=$RANDOM" | shasum -a 256 | cut -d' ' -f1)
for f in <후보1> <후보2> <후보3>; do
printf '%-40s %s\n' "$f" \
"$([ "$(shasum -a 256 "$f" | cut -d' ' -f1)" = "$LIVE" ] && echo 일치 || echo 다름)"
done
캐시 버스터를 꼭 붙입니다. 안 붙이면 캐시된 옛 응답과 옛 사본이 일치해 엉뚱한 결론이 납니다.
이걸로 확정한 것
운영 중인 사이트의 소스가 git 어디에도 없었습니다. 로컬 후보 8곳을 라이브와 대조했습니다.
후보 8곳 → 라이브와 일치: 0곳
어느 것도 정본이 아니었습니다. git 연동 없이 직접 업로드하는 방식이라 이력이 남지 않아 생긴 공백이었습니다. 이 상태에서는 누가 무엇을 고쳐도 다음 배포에 사라질 수 있습니다. 라이브 산출물을 해시로 검증해 저장소에 정본으로 고정하고, 배포를 단방향 스크립트로 못박았습니다.
이 절차가 못 잡는 것
- 크롤로 만든 사본은 사이트의 사본이 아닙니다. 해시가 같아도 서버 코드,
og:image처럼 메타태그에만 있는 자산,sitemap.xml에만 있는 미링크 페이지, CSSurl()안의 파일은 빠집니다. - 응답에 타임스탬프나 nonce 가 섞이면 매번 해시가 달라집니다. 그 필드를 지우고 비교해야 합니다.
- "일치하는 게 없다"까지만 알려줍니다. 어느 것을 정본으로 삼을지는 사람이 정해야 합니다.
3어절 쪼개짐을 센다
언제 쓰나
"뭔가 어색한데 뭐가 문제인지 모르겠어요." 대개 내용이 아니라 기본기입니다. 가장 흔한 것이 한글이 단어 중간에서 줄바꿈되는 것입니다.
저희는 사이트를 점검하고 개선 합니다
읽는 사람은 내용을 판단하기 전에 "검수가 안 됐구나" 를 먼저 느낍니다.
왜 생기나
브라우저 기본값 word-break: normal 은 CJK를 음절 단위로 끊어도 되는 것으로 취급합니다. 라틴 문자는 공백에서만 끊기지만 한글은 아무 글자 사이에서나 끊깁니다.
body { word-break: keep-all; overflow-wrap: break-word; }
overflow-wrap: break-word 는 URL 처럼 끊을 수 없는 긴 토큰이 넘칠 때만 쓰이는 안전장치입니다. anywhere 를 쓰면 keep-all 을 무시하고 도로 끊깁니다.
절차
눈으로 세지 않습니다. Intl.Segmenter 로 어절을 자르고, 각 어절의 getClientRects() 가 두 줄에 걸치는지 봅니다. 걸치면 쪼개진 것입니다.
(() => {
const seg = new Intl.Segmenter('ko', { granularity: 'word' });
let checked = 0, broken = [];
const tw = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
let n;
while ((n = tw.nextNode())) {
const t = n.nodeValue;
if (!t || !/[가-힣]/.test(t)) continue;
const p = n.parentElement;
if (!p || !p.offsetParent) continue;
const r0 = p.getBoundingClientRect();
if (r0.right < 0 || r0.width <= 1) continue; // 화면 밖·허니팟 제외
for (const s of seg.segment(t)) {
if (!s.isWordLike || !/[가-힣]/.test(s.segment) || s.segment.length < 2) continue;
checked++;
const r = document.createRange();
r.setStart(n, s.index);
r.setEnd(n, s.index + s.segment.length);
if (r.getClientRects().length > 1) broken.push(s.segment);
}
}
return { checked, broken };
})()
390 · 768 · 1280 · 1440px 네 폭에서 각각 0 이어야 통과입니다. 좁은 화면일수록 잘 드러납니다.
이걸로 확정한 것
운영 중인 사이트 네 곳과 예시 25페이지에서 찾아 전부 0건으로 만들었습니다. keep-all 선언이 있는 페이지는 0건, 없는 페이지는 반드시 발생했습니다 — 예외가 없었습니다.
이 절차가 못 잡는 것
- 화면 밖 요소를 세면 오탐이 납니다. 봇 트랩(허니팟)은
left:-9999px, 폭 1px 로 숨겨 두는데 1px 상자 안에서는 당연히 줄이 바뀝니다. 위 코드의r0.right < 0 || r0.width <= 1이 그걸 거릅니다. 이 조건이 없어서 "쪼개짐 2건"으로 잘못 보고한 적이 있습니다. - 정적 스캔으로 못 잡는 것이 있습니다. 영문 페이지 8개가 "본문 한글 0건"이었는데 담기 버튼을 눌러보니 장바구니에 한국어 상품명이 떴습니다. 값이
data-*에 있고 JS 가 그걸 화면에 씁니다. 실제로 눌러봐야 보이는 결함이 있습니다. - 쪼개짐이 0이어도 두 줄짜리 제목의 마지막 줄에 한 어절만 남는 것은 따로 봐야 합니다(
text-wrap: balance).
공통으로 배운 것
측정이 실패를 뱉으면 도구부터 의심합니다. 위 세 절차를 쓰면서 실제로 틀린 판정을 여러 번 냈고, 그중 상당수가 코드나 사이트가 아니라 재는 방법의 문제였습니다.
- 항목의 자체 여백을 빼먹어 간격 비율이 미달로 나옴
- 가로로 놓인 요소를 세로 간격으로 잼
line-height: normal을parseFloat이NaN으로 만들어 행간이 0으로 나옴head -c 6으로 자른<?xml을'<?xml'과 비교해 전부 불일치로 나옴elementFromPoint는 뷰포트 좌표인데 화면 밖 요소에 그대로 씀
결과를 보고하기 전에 그 값이 어떻게 나왔는지 한 번 더 확인합니다.