판별 절차

형용사 대신 남이 따라 할 수 있는 절차로 적었습니다. 각 절차마다 그것으로 실제로 무엇을 확정했는지, 그리고 그 절차가 못 잡는 것이 무엇인지 같이 적습니다.

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면 배포 실패.

이 절차가 못 잡는 것

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 연동 없이 직접 업로드하는 방식이라 이력이 남지 않아 생긴 공백이었습니다. 이 상태에서는 누가 무엇을 고쳐도 다음 배포에 사라질 수 있습니다. 라이브 산출물을 해시로 검증해 저장소에 정본으로 고정하고, 배포를 단방향 스크립트로 못박았습니다.

이 절차가 못 잡는 것

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건, 없는 페이지는 반드시 발생했습니다 — 예외가 없었습니다.

이 절차가 못 잡는 것

공통으로 배운 것

측정이 실패를 뱉으면 도구부터 의심합니다. 위 세 절차를 쓰면서 실제로 틀린 판정을 여러 번 냈고, 그중 상당수가 코드나 사이트가 아니라 재는 방법의 문제였습니다.

결과를 보고하기 전에 그 값이 어떻게 나왔는지 한 번 더 확인합니다.