Hook
“데드 버튼 만들지 마세요.” 이렇게 당부하면 다음 PR에도 데드 버튼이 나옵니다. “목 데이터 지워주세요.” 그래도 mock 변수가 프로덕션에 들어갑니다. 사람에게 규칙을 맡기면 실패합니다.
세 프로젝트에서 얻은 가장 중요한 교훈: 규칙은 기계가 지키게 하고, 사람은 판단에만 쓴다. 이 글에서는 pre-commit부터 배포까지 3-Tier 게이트로 규칙을 기계적으로 강제하는 구조를 공유합니다.
TL;DR
- 사람은 판단에만 쓰고, 규칙 준수는 기계에 위임: 3-Tier 게이트: 자동화(pre-commit+CI) → 인간 리뷰 → 보안/법률
- 데드 버튼 정적 탐지: 빈
onclick,href="#", TODO 인터랙티브 요소를 CI가 자동 차단 - 목 데이터 정적 탐지:
mock/dummy/fake/sample변수명 패턴으로 프로덕션 잔류 차단 - 의존성 없는 검증 스크립트(
tools/check.js)가 CI/로컬/hook 3곳에서 동일 실행
Background: 사람이 규칙을 지키지 못하는 이유
반복되는 실패 패턴
개발자/AI의 심리:
"이 버튼은 임시로 비워둘게요" → 까먹음 → 데드 버튼 배포
"이건 나중에 진짜 API로 바꿀게요" → 까먹음 → mock 변수 잔류
"이 시크릿은 잠깐만 하드코딩" → 까먹음 → API 키 노출
| 규칙 | 사람 의존 시 실패율 | 기계 강제 시 실패율 |
|---|---|---|
| 데드 버튼 없음 | ~40% (PR당) | 0% (CI 차단) |
| 목 데이터 없음 | ~20% | 0% (정적 탐지) |
| 시크릿 하드코딩 없음 | ~10% | 0% (스캔) |
| 커밋 메시지 규칙 | ~30% | 0% (hook 강제) |
Solution: 3-Tier CI 게이트
전체 구조
전체 구조
Gate 1: 자동화 (기계)
┌─────────────────────────────────────────────────────┐
│ pre-commit hook → CI Pipeline │
│ ├── lint + format fix ├── lint (엄격) │
│ ├── typecheck ├── typecheck │
│ ├── 시크릿 스캔 ├── test:unit │
│ ├── 커밋 메시지 검증 ├── test:components │
│ └── .env 커밋 차단 ├── test:contracts │
│ ├── audit │
│ └── build dry-run │
└─────────────────────────────────────────────────────┘
↓ 통과 시
Gate 2: 인간 리뷰
┌─────────────────────────────────────────────────────┐
│ ├── CODEOWNERS 승인 (1~2명) │
│ ├── 디자인 체크리스트 │
│ ├── 수동 브라우저 테스트 │
│ └── PR 500라인 이하 권장 │
└─────────────────────────────────────────────────────┘
↓ 통과 시
Gate 2.5: 보안/법률
┌─────────────────────────────────────────────────────┐
│ ├── OWASP Top 10 통과 │
│ ├── 하드코딩 시크릿 없음 │
│ ├── 파라미터화된 SQL만 │
│ ├── 법적 제약 준수 │
│ └── AI 생성 명시 │
└─────────────────────────────────────────────────────┘
↓ 통과 시
배포 (dev → staging → prod)
Gate 1: pre-commit 훅 구성
| 카테고리 | 검사 내용 |
|---|---|
| 일반 파일 | 대용량 파일 거부 (1MB→R2), 병합 충돌, JSON/YAML/TOML 검증, EOF/후행공백 |
| 시크릿 스캔 | detect-secrets (16개 감지 플러그인: AWS/GitHub/JWT/Stripe 등) |
| 커밋 메시지 | Conventional Commits type 화이트리스트 강제 |
| 코드 품질 | ESLint/Biome --fix, Prettier, tsc --noEmit |
| 의존성 감사 | pnpm audit --audit-level=high (push 단계만) |
| 환경 변수 | .env/.dev.vars 커밋 차단 |
핵심 기능 1: 데드 버튼 정적 탐지
AI가 만드는 가장 흔한 안티패턴. 반응 없는 버튼을 CI에서 정적 탐지합니다:
// tools/check.js — 데드 버튼 탐지 로직 (개념)
const DEAD_BUTTON_PATTERNS = [
/onclick=["']\{\}["']/, // onclick="{}"
/onclick=["']\s*["']/, // onclick=""
/href=["']\#["']/, // href="#"
/href=["']javascript:void\(0\)/, // href="javascript:void(0)"
/<button>[^<]*<\/button>\s*<!--\s*TODO/, // TODO 버튼
];
// HTML 파일 스캔 → 패턴 매칭 → 실패
탐지 대상:
| 패턴 | 설명 | 조치 |
|---|---|---|
onclick="" | 빈 이벤트 핸들러 | ❌ 차단 |
href="#" | 의미 없는 링크 | ❌ 차단 |
href="javascript:void(0)" | 안티패턴 | ❌ 차단 |
TODO가 있는 <button> | 미구현 버튼 | ❌ 차단 |
disabled + title 명시 | 의도적 비활성 | ✅ 통과 |
핵심 기능 2: 목 데이터 정적 탐지
프로덕션 코드에 남는 mock/dummy/fake/sample 변수를 탐지합니다:
// 탐지 대상 변수명 패턴
const MOCK_PATTERNS = [
/\bmock[A-Z]/, // mockData, mockUser
/\bdummy[A-Z]/, // dummyData
/\bfake[A-Z]/, // fakeResponse
/\bsample[A-Z]/, // sampleUser
/\btest[A-Z]Data/, // testData
];
테스트 파일(
*.test.js,*.spec.js)은 예외 대상에서 제외합니다.
핵심 기능 3: 의존성 없는 검증 스크립트
피트니스 PWA에서 사용하는 tools/check.js의 핵심 설계:
왜 의존성이 없어야 하는가?
npm install없이 실행 가능 → 로컬, CI, hook 어디서든 동일하게 동작- Node 표준 모듈(
fs,path)만 사용 → 유지보수 부담 최소 - CI와 로컬에서 다른 결과가 나오는 “환경 문제” 원천 차단
9개 검증 카테고리
tools/check.js 검증 항목 전체
| # | 카테고리 | 검증 내용 | 실패 시 |
|---|---|---|---|
| 1 | 파일 크기 | 개별 파일 400KB 초과 | 하드 실패 |
| 2 | manifest 스키마 | 필수 필드/타입 | 하드 실패 |
| 3 | HTML 구조 | 필수 요소 존재 | 하드 실패 |
| 4 | 데드 버튼 | 빈 onclick/href=”#” | 하드 실패 |
| 5 | 목 데이터 | mock/dummy/fake/sample 변수 | 하드 실패 |
| 6 | SW precache | 모든 자산 캐시 포함 | 하드 실패 |
| 7 | 기능 회귀 | 핵심 데이터 구조 보존 | 하드 실패 |
| 8 | i18n 구조 | 번역 파일 정합성 | 하드 실패 |
| 9 | 사용자 데이터 | LocalStorage 키 보존 | 하드 실패 |
CI/CD 파이프라인 흐름
PR 생성
└── lint · typecheck · test · audit · build dry-run
└── 통과 → 인간 리뷰
└── 승인 → main 머지
└── deploy:dev 자동 + migrate:dev
└── 태그 v*.*.*
└── deploy:staging (수동 승인)
└── smoke test (핵심 여정)
└── deploy:prod (테크 리드 수동 승인)
즉시 롤백 기준
| 조건 | 조치 |
|---|---|
| 에러율 > 10% | 즉시 롤백 |
| 결제 기능 장애 | 즉시 롤백 |
| 핵심 API 5xx > 5% | 즉시 롤백 |
| 로그인(OAuth) 실패 | 즉시 롤백 |
| 데이터 손실 | 즉시 롤백 |
롤백 우선 원칙: 근본 원인 파악 전에 먼저 복구. 포스트모템은 비난 없음(blameless).
게이트 우회 (핫픽스만)
# 긴급 핫픽스 시에만 허용
SKIP_PREFLIGHT=1 git push
# 또는
git push --no-verify
남용 금지, 사유 명시 의무. 정기 배포: 화/목 10:00-14:00 KST (저트래픽).
Result: 자동화 도입 전후
| 지표 | 수동 검증 (Before) | 자동화 (After) |
|---|---|---|
| 데드 버튼 배포 | PR당 5~8개 | 0개 |
| 목 데이터 잔류 | 월 1~2건 | 0건 |
| 시크릿 노출 사고 | 분기별 1~2건 | 0건 |
| PR 리뷰 시간 (규칙 검증) | 리뷰어가 수동 체크 | 자동 (0초) |
| 커밋 메시지 규칙 위반 | ~30% | 0% |
마치며
처음 데드 버튼 문제를 발견했을 때, 해결책은 단순했습니다. “데드 버튼 만들지 마세요”라고 팀원에게 당부하는 것이었습니다. 하지만 다음 PR에도 데드 버튼은 나왔고, “목 데이터 지워주세요”라고 말해도 mock 변수는 프로덕션에 들어갔습니다. 사람에게 규칙을 맡기면 “이번만 예외”가 반복되고 결국 규칙이 무너진다는 것을 반복적으로 경험했습니다. 처음에는 팀의 규율 문제라고 생각했지만, 사실은 시스템 설계의 문제였습니다.
전환점은 규칙을 기계에게 맡기기로 한 결정이었습니다. CI가 onclick="" 패턴을 정적 탐지해서 PR을 자동 차단하면 예외가 불가능해집니다. 놀랍게도 규칙의 대부분은 자동화할 수 있었고, 자동화된 규칙은 100% 지켜졌습니다. 더 중요한 발견은 게이트가 많아질수록 개발이 느려질 것이라는 우려와 달리, 실제로는 정반대였다는 점입니다. 자동화된 게이트가 규칙 검증을 0초로 만들어주자 리뷰어는 “이 버튼 작동하나요?”가 아니라 “이 접근이 최선인가?”에 집중할 수 있게 되었습니다. PR당 리뷰 시간이 오히려 단축되었습니다.
검증 스크립트를 의존성 없이 작성한 것도 중요한 설계 원칙이었습니다. Node 표준 모듈만 사용하면 CI와 로컬에서 항상 동일한 결과가 나오고, “로컬에서는 통과하는데 CI에서는 실패함”이라는 가장 짜증나는 상황을 원천 차단할 수 있습니다. 앞으로 AI 에이전트가 더 많은 코드를 더 빠르게 생성할수록, 사람의 판단력이 필요한 영역과 기계가 강제할 수 있는 영역을 명확히 나누는 능력은 더욱 중요해질 것입니다. 규칙은 기계가 지키게 하고, 사람은 판단에만 쓰는 것이 그 답입니다.
| ← 이전 | 시리즈: AI 주도 개발 거버넌스 | 다음 → |
|---|---|---|
| (5) 레이어드 테스팅 | (6) 3-Tier CI 게이트 | (7) ADR 14선 |
게이트웨이 On-promise 제품 팀에서 시스템 모니터링 및 관리를 쉽게 다가갈 수 있도록 하기 위한 업무를 하고 있습니다.
Contact: lhjnano@gmail.com