전체 검색

글, 태그와 카테고리를 검색하고 바로 이동해요

컨벤션은 문서가 아니라 실패하는 빌드다 — 규칙을 기계로 강제하는 4개 층

반복되는 props·레이어 규칙을 검사로 옮겼어요. AST의 정상·위반 사례 테스트와 PR CI 연결을 포함해, 정적 검사·훅·아키텍처 테스트·산출물 검증의 역할과 한계를 정리합니다.

2026. 07. 14. 18:03:447분 읽기프로젝트/데이터 플랫폼
#정적 분석#CI#코드 컨벤션
조회 71
규칙 검사 게이트 썸네일

정성껏 쓴 규칙 문서가 있어요. 그리고 그 규칙이 깨진 코드가 있죠. 문서를 쓴 사람도 저고, 규칙을 어긴 사람도 저였습니다.

사람은 원래 그래요. 그래서 이 시리즈의 제목이 "규칙은 기계가 지킨다"입니다.

저희 규칙 문서에는 여러 책임이 나뉘어 있어요. 아키텍처 경계, 패키지 의존 방향, 에러 처리, 빌드와 배포 계약, 작업 유형별 절차까지요.

누가 그걸 다 기억하냐고요? 아무도 못 합니다. 저도 못 해요. 그래서 기억을 포기하고 기계에 맡겼습니다.

왜 문서가 안 통하는지부터

문서가 나쁘다는 게 아니에요. 문서는 "왜"를 설명하는 데는 최고입니다. 그런데 "지금 이 코드가 규칙을 어겼는가"를 판정하지는 못해요.

상황문서가 하는 일실제로 필요한 것
새 코드를 짤 때참고할 수 있음대부분 안 읽음
리뷰할 때근거로 인용 가능리뷰어가 규칙을 기억해야 함
규칙이 바뀔 때고칠 수 있음기존 코드가 어긴 걸 못 찾음
규칙을 어겼을 때아무 일도 안 일어남막혀야 함

마지막 줄이 핵심이에요. 어겨도 아무 일이 안 일어나는 규칙은 규칙이 아니라 권고입니다. 그리고 권고는 일정이 급해지면 제일 먼저 버려져요.

(그리고 그 판단이 대체로 합리적이라는 게 문제예요. 급할 때 문서를 지키는 건 비합리적이거든요.)

그래서 네 개 층을 쌓았습니다

한 가지 검사로 다 잡으려고 하지 않았어요. 잡을 수 있는 시점이 다르거든요.

층언제무엇을 잡나
1. 정적 검사저장할 때문법·패턴·금지된 표현
2. 훅push할 때올리기 전 마지막 방어
3. 아키텍처 테스트CI에서의존 방향·경계 위반
4. 산출물 검증빌드 뒤결과물이 계약과 맞는가

아래로 갈수록 늦게 잡히지만 더 깊은 걸 잡아요. 위로 갈수록 빠르지만 표면만 봅니다.

1층 — 정적 검사

제일 흔한 층이죠. 다만 여기서 잡을 수 있는 것과 없는 것을 구분하는 게 중요했어요.

잡을 수 있음:
  금지된 import 경로
  금지된 API 사용
  네이밍 규칙
  파일 위치 규칙

못 잡음:
  "이 패키지가 저 패키지를 알면 안 된다" 같은 그래프 규칙
  런타임에만 드러나는 결합

그래서 정적 검사는 "빠르게 잡히는 것들"만 맡겼습니다. 여기에 그래프 규칙을 욱여넣으면 규칙 파일이 괴물이 돼요.

2층 — push 훅

여기서 결정 하나가 있었어요. 커밋 훅이냐 push 훅이냐.

커밋마다 검사를 돌리면 확실한데, 커밋이 느려집니다. 그리고 커밋은 자주 하는 행위라 느려지면 사람들이 커밋을 덜 쪼개요. 그건 더 나쁜 결과고요.

그래서 push 시점으로 뒀습니다. 커밋은 마음껏 쪼개되, 남에게 보내기 전에 한 번 걸러지는 거죠.

"prepare": "node build/scripts/setup-git-hooks.js"

그리고 훅 설치를 의존성 설치 단계에 걸었어요. 새로 합류한 사람이 별도 설정을 안 해도 자동으로 걸립니다. 훅은 설치를 잊으면 없는 것과 같으니까요.

3층 — 아키텍처 테스트

구체적인 예로 컴포넌트 props 구조 검사가 있어요. TypeScript AST로 도메인 필드를 펼친 flat props와 참조 타입을 살펴보고, 명시적 위반과 검토 대상·예외를 구분합니다. 위반이 남으면 테스트가 실패하도록 PR 정적 검사에 연결했어요.

위반 예시만 잡는지 확인하지는 않았어요. 정상적인 도메인 객체 구조, 다른 파일의 타입 참조, 공용 UI의 예외가 의도대로 판정되는지도 테스트합니다. 검사 결과가 실제 계약과 맞는지 확인하는 책임은 남아요.

여기가 이 체계의 핵심이에요. 의존 방향 같은 건 파일 하나만 봐서는 알 수 없거든요.

"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 시간검증 단계가 늘어남 — 중복 제거로 관리
새 규칙 추가가 무거움문서 한 줄이 아니라 검사 하나를 만들어야 함
오탐 대응정당한 예외가 막힐 때 예외 경로를 설계해야 함
검사 자체의 유지보수검사 코드에도 테스트가 필요함

두 번째 줄이 실제로는 이득이기도 했어요. 검사를 만들 수 없는 규칙은 대개 규칙이 아직 덜 정의된 것이거든요. "적절히 나눈다" 같은 건 검사를 못 만듭니다. 만들려고 하면 무엇이 적절한지 정의하게 돼요.

그래서 결론은

개인 프로젝트든 회사 플랫폼이든 같은 결론에 도달했어요.

컨벤션 문서는 언젠가 잊히고, 잊힌 규칙은 없는 규칙입니다.

규칙을 지키게 만드는 건 의지가 아니라 구조예요. 어기면 빨간불이 들어오는 구조요.

다만 검사로 선언한 규칙과 화면의 실제 동작은 구분해야 해요. 구조 검사가 통과했다는 이유만으로 UI가 이전과 같다고 말할 수는 없습니다.


다음 글에서는 구조 검사·API 요청 검사·사람의 검토가 확인하는 범위를 나눠 봤어요. 구조 검사가 통과해도 화면 검토가 남는 이유입니다.

시리즈 이어읽기

규칙은 기계가 지킨다

1 / 4회

이야기 나누기

반응을 남기고 새 글 소식을 받아보세요.

댓글0
서로를 존중하는 대화를 남겨주세요.
아직 남겨진 대화가 없어요
첫 댓글을 남겨 대화를 시작해보세요.
새 글 구독
새 글이 공개되면 이메일로 알려드려요.