정적 사이트에는 런타임이 없습니다. 잘못된 상태가 있어도 예외를 던질 코드가 없고, 그냥 조금 이상한 HTML이 배포될 뿐입니다. 브라우저는 아무 불평도 하지 않습니다.
그래서 어긋난 상태가 오래 남습니다. 이 사이트에서 세 번 반복해서 깨졌던 규칙이 있습니다.
셋 다 문서에 적혀 있었습니다. 셋 다 깨졌습니다. 지금은 셋 다 빌드가 실패시킵니다. 그 뒤로는 한 번도 안 깨졌습니다.
1. 세 규칙이 깨지면 무슨 일이 생기나
중요한 건 셋 다 즉시 눈에 띄지 않는다는 점입니다. 이게 이 규칙들을 빌드로 옮겨야 하는 이유입니다.
로케일 한쪽만 있을 때
레이아웃은 모든 글에 hreflang 두 개를 찍습니다. 한국어 글이 영어판 URL을 가리키고, 영어 글이 한국어판을 가리킵니다. 한쪽만 올라가면 살아 있는 쪽이 존재하지 않는 URL을 가리키게 됩니다.
브라우저로 보면 완벽하게 정상입니다. <link> 태그는 눈에 안 보이니까요. 검색엔진만 이상하게 봅니다.
발행일이 어긋날 때
이건 더 고약합니다. 예약 게시는 각 파일의 date로 판정하니, 한국어 8월 30일 · 영어 9월 1일이면 이틀 동안 위와 똑같은 상태가 됩니다. 파일은 두 개 다 있는데도요.
커밋 시점에는 아무 문제가 없어 보이고, 이틀 뒤 저절로 나으며, 그 사이에는 아무도 모릅니다. 사람이 잡을 수 있는 종류의 오류가 아닙니다.
링크가 미래를 향할 때
8월 30일 글에서 9월 4일 글로 링크를 걸면 닷새 동안 404입니다. 링크를 쓸 때는 대상 파일이 저장소에 멀쩡히 있어서 검증할 것도 없어 보입니다.
코드 리뷰로 잡히지 않습니다. 리뷰어가 보는 건 파일이고, 문제는 "그 파일이 언제 산출물에 들어가는가"에서 생기기 때문입니다.
2. 빌드 시작 시점에 한 번에 검사한다
Eleventy에는 eleventy.before 이벤트가 있습니다. 템플릿을 처리하기 전에 한 번 돕니다. 여기서 소스 파일을 직접 읽어 검사합니다.
글마다 검사하지 않고 빌드당 한 번 도는 게 중요합니다. 대칭이나 링크 방향 같은 규칙은 개별 글이 아니라 전체 집합에 대한 성질이라, 글 하나만 봐서는 판정할 수 없습니다.
왜 CI 나 커밋 훅이 아니라 빌드인가
검사를 걸 수 있는 자리가 셋 있었습니다.
빌드를 고른 이유는 우회할 자리가 없어서입니다. 배포는 반드시 빌드를 거칩니다. 검사를 빌드 안에 두면 "검사를 건너뛰고 배포하기"라는 경로가 아예 존재하지 않습니다.
부수 효과가 하나 더 있습니다. 로컬 npm run build 와 배포가 같은 검사를 돕니다. CI 에만 두면 로컬에서 통과한 게 CI 에서 떨어지는 일이 생기고, 그 간격만큼 왕복이 늘어납니다. 특히 에이전트가 작업할 때는 왕복 한 번이 비쌉니다.
대신 대가가 있습니다. 검사가 모든 빌드에 붙습니다. npm run dev 의 매 저장마다, 미리보기 배포마다 돕니다. 그래서 다음 절의 "파일을 한 번만 읽는다"가 성능 최적화가 아니라 사용성 문제가 됩니다.
파일 목록만 비교하면 끝입니다. 10줄짜리 검사가 문서 한 페이지보다 강력합니다.
3. 검사를 위해 파일을 한 번만 읽는다
나머지 두 검사는 파일 내용이 필요합니다. 각 검사가 따로 읽으면 같은 파일을 두 번 읽게 되니, 앞에서 한 번에 읽어 재사용합니다.
여기서 date를 정규식으로 긁어 문자열로 둡니다. YAML 파서를 붙이지 않았고, Date 객체로도 바꾸지 않았습니다.
날짜가 YYYY-MM-DD 형식이라 사전순 비교가 곧 날짜 비교이기 때문입니다. 시간대도, 파싱 실패도, 의존성도 없습니다. 문자열이 다르면 날짜가 다르고, 문자열이 크면 날짜가 늦습니다.
가드가 복잡해지면 가드 자체에 버그가 생깁니다. 그리고 가드의 버그는 아무도 검사해주지 않습니다.
비용도 실측해두는 편이 낫습니다. 글이 늘어나면 매 빌드에 붙는 시간도 늘어나니까요.
지금은 신경 쓸 값이 아닙니다. 글이 몇 배로 늘어 npm run dev 의 저장마다 체감될 정도가 되면, 그때 파일 전체가 아니라 front matter 만 읽도록 잘라내면 됩니다. 미리 하지 않은 건 가드를 단순하게 두는 값이 지금의 수십 ms 보다 크기 때문입니다.
4. 날짜 일치와 링크 방향
읽어둔 posts 맵으로 나머지 두 검사를 합니다.
링크 검사는 두 가지를 한 번에 봅니다. 대상이 존재하는가(오타·삭제된 글)와 대상이 나보다 먼저 게시되는가입니다. 뒤쪽이 예약 게시 때문에 필요한 검사입니다.
정규식으로 링크를 긁는 게 조잡해 보일 수 있는데, 여기서는 충분합니다. 검사 범위가 같은 로케일의 글 링크로 한정되어 있고 그 형식은 고정이기 때문입니다. HTML 파서를 붙일 만한 이유가 없습니다.
5. 실패 메시지가 곧 수정 지시가 되게
가드를 만들 때 코드보다 오래 고민한 게 메시지였습니다.
좋은 쪽은 무엇이 · 어디서 · 왜 문제인지를 한 번에 줍니다. 특히 "예약 게시 시점이 어긋납니다"라는 한 문장이 중요합니다. 날짜가 왜 같아야 하는지 모르는 사람도 이 메시지만 보면 이해합니다.
그리고 위반을 모아서 한 번에 보고합니다. 첫 번째에서 멈추면 고치고 다시 돌리고를 반복하게 됩니다. 특히 AI 에이전트가 작업할 때 차이가 큽니다 — 실패한 빌드 한 번에서 남은 일을 전부 파악할 수 있습니다.
메시지에 넣을 것을 네 가지로 고정했습니다.
"실제 값"이 의외로 큽니다. 날짜 둘을 같이 보여주면 어느 쪽이 오타인지 대개 한눈에 보입니다. 이게 없으면 파일 두 개를 열어 비교해야 합니다.
실제로 무엇이 걸렸나
가드를 넣은 뒤 실제로 빌드를 실패시킨 유형은 셋입니다.
첫 번째가 압도적입니다. 그리고 이건 원래 가장 눈에 안 띄던 유형이었습니다 — ko 만 있어도 사이트는 멀쩡히 열리고, 영어 목록에 한 줄이 없을 뿐이니까요.
두 번째는 가드를 만들 때 예상하지 못했던 게 하나 있습니다. 같은 회차에서 여러 편을 쓸 때 가장 자주 걸립니다. 8편을 연달아 쓰면서 3번 글에서 7번 글로 링크를 걸면 걸립니다. 저장소에는 두 파일이 다 있으니 사람 눈에는 문제가 없어 보입니다.
세 번째가 드문 건 가드가 있기 때문이 아니라 같은 커밋에서 두 파일을 같이 만들어서입니다. 날짜를 한쪽만 고치는 상황 자체가 잘 안 생깁니다. 그래도 빼지 않은 건 이 유형이 가장 늦게 발견되는 종류라서입니다.
6. 무엇을 가드로 만들지 판단하는 기준
모든 규칙을 빌드에 넣을 수는 없습니다. 셋을 고른 기준은 이랬습니다.
- 조용히 깨지는가. 배포 후에도 브라우저에서 정상으로 보이는 오류 — 최우선입니다.
- 기계적으로 판정 가능한가. 파일 목록과 날짜 문자열 비교면 끝나는 것들입니다.
- 실제로 깨진 적이 있는가. 가상의 위험이 아니라 이미 밟은 것만 넣습니다.
반대로 넣지 않은 것도 분명합니다. "영문판이 기계 번역처럼 읽히지 않아야 한다"는 지켜야 할 규칙이지만 검사할 방법이 없습니다. 이런 건 문서에 남기고 사람이 봅니다.
애매한 경우엔 깨졌을 때 얼마나 늦게 알게 되는가로 판단합니다. 즉시 알게 되는 오류는 가드가 없어도 됩니다. 몇 주 뒤 검색 콘솔에서나 알게 되는 오류는 가드가 필요합니다.
두 축을 교차시키면 판정표가 나옵니다. 조용히 깨지는가와 기계적으로 판정 가능한가입니다.
아래 두 칸이 중요합니다. 시끄럽게 깨지는 건 가드를 만들 필요가 없습니다. 이미 알려주는 장치가 있으니까요. 가드를 늘리는 데는 비용이 있고, 이미 잡히는 걸 또 잡는 가드는 그 비용만 냅니다.
오른쪽 위 칸이 가장 불편한 자리입니다. 조용히 깨지는데 검사할 방법이 없습니다. 여기 있는 항목은 사람이 언제 확인하는지를 절차로 고정해야 합니다 — 규칙 문서에 "빌드가 잡아주지 않는 것" 목록을 따로 둔 이유입니다. 규칙 문서에 무엇을 적을지 정할 때도 같은 구분을 씁니다. 왼쪽 위는 코드로 옮기고, 오른쪽 위만 문서에 남깁니다.
왼쪽 위 칸에도 아직 옮기지 않은 게 있습니다. staticPages.js 와 실제 페이지 목록의 일치는 조용히 깨지고 기계 판정도 가능합니다. 넣지 않은 이유는 셋째 기준 때문입니다 — 아직 실제로 깨진 적이 없습니다. 한 번 깨지면 그날 넣습니다.
7. 남은 구멍은 남아 있다고 적는다
가드를 넣으면 "빌드가 통과했으니 괜찮다"는 착각이 생깁니다. 이게 가드의 부작용입니다.
그래서 규칙 문서에 빌드가 못 잡는 것을 따로 나열했습니다.
두 번째 항목이 실제로 자주 걸립니다. sitemap.xml은 글은 컬렉션에서 자동으로 채우지만 글이 아닌 페이지는 손으로 관리하는 목록에서 가져옵니다. 페이지를 옮기면 그 목록을 같이 고쳐야 하는데, 빌드는 모릅니다. 이것도 언젠가 가드로 옮길 후보입니다.
이 목록을 가드 코드 옆이 아니라 규칙 문서에 둔 것도 의도입니다. 가드를 고치러 오는 사람은 이미 무엇이 검사되는지 알고 있습니다. 이 목록이 필요한 사람은 작업을 끝내고 "다 됐나" 를 판단하려는 사람이고, 그 사람이 여는 건 규칙 문서입니다.
8. 자주 나오는 질문
날짜를 문자열로 비교하는 게 정말 안전한가요?
YYYY-MM-DD 형식이 고정돼 있는 동안에는 안전합니다. 자릿수가 같고 큰 자리가 앞에 오므로 사전순과 시간순이 일치합니다. 깨지는 경우는 형식이 섞일 때입니다 — 2026-8-5 처럼 한 자리로 쓰거나 시각을 붙이면 어긋납니다. 그래서 정규식을 /^date:\s*(\S+)\s*$/m 로 두고, 형식이 다른 값이 들어오면 비교 전에 눈에 띄도록 했습니다.
정규식으로 HTML 링크를 긁는 건 위험하지 않나요?
일반적으로는 위험합니다. 여기서 괜찮은 건 검사 범위가 좁기 때문입니다 — 같은 로케일의 글 링크만 보고, 그 형식은 /ko/posts/{slug}.html 하나로 고정입니다. 못 잡는 경우도 분명합니다. 주석 안의 링크나 템플릿 변수로 만든 링크는 안 걸립니다. 지금은 그런 링크를 쓰지 않아서 문제가 안 됩니다.
검사가 틀려서 멀쩡한 빌드를 막으면요?
그게 가드를 단순하게 유지하는 이유입니다. 지금까지 오탐은 없었지만, 생긴다면 가드를 고치지 임시로 끄지 않습니다. 끌 수 있는 스위치를 만들어두면 그 스위치가 켜진 채로 배포되는 날이 옵니다.
규칙이 늘어나면 어떻게 관리하나요?
지금은 함수 하나에 검사 셋이 들어 있습니다. 더 늘어나면 검사마다 함수를 나누고 위반 목록을 모아 한 번에 던지는 구조가 됩니다. 중요한 건 파일을 읽는 부분과 판정하는 부분을 분리해 두는 것입니다 — 검사가 늘어도 파일은 여전히 한 번만 읽어야 합니다.
모노레포가 아니라 저장소가 나뉘어 있으면요?
그때는 빌드가 못 봅니다. 이 사이트의 딥링크 규칙이 그 경우입니다 — 웹이 던지는 문자열을 iOS·Android 가 파싱하는데, 웹 저장소의 빌드는 앱 코드를 볼 수 없습니다. 검사 대상이 빌드의 시야 밖에 있으면 가드로 못 옮깁니다. 이런 규칙은 문서와 코드 주석 양쪽에 남기는 게 차선입니다.
Eleventy 가 아닌 다른 정적 사이트 생성기에서도 되나요?
이름만 다릅니다. 필요한 건 템플릿 처리 전에 한 번 도는 훅 하나입니다. Astro 는 astro:build:start, Next 는 빌드 스크립트 앞단, Hugo 처럼 훅이 없으면 npm run build 를 node check.mjs && hugo 로 묶으면 같은 효과입니다. 핵심은 도구가 아니라 배포 경로가 반드시 지나가는 자리에 두는 것입니다.
9. 정리
전부 합쳐 60줄 남짓입니다. 그 60줄이 규칙 문서 한 페이지보다 확실하게 동작했습니다.
- 조용히 깨지는 규칙부터 옮긴다. 시끄럽게 깨지는 건 이미 잡힙니다.
- 전체 집합에 대한 성질은 빌드 시작 시점에 한 번에 본다.
- 검증 코드는 최대한 단순하게. 날짜를 문자열로 두면 파서도 시간대도 필요 없습니다.
- 메시지가 수정 지시가 되게, 위반은 모아서 한 번에.
- 가드가 못 잡는 것을 명시한다. 녹색 빌드가 전부 확인했다는 뜻이 되면 안 됩니다.
- 우회할 자리가 없는 곳에 둔다. 배포가 반드시 지나가는 자리가 빌드입니다.
처음 시작한다면 저장소 구조를 정할 때 같이 정하는 편이 낫습니다. 어떤 규칙이 나중에 가드가 될지는 구조가 결정하고, 저장소가 나뉘면 애초에 가드로 옮길 수 없는 규칙이 생깁니다.
규칙 문서에서 같은 문장을 세 번째 강조하고 있다면, 그건 그 규칙이 문서에 있으면 안 된다는 신호입니다. 지켜야 할 것은 지키게 만들고, 지킬 수 없는 것만 적으세요.