컨벤션은 문서가 아니라 실패하는 빌드다 — 규칙을 기계로 강제하는 4개 층
규칙 문서 32개, 4,500줄. 아무도 다 기억 못 합니다. 그래서 기억을 포기하고 기계에 맡겼어요. 훅·정적 검사·아키텍처 테스트·산출물 검증으로 쌓은 네 개 층과, 각 층이 무엇을 잡고 무엇을 못 잡는지 적었습니다.

정성껏 쓴 규칙 문서가 있어요. 그리고 그 규칙이 깨진 코드가 있죠. 문서를 쓴 사람도 저고, 규칙을 어긴 사람도 저였습니다.
사람은 원래 그래요. 그래서 이 시리즈의 제목이 "규칙은 기계가 지킨다"입니다.
저희 규칙 문서는 32개 파일에 4,500줄쯤 됩니다. 아키텍처 경계, 패키지 의존 방향, 에러 처리, 빌드와 배포 계약, 작업 유형별 절차까지요.
누가 그걸 다 기억하냐고요? 아무도 못 합니다. 저도 못 해요. 그래서 기억을 포기하고 기계에 맡겼습니다.
왜 문서가 안 통하는지부터
문서가 나쁘다는 게 아니에요. 문서는 "왜"를 설명하는 데는 최고입니다. 그런데 "지금 이 코드가 규칙을 어겼는가"를 판정하지는 못해요.
| 상황 | 문서가 하는 일 | 실제로 필요한 것 |
|---|---|---|
| 새 코드를 짤 때 | 참고할 수 있음 | 대부분 안 읽음 |
| 리뷰할 때 | 근거로 인용 가능 | 리뷰어가 규칙을 기억해야 함 |
| 규칙이 바뀔 때 | 고칠 수 있음 | 기존 코드가 어긴 걸 못 찾음 |
| 규칙을 어겼을 때 | 아무 일도 안 일어남 | 막혀야 함 |
마지막 줄이 핵심이에요. 어겨도 아무 일이 안 일어나는 규칙은 규칙이 아니라 권고입니다. 그리고 권고는 일정이 급해지면 제일 먼저 버려져요.
(그리고 그 판단이 대체로 합리적이라는 게 문제예요. 급할 때 문서를 지키는 건 비합리적이거든요.)
그래서 네 개 층을 쌓았습니다
한 가지 검사로 다 잡으려고 하지 않았어요. 잡을 수 있는 시점이 다르거든요.
| 층 | 언제 | 무엇을 잡나 |
|---|---|---|
| 1. 정적 검사 | 저장할 때 | 문법·패턴·금지된 표현 |
| 2. 훅 | push할 때 | 올리기 전 마지막 방어 |
| 3. 아키텍처 테스트 | CI에서 | 의존 방향·경계 위반 |
| 4. 산출물 검증 | 빌드 뒤 | 결과물이 계약과 맞는가 |
아래로 갈수록 늦게 잡히지만 더 깊은 걸 잡아요. 위로 갈수록 빠르지만 표면만 봅니다.
1층 — 정적 검사
제일 흔한 층이죠. 다만 여기서 잡을 수 있는 것과 없는 것을 구분하는 게 중요했어요.
잡을 수 있음:
금지된 import 경로
금지된 API 사용
네이밍 규칙
파일 위치 규칙
못 잡음:
"이 패키지가 저 패키지를 알면 안 된다" 같은 그래프 규칙
런타임에만 드러나는 결합그래서 정적 검사는 "빠르게 잡히는 것들"만 맡겼습니다. 여기에 그래프 규칙을 욱여넣으면 규칙 파일이 괴물이 돼요.
2층 — push 훅
여기서 결정 하나가 있었어요. 커밋 훅이냐 push 훅이냐.
커밋마다 검사를 돌리면 확실한데, 커밋이 느려집니다. 그리고 커밋은 자주 하는 행위라 느려지면 사람들이 커밋을 덜 쪼개요. 그건 더 나쁜 결과고요.
그래서 push 시점으로 뒀습니다. 커밋은 마음껏 쪼개되, 남에게 보내기 전에 한 번 걸러지는 거죠.
"prepare": "node build/scripts/setup-git-hooks.js"그리고 훅 설치를 의존성 설치 단계에 걸었어요. 새로 합류한 사람이 별도 설정을 안 해도 자동으로 걸립니다. 훅은 설치를 잊으면 없는 것과 같으니까요.
3층 — 아키텍처 테스트
여기가 이 체계의 핵심이에요. 의존 방향 같은 건 파일 하나만 봐서는 알 수 없거든요.
"audit:package-dependencies:check" # 패키지 간 의존 방향 검사
"manifest:check" # 선언과 실제가 일치하는가
"shared-runtime:check" # 공유 런타임 계약이 최신인가
"test:architecture" # 조립 구조 회귀 테스트두 번째 줄을 설명할게요. 저희는 각 패키지가 자기 기여를 선언하고, 그 선언을 모아 코드를 생성합니다. 그런데 선언을 고치고 생성을 안 돌리면 어떻게 될까요?
생성된 파일이 낡은 채로 남아요. 로컬에서는 동작하는데 클린 체크아웃에서는 다른 결과가 나옵니다. 제일 찾기 어려운 종류의 버그죠.
그래서 "다시 생성했을 때 결과가 지금과 같은가"를 검사합니다. 다르면 실패예요. 생성물을 커밋하되 낡지 않게 강제하는 방법입니다.
4층 — 산출물 검증
마지막 층은 빌드가 끝난 뒤에 돕니다. 소스가 아니라 결과물을 봐요.
"artifact:validate" # 조립된 결과물이 계약과 맞는가
"artifact:evidence:validate" # 빌드 근거까지 포함해 검증여기서 잡는 건 앞의 세 층이 원리적으로 못 잡는 것들이에요. 빌드 도구의 동작, 청크 분할 결과, 버전 불일치 같은 것들요.
소스가 다 맞아도 결과물이 틀릴 수 있거든요. 그래서 결과물을 따로 봅니다.
그리고 규칙을 하나 더 뒀어요
층을 여럿 쌓으면 자연히 생기는 문제가 있습니다. 같은 검사가 여러 층에서 중복으로 돌아요.
안전해 보이지만 실제로는 CI 시간만 먹고 아무도 안 읽는 로그를 만듭니다. 그리고 CI가 느려지면 사람들이 CI를 안 기다려요.
그래서 문서에 못 박았습니다. 같은 검사를 여러 단계에서 반복하지 않는다.
개발자가 빠르게 돌려보는 명령이 따로 있는데, 그걸 전체 검증 뒤에 또 돌리지 않아요. 각 층은 자기만 잡을 수 있는 것을 잡습니다.
(중복 검사는 성실함처럼 보이지만, 실제로는 설계를 안 한 흔적이에요.)
예외를 어떻게 다루나
검사를 촘촘히 걸면 반드시 만나는 순간이 있어요. 정당한데 막히는 경우요.
여기서 어떻게 하느냐가 이 체계의 수명을 결정합니다. 잘못 다루면 여러 방향으로 망가져요.
| 나쁜 대응 | 결과 |
|---|---|
| 검사를 끈다 | 한 번 끄면 다시 안 켬 |
| 예외 주석을 자유롭게 허용 | 주석이 늘어나며 검사가 무력화 |
| 규칙을 느슨하게 바꾼다 | 원래 막으려던 것도 통과 |
| 우회 코드를 짠다 | 규칙은 지켜지고 코드는 이상해짐 |
저희가 고른 방식은 이거예요. 예외를 허용하되 예외 목록을 한 곳에 모으고, 그 목록이 줄어드는 방향으로만 움직이게 하는 것.
그러니까 지금 있는 예외는 인정하되 새 예외는 추가를 어렵게 만듭니다. 그러면 시간이 지날수록 예외가 줄어요.
(예외를 0으로 만드는 건 비현실적이에요. 늘지 않게 만드는 게 현실적인 목표입니다.)
그리고 검사에도 테스트가 필요했어요
의외로 놓치기 쉬운 부분인데, 검사 스크립트 자체가 버그를 가질 수 있습니다.
제일 위험한 건 아무것도 안 잡는 검사예요. 통과하니까 잘 동작하는 줄 알거든요. 실제로는 검사 대상 경로를 잘못 잡아서 파일을 하나도 안 보고 있을 수 있습니다.
"test:scripts": "node --test build/scripts/*.test.js ..."그래서 검사 스크립트에도 테스트를 붙였어요. 위반 사례를 일부러 만들어 넣고 제대로 실패하는지를 확인합니다.
통과하는 케이스만 테스트하면 반쪽이에요. 검사는 실패시키는 게 일이니까, 실패시키는지를 테스트해야 합니다.
층 사이의 순서도 규칙이에요
마지막으로 순서 이야기를 적을게요. CI에서 검사를 어떤 순서로 돌리느냐도 설계입니다.
1. 의존성 설치 (고정된 버전으로)
2. 정적 검사 · 테스트 · 타입 검사
3. 브라우저 기반 격리 검사
4. 산출물 빌드
5. 산출물 계약 검증빠르고 싼 것을 앞에, 느리고 비싼 것을 뒤에 뒀어요. 타입 오류가 있는데 브라우저를 띄울 이유가 없거든요.
그리고 1번의 "고정된 버전"이 은근히 중요합니다. 의존성이 매번 조금씩 다르면 검사 결과도 흔들려요. 그러면 사람들이 빨간불을 안 믿기 시작하고, 안 믿는 순간 이 체계 전체가 무의미해집니다.
검사는 정확한 것보다 일관된 게 먼저예요. 가끔 틀리는 검사는 없는 검사보다 나쁩니다.
이 체계의 진짜 이득
처음엔 "실수를 막는 장치"라고 생각했는데, 쓰다 보니 다른 이득이 더 컸어요.
1. 리뷰가 내용에 집중합니다
규칙 위반을 기계가 잡으니까 리뷰에서 그 얘기를 안 해요. "여기 import 방향이 틀렸어요" 대신 "이 로직이 이 위치에 있는 게 맞나요"를 얘기하게 됩니다.
2. 큰 변경이 무섭지 않아요
구조를 갈아엎을 때 무서운 건 "내가 뭘 깼는지 모른다"는 거잖아요. 검사가 촘촘하면 깬 것이 즉시 빨간불로 돌아옵니다.
이 코드베이스가 지금도 큰 구조 변경을 두려움 없이 하는 이유가 여기 있어요.
3. 규칙을 고칠 수 있게 됩니다
의외의 이득이었어요. 규칙이 코드로 있으면 규칙 자체를 리팩터링할 수 있습니다.
문서로만 있으면 규칙을 바꿔도 기존 코드가 따라오는지 알 수 없어요. 검사로 있으면 규칙을 바꾸고 돌려보면 됩니다.
치른 값
| 비용 | 실제로 겪은 일 |
|---|---|
| CI 시간 | 검증 단계가 늘어남 — 중복 제거로 관리 |
| 새 규칙 추가가 무거움 | 문서 한 줄이 아니라 검사 하나를 만들어야 함 |
| 오탐 대응 | 정당한 예외가 막힐 때 예외 경로를 설계해야 함 |
| 검사 자체의 유지보수 | 검사 코드에도 테스트가 필요함 |
두 번째 줄이 실제로는 이득이기도 했어요. 검사를 만들 수 없는 규칙은 대개 규칙이 아직 덜 정의된 것이거든요. "적절히 나눈다" 같은 건 검사를 못 만듭니다. 만들려고 하면 무엇이 적절한지 정의하게 돼요.
그래서 결론은
개인 프로젝트든 회사 플랫폼이든 같은 결론에 도달했어요.
컨벤션 문서는 언젠가 잊히고, 잊힌 규칙은 없는 규칙입니다.
규칙을 지키게 만드는 건 의지가 아니라 구조예요. 어기면 빨간불이 들어오는 구조요.
그리고 이 원칙은 이 시리즈의 다음 편에서 극단까지 갑니다. 화면을 1px도 못 바꾸게 만든 안전망 이야기요.
다음 편은 이 철학의 끝판왕이에요. 구조를 통째로 갈아엎으면서 화면은 1px도 못 바꾸게 만든 안전망 — SHA-256 해시가 UI를 지키는 이야기입니다.


