핵심 정리
# 1. husky 설치 및 초기화
npm install --save-dev husky
npx husky init
# 2. pre-commit 훅 추가 (lint-staged 실행)
echo "npx lint-staged" > .husky/pre-commit
# 3. lint-staged 설정 (package.json)
# "lint-staged": {
# "*.{ts,tsx}": ["eslint --fix", "prettier --write"]
# }
# 4. commit-msg 훅으로 메시지 형식 강제
echo 'npx commitlint --edit "$1"' > .husky/commit-msg기본 흐름
어떤 hook 흐름을 먼저 떠올리면 되나
| 상황 | 먼저 떠올릴 선택 |
|---|---|
| 커밋 직전 검사 | pre-commit |
| 커밋 메시지 형식 검사 | commit-msg |
| push 직전 검사 | pre-push |
| 팀 단위 공유 자동화 | Husky 같은 도구로 버전 관리 |
| 로컬 전용 실험 | .git/hooks/ 직접 스크립트 |
Git 훅은 .git/hooks/의 셸 스크립트다 — 기본 위치만 쓰면 팀 공유가 번거롭다
Git은 특정 이벤트 발생 시 자동으로 실행할 스크립트를 .git/hooks/ 디렉터리에서 찾는다. pre-commit, commit-msg, pre-push 등 이벤트 이름과 일치하는 실행 파일이 있으면 Git이 직접 호출한다. 문제는 기본 위치인 .git/hooks/가 버전 관리 대상이 아니라는 점이다. 이 위치만 쓰면 git clone 후 팀원마다 직접 설치해야 한다. 이 번거로움을 줄이기 위해 husky나 lefthook 같은 도구가 자주 쓰인다. 이런 도구들은 훅 스크립트를 프로젝트 루트의 별도 디렉터리(.husky/ 등)에 두고 core.hooksPath를 설정해, 버전 관리되는 훅 구성을 팀이 함께 맞추기 쉽게 만든다.
# 수동으로 훅 경로를 직접 지정하는 방식 (도구 없이)
git config core.hooksPath .githooks
chmod +x .githooks/pre-commitpre-commit은 커밋 직전에 실행되어 exit code로 커밋을 통과/차단한다
pre-commit 훅은 git commit 명령 직후, 커밋 객체가 생성되기 직전에 실행된다. 스크립트가 exit code 0을 반환하면 커밋이 진행되고, 0이 아니면 커밋이 중단된다. 이 시점에 린트, 포맷터, 단위 테스트를 실행하면 규칙을 어긴 코드가 저장소에 들어오는 것을 막을 수 있다. 그러나 pre-commit에서 전체 파일을 검사하면 속도가 느려진다. lint-staged를 함께 쓰면 스테이징된 파일만 검사해 실행 시간을 크게 줄인다.
# .husky/pre-commit
#!/bin/sh
npx lint-staged
# package.json lint-staged 설정
# "lint-staged": {
# "*.{js,ts,tsx}": [
# "eslint --fix --max-warnings=0",
# "prettier --write"
# ],
# "*.{css,scss}": ["stylelint --fix"]
# }husky + lint-staged는 Node 프로젝트에서 많이 쓰는 공유 패턴이다
husky는 보통 package.json의 prepare 스크립트를 통해 설치 시점에 Git 훅 경로를 설정한다. .husky/ 디렉터리 안의 훅 파일을 git으로 추적하면 팀원이 저장소를 클론한 뒤 의존성을 일반적인 방식으로 설치할 때 같은 훅 구성을 쉽게 맞출 수 있다. 다만 prepare가 비활성화된 환경이나 스크립트를 건너뛴 설치에서는 별도 초기화가 필요할 수 있다. lefthook은 단일 YAML 파일로 훅을 정의하고 병렬 실행을 지원해 속도가 빠르며, Node.js 없이도 동작하는 바이너리를 제공한다.
# husky 설치 흐름 (Node.js 프로젝트)
npm install --save-dev husky lint-staged
npx husky init
# → .husky/pre-commit 생성, package.json에 prepare 스크립트 추가
# package.json 결과
# {
# "scripts": {
# "prepare": "husky"
# }
# }
# lefthook 예시 (lefthook.yml)
# pre-commit:
# parallel: true
# commands:
# lint:
# glob: "*.{ts,tsx}"
# run: npx eslint {staged_files}
# format:
# glob: "*.{ts,tsx,css}"
# run: npx prettier --write {staged_files}로컬 전용 훅과 팀 공유 훅은 목적이 다르다
나 혼자 쓰는 빠른 실험이면 .git/hooks/ 직접 수정도 충분합니다. 팀 전체가 같은 규칙을 공유해야 하면 husky나 lefthook 같은 버전 관리 가능한 방식이 맞습니다. .git/hooks/는 clone해도 전달되지 않기 때문에 팀 규칙으로는 부적합하고, 버전 관리되는 훅은 온보딩과 CI 기준을 맞추기 쉽습니다.
commit-msg 훅으로 Conventional Commits 형식을 강제한다
commit-msg 훅은 사용자가 메시지를 작성한 직후 실행된다. 첫 번째 인수로 임시 커밋 메시지 파일 경로가 전달된다. commitlint와 연동하면 feat:, fix:, chore: 등 Conventional Commits 형식에서 벗어난 메시지를 가진 커밋을 자동으로 거부할 수 있다. CHANGELOG 자동 생성과 시맨틱 버전 자동화를 위한 전제 조건이기도 하다.
# 패키지 설치
npm install --save-dev @commitlint/cli @commitlint/config-conventional
# commitlint.config.js
# module.exports = { extends: ['@commitlint/config-conventional'] };
# .husky/commit-msg
#!/bin/sh
npx commitlint --edit "$1"
# 통과하는 커밋 메시지 예시
# feat: 로그인 폼 유효성 검사 추가
# fix: 사용자 프로필 이미지 로딩 오류 수정
# chore: 의존성 패키지 업데이트
# 거부되는 메시지 예시
# 로그인 고침 ← type 없음
# Update stuff ← 형식 불일치체크포인트
| 상황 | 적합한 선택 |
|---|---|
| 커밋 전 린트·포맷 자동 실행 | pre-commit 훅 + lint-staged |
| 커밋 메시지 형식 강제 | commit-msg 훅 + commitlint |
| push 전 전체 테스트 실행 | pre-push 훅 + npm test |
| 팀 전체에 훅 배포 | husky 또는 lefthook (버전 관리) |
| 훅 실행 없이 긴급 커밋 | git commit --no-verify (CI에서 반드시 재검사) |
주의할 점
git commit --no-verify로 모든 훅을 우회할 수 있습니다. 훅은 개발자 편의를
위한 로컬 보조 수단이지 보안 장치가 아닙니다.
# ❌ 훅에만 의존하면 우회된 커밋이 그대로 배포될 수 있다
git commit --no-verify -m "fix: 급하게 배포"
# ✅ CI 파이프라인에서 동일한 검사를 반드시 중복 실행한다
# .github/workflows/ci.yml
# - run: npx eslint .
# - run: npx tsc --noEmit
# - run: npm test훅은 "실수로 잘못된 코드를 커밋하는 것"을 방지하는 용도입니다. 의도적 우회를 막으려면 CI/CD에서 동일 검사를 필수로 실행해야 합니다.
# .git/hooks/pre-commit에만 스크립트를 넣어둔 경우
git clone https://github.com/org/project
cd project
git commit -m "feat: add login"
# 새로 clone한 팀원 저장소에는 같은 훅이 자동으로 생기지 않음