상황

코드 품질 규칙을 문서로 외재화하다 보면 자연스럽게 두 겹 구조가 만들어진다. 진입점 문서에는 "이 규칙들을 이런 순서로 검사한다"는 인라인 요약을 두고, 상세 판정 기준은 별도 references 문서에 둔다. 진입점을 읽는 쪽은 빠르게 훑을 수 있고, 깊게 파고들 필요가 있으면 상세 문서로 내려간다. 문서 UX로만 보면 합리적인 배치다.

문제는 이 구조를 사람과 기계가 동시에 읽는다는 점이다. 정적 스캐너는 스캐너대로 구현되어 있고, 리뷰어는 문서를 읽고 지적한다. 둘이 서로 다른 층을 읽으면 같은 코드에 대해 서로 다른 결론이 나온다.

문제

증상은 문서 불일치가 아니라 판정 모순으로 먼저 나타난다.

한 품질 게이트 스킬에서 상수 네이밍 규칙이 두 곳에 존재했다.

  • 진입점 문서: "모든 상수는 export 여부와 무관하게 SCREAMING_SNAKE_CASE"
  • references 상세 문서: "비-export 상수가 SCREAMING_SNAKE이면 camelCase로 바꿔라"

동일한 비-export 상수 하나가 한쪽 기준으로는 준수, 다른 쪽 기준으로는 위반이 된다. 스캐너가 어느 쪽을 구현했는지에 따라 지적이 나오기도 하고 안 나오기도 한다. 리뷰를 받는 쪽 입장에서는 재현되지 않는 지적이다 — 같은 패턴인데 어떤 날은 통과하고 어떤 날은 걸린다.

flowchart TB
    C["동일한 비-export 상수<br/>const MAX_RETRY = 3"]
    C --> A["진입점 인라인 요약<br/>'모든 상수 SCREAMING_SNAKE'"]
    C --> B["references 상세 문서<br/>'비-export는 camelCase'"]
    A --> PA["판정: 준수"]
    B --> PB["판정: 위반"]
    PA --> X{"판정 권위 부재<br/>재현 불가능한 지적"}
    PB --> X

근본 원인은 오타나 실수가 아니다. 같은 판정 기준을 두 곳에 복사한 순간, 권위가 둘로 쪼개진 것이다. 복사 시점에는 일치했더라도 한쪽만 수정되는 순간 drift가 시작되고, 어느 쪽이 정본인지 문서 어디에도 적혀 있지 않으므로 모순을 발견해도 해소할 근거가 없다.

접근과 판단

해소 선택지는 셋이다.

(a) 정본 선언. 진입점에 "충돌 시 references 우선"을 명시한다. 가장 적은 수정이고 기존 구조를 유지한다. 다만 두 곳의 내용은 여전히 갈라진 채로 남고, 독자는 매번 두 문서를 대조해야 한다.

(b) 요약에서 판정 기준 제거. 인라인 요약에는 규칙 이름과 링크만 남기고, 임계값·대소문자·예외 조항은 전부 상세 문서로 내린다. 구조적으로 drift가 불가능해진다.

(c) 규칙 자체 삭제. 규칙의 가치가 낮고 팀 관행이 이미 한쪽으로 굳어 있으면, 규칙을 없애는 것이 가장 싸다.

이 사례에서는 (c)를 택했다. 판단 근거는 두 가지다. 첫째, 팀 관행이 이미 "상수는 무조건 SCREAMING_SNAKE"로 굳어 있었다. 즉 위반이 사실상 발생하지 않는 규칙이었고, 스캐너가 잡는 것은 대부분 오탐이었다. 둘째, 두 문서를 억지로 동기화하는 선택은 drift를 없애는 게 아니라 미루는 것이다. 다음 규칙 변경 때 같은 문제가 재발한다.

가치 낮은 규칙을 유지 비용까지 들여 살릴 이유는 없다. 규칙 개수가 줄면 문서도 짧아지고 스캐너도 가벼워진다.

구현

규칙 삭제는 본문 한 섹션을 지우는 것으로 끝나지 않는다. 규칙 하나는 문서와 코드 여러 지점에 흩어져 있고, 하나라도 남으면 "규칙은 삭제됐는데 트리거는 살아있는" 상태가 된다.

한 세트로 훑어야 할 지점:

  1. 규칙 본문 섹션 — 규칙의 정의와 예시가 있는 곳
  2. 상위 Step 제목의 규칙 번호 목록 — "Step 3: 규칙 7, 9, 12 검사" 같은 인용 목록. 여기 번호가 남으면 존재하지 않는 규칙을 가리키는 참조가 된다
  3. description의 트리거 키워드 — 스킬/도구가 어떤 상황에서 활성화될지 결정하는 부분. 여기 키워드가 남으면 삭제된 규칙 때문에 도구가 호출된다
  4. 스캐너 구현 함수 — 판정 로직 본체
  5. 파이프라인 호출부 — 그 함수를 부르는 곳

4와 5는 반드시 함께 처리한다. 함수만 지우면 런타임 에러, 호출부만 지우면 죽은 코드가 남는다.

삭제 시 한 가지 함정이 더 있다. 공유 헬퍼는 다른 규칙의 의존성을 먼저 확인한다. 이 사례에서 삭제 대상 규칙이 쓰던 countImports()는 정적 데이터 위치 규칙에서도 사용 중이었다. 규칙과 함께 지웠다면 무관한 규칙이 깨졌을 것이다. 확인 결과 존치로 결정했다.

마무리 검증은 두 개면 충분하다.

  • node --check — 구문 무결성
  • 잔여 참조 grep 0건 — 규칙 번호, 규칙 이름, 트리거 키워드로 전체 검색

grep이 0건이 아니면 위 5개 지점 중 놓친 곳이 있다는 뜻이다.

결과

규칙 하나가 문서 3곳과 실행 코드 2곳에서 함께 제거됐고, 같은 코드에 상반된 판정이 나오는 상황이 사라졌다. 남은 규칙들에 대해서도 인라인 요약이 판정 기준을 복사하고 있는지 점검하는 기준이 생겼다.

부수 효과로 스킬 진입점 문서가 짧아졌다. 규칙 문서는 길어질수록 실제로 읽히지 않으므로, 가치 낮은 규칙의 삭제는 남은 규칙의 준수율에도 유리하게 작용한다.

배운 것

  • 요약본을 만드는 순간 판정 권위가 둘로 쪼개진다. 스킬·린터·컨벤션 문서처럼 기계와 사람이 같이 읽는 규칙 문서에서는, 요약본에 판정 기준(임계값, 대소문자, 예외 조항)을 절대 복사하지 않는다. 규칙 이름과 링크만 둔다.
  • 문서 불일치의 증상은 문서에서 나타나지 않는다. 재현되지 않는 리뷰 지적, 사람마다 다른 판정 — 이런 현상이 보이면 규칙 문서의 이중화를 의심한다.
  • 동기화는 해결이 아니라 유예다. 두 사본을 맞추는 선택은 다음 변경 때 같은 문제를 다시 만든다. 구조적으로 갈라질 수 없게 만들거나(포인터만 남기기), 한쪽을 없애야(규칙 삭제) 끝난다.
  • 규칙 삭제의 체크리스트는 문서에서 끝나지 않는다. 정의 · 인용 목록 · 트리거 키워드 · 구현 함수 · 호출부를 한 세트로 본다. 공유 헬퍼는 다른 규칙의 의존성을 확인한 뒤에만 손댄다.