Husky + lint-staged — 커밋 훅으로 품질 게이트 구축

Husky로 Git pre-commit 훅을 설정하고 lint-staged로 스테이징 파일만 린트·포맷하는 방법, commit-msg 훅으로 커밋 메시지 검증, CI 비교, 팀 세팅 전략을 정리합니다.

· 5 min read · PALDYN Team

지난 글에서 TypeScript ESLint로 타입 인식 린팅을 설정하는 방법을 살펴봤습니다. 린팅 규칙이 아무리 훌륭해도 개발자가 git commit 전에 npm run lint를 실행하지 않으면 의미가 없습니다. Huskylint-staged는 이 문제를 Git 훅으로 자동화합니다. 커밋 순간에 품질 검사를 실행해, 오류가 있는 코드가 저장소에 들어오지 못하도록 막는 **품질 게이트(Quality Gate)**를 구성합니다.


커밋 훅 흐름

Husky + lint-staged 커밋 흐름

핵심 원리는 “스테이징된 파일만” 검사한다는 것입니다. 전체 프로젝트를 린팅하면 커밋마다 수십 초가 걸릴 수 있습니다. lint-staged는 git diff --cached --name-only로 스테이징 파일 목록을 가져와 해당 파일만 처리합니다. 이를 통해 커밋 속도를 유지하면서 품질 게이트를 지킵니다.


설치 및 초기화

Husky + lint-staged 설치 단계

# 패키지 설치
npm install -D husky lint-staged

# Husky 초기화 (v9+)
npx husky init

npx husky init.husky/ 디렉토리를 만들고 package.jsonscripts.preparehusky를 추가합니다. preparenpm install 시 자동으로 실행되므로 팀원이 설치하면 훅이 자동으로 활성화됩니다.


pre-commit 훅 설정

npx husky init이 생성한 .husky/pre-commit을 편집합니다.

# .husky/pre-commit
npx lint-staged

이 한 줄이 전부입니다. lint-staged가 나머지를 담당합니다.


lint-staged 설정

// package.json
{
  "lint-staged": {
    "*.{ts,tsx}": [
      "eslint --fix",
      "prettier --write"
    ],
    "*.{js,mjs,cjs}": [
      "eslint --fix",
      "prettier --write"
    ],
    "*.{json,md,yml,yaml,css,scss}": [
      "prettier --write"
    ]
  }
}

또는 lint-staged.config.mjs로 분리할 수 있습니다.

// lint-staged.config.mjs
export default {
  '*.{ts,tsx}': (files) => [
    `eslint --fix ${files.join(' ')}`,
    `prettier --write ${files.join(' ')}`,
  ],
}

함수 형태를 쓰면 파일 목록을 직접 조작할 수 있습니다. 예를 들어 파일 수가 너무 많을 때 배치로 나누거나, 특정 조건에서 명령을 건너뛸 수 있습니다.


commit-msg 훅 — 커밋 메시지 검증

Conventional Commits 형식을 강제하려면 commitlint를 추가합니다.

npm install -D @commitlint/cli @commitlint/config-conventional
// commitlint.config.mjs
export default {
  extends: ['@commitlint/config-conventional'],
  rules: {
    'type-enum': [2, 'always', [
      'feat', 'fix', 'docs', 'style',
      'refactor', 'test', 'chore', 'ci',
    ]],
  },
}
# .husky/commit-msg
npx --no -- commitlint --edit "$1"

이제 git commit -m "wip" 같은 형식은 차단되고, git commit -m "feat: 로그인 기능 추가"만 허용됩니다.


CI와 로컬 훅의 차이

항목로컬 훅 (Husky)CI
실행 시점커밋 순간PR 병합 전
검사 범위스테이징 파일만전체 코드베이스
건너뛰기git commit --no-verify불가 (PR 차단)
목적빠른 피드백최종 품질 보장

로컬 훅은 git commit --no-verify로 건너뛸 수 있기 때문에, CI에서도 전체 린팅을 반드시 실행해야 합니다. 로컬 훅은 편의를 위한 빠른 피드백, CI는 강제적인 게이트입니다.


팀 환경에서 주의사항

// package.json — prepare 스크립트
{
  "scripts": {
    "prepare": "husky"
  }
}
  • npm install 후 자동으로 husky가 실행되어 Git 훅이 설치됩니다.
  • Windows에서는 .husky/pre-commit이 sh 스크립트이므로 Git Bash 또는 WSL 환경이 필요합니다.
  • CI 환경에서 prepare가 실행되면 .git 디렉토리가 없어 오류가 날 수 있습니다.
// CI에서 prepare 스킵
{
  "scripts": {
    "prepare": "node -e \"process.env.CI || require('husky').install()\""
  }
}

Husky v9에서는 이 문제가 개선되어 .git이 없으면 자동으로 스킵합니다.


성능 최적화

lint-staged의 처리 속도가 느리다면 ESLint 대신 tsc --noEmit로 타입 검사만 하거나, 병렬 실행을 구성할 수 있습니다.

// lint-staged.config.mjs — 느린 tsc는 별도 처리
export default {
  '*.{ts,tsx}': [
    'eslint --fix',
    'prettier --write',
  ],
  // tsc는 전체 프로젝트를 검사해야 하므로 파일 인수 없이 실행
  '**/*.ts?(x)': () => 'tsc --noEmit',
}

() => 함수로 반환하면 lint-staged가 파일 목록을 인수로 붙이지 않습니다. tsc는 파일 경로를 직접 받으면 tsconfig.json을 무시하기 때문에 이 패턴이 필요합니다.


지난 글: TypeScript ESLint — 타입 인식 린팅 완전 가이드

다음 글: ESLint 커스텀 규칙 — AST 기반 규칙 작성


읽어주셔서 감사합니다. 😊