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% |
Takeaway
“데드 버튼 만들지 마세요”보다 CI가 차단하는 것이 확실합니다 — 사람에게 규칙을 맡기면 “이번만 예외”가 반복되고, 결국 규칙이 무너집니다. 반면 CI가
onclick=""패턴을 감지해서 PR을 자동 차단하면, 예외가 불가능합니다. 규칙의 90%는 자동화할 수 있고, 자동화된 규칙은 100% 지켜집니다. 사람의 에너지는 자동화할 수 없는 판단(아키텍처, 도메인 설계, 코드 리뷰)에 집중해야 합니다검증 스크립트는 의존성이 없어야 합니다 —
npm install이 필요한 검증은 CI와 로컬에서 다른 결과를 낳을 수 있습니다. Node 표준 모듈만 사용하는 검증 스크립트는 어디서든 동일하게 동작하며, 설치 실패나 버전 충돌로 인한 “환경 문제”를 원천 차단합니다. 이 설계 원칙 하나로 “로컬에서는 통과하는데 CI에서는 실패함”이라는 가장 짜증나는 상황을 없앨 수 있습니다3-Tier 게이트는 속도 저하가 아니라 품질 향상입니다 — “게이트가 너무 많으면 개발이 느려진다”는 우려가 있지만, 실제로는 역입니다. 자동화된 게이트가 규칙 검증을 0초로 만들기 때문에, 리뷰어는 규칙 준수가 아닌 코드 품질에만 집중할 수 있습니다. PR당 리뷰 시간이 오히려 단축됩니다. 게이트가 없으면 리뷰어가 “이 버튼 작동하나요?”를 확인해야 하고, 게이트가 있으면 “이 접근이 최선인가?”에만 집중합니다
| ← 이전 | 시리즈: AI 주도 개발 거버넌스 | 다음 → |
|---|---|---|
| (5) 레이어드 테스팅 | (6) 3-Tier CI 게이트 | (7) ADR 14선 |
게이트웨이 On-promise 제품 팀에서 시스템 모니터링 및 관리를 쉽게 다가갈 수 있도록 하기 위한 업무를 하고 있습니다.
Contact: lhjnano@gmail.com