
이전 프로젝트에서 commitlint를 사용하며 커밋 메시지 규칙만 제대로 잡아도 히스토리가 크게 정돈된다는 점을 체감했다. 이번 프로젝트에서도 커밋 실수를 줄이고 일관성을 높이고자 Git Hook을 도입했다.
특히 커밋 직전에 Prettier/ESLint를 자동 실행하는 pre-commit 훅을 함께 사용해, 가능하면 즉시 자동 수정되도록 했고 자동 수정이 불가능한 경우에는 커밋을 차단하도록 했다. 그 결과 포맷 누락·사소한 린트 이슈로 PR에서 불필요하게 오가는 시간을 줄였다.
메시지 규칙은 commit-msg 훅 + commitlint로 강제했다.
내 프로젝트에서의 규칙인CDP-숫자 type이모지(scope): subject(예:CDP-32 chore⚙️(config): update ESLint & Prettier)를 통과하지 않으면 커밋을 막도록 했다.
최종적으로 코드 품질(pre-commit)과 히스토리 품질(commit-msg)을 동시에 확보해, 로컬에서 빠르게 피드백을 받고 PR/CI 소음을 최소화했다.
아래 내용은
Git공식 문서를 참고해 처음 접하는 사람이 이해하기 쉽게 풀어 썼다.
공식 문서개념
Git Hook은Git이 특정 이벤트 시점(예: 커밋 작성, 커밋 메시지 저장, 푸시 전/후 등)에 자동으로 실행하는 스크립트다.훅은 로컬에서 동작하는
클라이언트훅과 원격 저장소에서 동작하는서버훅으로 나뉜다.
훅 스크립트가0이 아닌 종료 코드로 끝나면 해당Git동작이 즉시 중단된다(예: 커밋 취소, 푸시 거절).
이 메커니즘으로 코드 검사, 메시지 규칙, 푸시 정책 같은 팀 규칙을 자연스럽게 워크플로에 녹일 수 있다.
어디에 두고, 어떻게 설치하나
- 기본 위치는
<repo>/.git/hooks이다.
이 디렉터리에는Git이 제공하는 샘플 스크립트가 들어 있으며, 각 파일에 어떤 인자를 받고 언제 실행되는지가 주석으로 설명되어 있다. 샘플은 보통sh/Perl로 작성되어 있지만, 실행 가능한 파일이면Python,Ruby,Node.js등 어떤 언어도 사용할 수 있다.
- 활성화 방법(샘플 활용)
- 파일명에서
.sample을 제거한다- 실행 권한을 부여한다
mv .git/hooks/pre-commit.sample .git/hooks/pre-commit chmod +x .git/hooks/pre-commit- 처음부터 새로 만들기
확장자 없이 훅 이름과 동일한 파일명으로 만들고, 실행 권한을 준다.printf '%s\n' '#!/bin/sh' 'echo "pre-commit running..."' > .git/hooks/pre-commit chmod +x .git/hooks/pre-commit- 팀과 공유하려면
훅을 저장소와 함께 버전 관리하거나 여러 저장소에서 공통으로 사용하려면Git설정의core.hooksPath로 디렉터리를 바꿀 수 있다.git config core.hooksPath .githooks # 현재 저장소에만 git config --global core.hooksPath ~/.config/git/hooks # 전역(모든 저장소)VSCode에서 작성해도 됨
파일 생성·편집은VSCode에서 마우스로 해도 무방하다. 다만macOS/Linux에서는 한 번은chmod +x로 실행 권한을 꼭 줘야 훅이 동작한다.
- 더 쉬운 방법(
Husky)
수동으로.git/hooks를 관리하는 대신Husky를 쓰면 레포의.husky/폴더로 훅을 버전 관리하고, 설치 시 자동 활성화되는 패턴을 쉽게 구축할 수 있다. (아래 섹션에서Husky기반 설정을 별도로 다룬다)
클라이언트 훅은 로컬 저장소에서git작업이 수행될 때 자동으로 실행되는hook이며, 종류가 매우 다양하다.
이해하기 쉽게 세 가지 흐름으로 나누어 설명한다.Committing-workflow 훅
commit과정과 직접 맞물리는 훅 묶음이다. 커밋 직전/중/직후에 실행되어 코드 스냅샷 점검, 메시지 가공/검증, 후처리 같은 일을 담당한다.
pre-commit: 커밋 메시지를 작성하기 전에 실행된다.snapshot점검(빠진 파일, 테스트, 린트 검사 등)에 활용.exit코드가 0이 아니면 커밋 중단.prepare-commit-msg:git이 메시지를 생성한 직후, 편집기가 뜨기 전에 실행된다. 메시지 템플릿 삽입이나merge/squash/amend시 자동 수정에 적합.commit-msg: 메시지가 임시 파일에 기록된 후 실행된다. 정책 검사(예:conventional commits)를 통해 메시지 유효성 확인.post-commit: 커밋 완료 후 실행된다. 알림 전송이나 로그 처리 등 사후 작업에 적합.예: 커밋 전 포맷/린트 검사, 커밋 메시지 템플릿/접두어 설정, 커밋 후 알림 등
Email-workflow 스크립트
git am)에서 패치를 적용하기 전후로 실행되는 스크립트 묶음이다.
패치 메시지 형식 검증, 적용 전 테스트, 적용 후 정리/알림 같은 자동화를 붙일 수 있다.
applypatch-msg: 패치 메시지 파일을 받아 규칙 검사나 자동 수정에 활용.pre-applypatch: 패치가 적용된 뒤 실행된다. 테스트 실행 및snapshot검사 가능. 실패 시git am중단.post-applypatch: 패치 적용 후 알림을 보낼 때 활용. 중단은 불가능.
그 외(작업 전환/병합/푸시 직전 등)
브랜치 전환, 병합 완료, 푸시 직전처럼
commit외의 로컬 이벤트에 반응하는 훅들이다.
환경/산출물 동기화, 권한/의존성 정리, 푸시 직전의 마지막 검증 등 상황별 후크가 포함된다.
pre-rebase:rebase전에 실행, 조건에 맞지 않으면 중단. (예: 이미push한 커밋의rebase방지)post-rewrite:commit --amend,rebase등 커밋을 변경한 뒤 실행된다. 변경된 커밋 목록을 후처리할 때 유용.post-merge:merge완료 후 실행된다.git이 추적하지 않는 파일 권한, 문서 자동 생성/배치 확인 등에 활용.pre-push:push직전에 실행된다. 원격 이름/주소, 업데이트hash를 받아 검증 가능.exit코드가 0이 아니면push중단.pre-auto-gc:git이garbage collection(gc)을 실행하기 전에 실행된다. 사용자에게 알리거나gc를 지연시키는 용도로 활용.
서버 훅은 원격 저장소에push가 발생했을 때 실행되는hook이다.
클라이언트 훅으로도 정책을 강제할 수 있지만, 시스템 관리자는 push 단위 정책 제어를 위해 서버 훅을 주로 활용한다.
push전후로 동작하며, 0이 아닌 값을 반환하면 해당 push가 거절될 수 있다.Push 전 실행 훅
push직전에 실행되어 권한 제어나 정책 강제를 담당한다.
pre-receive:push시 가장 먼저 실행된다. 표준 입력(stdin)으로refs목록을 받아 검증한다.
fast-forward여부나 브랜치push권한 제어에 적합. 실패 시 전체refs거절.
update: 브랜치별로 실행된다. 브랜치 이름, 이전sha-1, 새sha-1을 아규먼트로 받는다.
특정 브랜치만 거절할 수 있어 세밀한 정책 적용 가능.
Push 후 실행 훅
push가 끝난 뒤 실행되며, 알림/연동 작업을 담당한다.
post-receive:push완료 후 실행된다.
표준 입력(stdin)으로refs목록을 받아 알림/ci/cd/ticket시스템과 연동 가능.커밋 메시지 파싱도 가능해 자동 티켓 생성·수정·닫기에 활용된다. 단,
push중단은 불가능하며 스크립트 종료까지 클라이언트 연결이 유지되므로 장시간 작업 시 주의가 필요하다.
좋습니다 👍 요청하신 대로 도입부에 공식문서를 참고했다는 안내를 추가해서 전체 내용을 다시 정리해드릴게요.
본 문서는 Husky 공식 문서를 참고하여 주요 특징과 사용법을 정리한 내용이다.
v10 기준으로 핵심 변화와 설치/활용 방법을 담았다.
Husky는 Git 훅 관리 도구로, 커밋과 푸시 시점에 자동으로 특정 작업을 실행하게 도와준다.
예를 들어 커밋 메시지 규칙 검사, 코드 린트, 테스트 실행 등을 자동화하여 일관된 코드 품질을 유지할 수 있다.
주요 특징
- 📦 가볍다: 압축 시 2KB, 의존성 없음
- ⚡ 빠르다: ~1ms 내외 실행 속도
- 🔑 새로운 Git 기능 활용:
core.hooksPath를 사용하여 깔끔한 훅 관리- 🖥️ 범용 지원: macOS, Linux, Windows + Git GUI + Node 버전 매니저 + 모노레포/서브프로젝트 환경
- 🔗 모든 클라이언트 훅 지원: 총 13종의 Git client-side hooks 지원
고급 기능
- 브랜치별 훅 지정 가능
- POSIX 쉘 기반 스크립트 작성으로 고도화된 자동화 가능
- Git의 기본 훅 구조와 일치해 학습 부담 최소화
npm prepare script와 연동해 설치 시 자동 세팅- opt-in/opt-out 유연한 설정 → 필요 시 전역 비활성화 가능
- 친절한 에러 메시지 제공
v10 핵심 변화
- 경량화: 코드 크기 2KB, 불필요한 의존성 제거 → 설치/실행 속도 개선
- Git 네이티브 정합성:
core.hooksPath를 기반으로 관리되어 Git의 표준 방식과 자연스럽게 호환- 사용성 개선: 글로벌 비활성화 옵션, 브랜치별 훅, GUI/Node 버전 매니저/모노레포까지 폭넓게 지원
# Husky를 devDependencies에 설치 npm install --save-dev husky --- # 프로젝트 초기화 (추천) # 👉 .husky/ 디렉토리 생성 # 👉 기본 pre-commit 훅 추가 # 👉 package.json에 "prepare": "husky" 자동 등록 npx husky init
# "Keep calm and commit" 메시지로 커밋 시도 # 👉 커밋 직전에 .husky/pre-commit 훅이 실행됨 git commit -m "Keep calm and commit"
# .husky/pre-commit 파일 예시 --- # 변경된(staged) 파일 중 추가/수정된 파일만 prettier 실행 prettier $(git diff --cached --name-only --diff-filter=ACMR | sed 's| |\\ |g') --write --ignore-unknown --- # 변경 사항을 다시 git index에 반영 git update-index --again
# pre-commit 훅 파일 생성 # 👉 커밋 시 자동으로 npm test 실행 echo "npm test" > .husky/pre-commit
# (1) 단일 커밋 시 훅 건너뛰기 git commit -m "..." -n --- # (2) 임시 비활성화 (해당 명령어 실행에만) HUSKY=0 git commit -m "skip hooks" --- # (3) 여러 명령 동안 비활성화 export HUSKY=0 # 훅 비활성화 git rebase ... git merge ... unset HUSKY # 다시 훅 활성화 --- # (4) 전역/GUI 환경에서 비활성화 # ~/.config/husky/init.sh 파일에 추가 export HUSKY=0 --- # (5) CI 환경에서 훅 비활성화 (GitHub Actions 예시) env: HUSKY: 0
트러블슈팅
설치 문제, Node 버전 매니저/Git GUI 관련 에러 등은 공식 문서의 Troubleshooting 섹션을 참고하면 된다.
이 글은 Husky 공식 문서를 참고해 정리했다.
전제: 이미Prettier/ESLint기본 설정을 마쳤고, 패키지 매니저는pnpm을 사용한다.
📦 package.json scripts
{ "scripts": { "lint": "eslint . --ext .js,.jsx,.ts,.tsx --max-warnings=0", "lint:fix": "pnpm lint --fix", "format": "prettier --write .", "format:check": "prettier --check .", "prepare": "husky" } }각 script 설명
- lint: ESLint 실행 (경고도 실패로 처리)
- lint\:fix: ESLint 자동 수정
- format: Prettier 자동 포맷
- format\:check: Prettier 포맷 검사 (수정은 안 함)
- prepare: husky 설치 시 훅 세팅
터미널
pnpm i pnpm dlx husky init # 또는 npx husky init결과
- 프로젝트 루트에
.husky/폴더가 생성되고, 기본 훅 파일(예:pre-commit)이 만들어질 수 있음.- 이후 우리가 직접 만든 파일로 덮어씌운다.
.husky/ ├── _/ # husky 내부 실행 스크립트 ├── pre-commit # 커밋 직전 코드 점검 └── commit-msg # 커밋 메시지 규칙 검증 commitlint.config.cjs
파일 위치:
.husky/pre-commit
아래 코드 사용금지❌
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
echo "🔎 pre-commit: lint-staged 실행 및 최종 점검 시작"
# 0) 커밋에 포함된 파일 목록 (추가/복사/수정)
STAGED_FILES="$(git diff --name-only --cached --diff-filter=ACMR)"
if [ -z "$STAGED_FILES" ]; then
echo "ℹ️ 스테이징된 파일이 없습니다. 건너뜁니다."
exit 0
fi
# 1) 변경 파일만 자동 수정 (eslint --fix, prettier --write)
# => package.json의 "lint-staged" 설정을 보고 파일별로 적절히 실행
if ! pnpm lint-staged; then
echo
echo "❌ lint-staged 단계에서 오류가 발생했습니다."
echo " - 자동수정 불가한 린트 에러가 있을 수 있어요."
echo " - 로컬에서 'pnpm lint'로 에러를 확인하고 수정한 뒤 다시 커밋하세요."
exit 1
fi
# 2) 최종 점검: 스테이징된 파일만 대상으로 검사(수정 없이 실패만 감지)
PRETTIER_FAIL=0
ESLINT_FAIL=0
# 개행만 구분자로 사용 (공백 포함 파일명 안전)
IFS="$(printf '\n')"
for f in $STAGED_FILES; do
case "$f" in
*.js|*.jsx|*.ts|*.tsx|*.json|*.css|*.md)
# Prettier 포맷 준수 확인 (--check는 수정하지 않고 위반만 감지)
pnpm -s prettier --check "$f" || PRETTIER_FAIL=1
;;
esac
done
for f in $STAGED_FILES; do
case "$f" in
*.js|*.jsx|*.ts|*.tsx)
# ESLint 최종 확인 (수정 없이 경고도 0개로 강제)
pnpm -s eslint --max-warnings=0 "$f" || ESLINT_FAIL=1
;;
esac
done
unset IFS
if [ "$PRETTIER_FAIL" -ne 0 ]; then
echo
echo "❌ Prettier 포맷 위반이 남아 있습니다."
echo " - 'pnpm format' 후 다시 커밋하세요."
exit 1
fi
if [ "$ESLINT_FAIL" -ne 0 ]; then
echo
echo "❌ ESLint 위반이 남아 있습니다."
echo " - 'pnpm lint:fix' 또는 수동 수정 후 다시 커밋하세요."
exit 1
fi
COUNT="$(printf '%s\n' "$STAGED_FILES" | wc -l | tr -d ' ')"
echo "✅ pre-commit 통과: ${COUNT}개 파일 점검 완료"
exit 0
실제로 무슨 일이 일어나나?
- staged 파일 목록만 수집 → 쓸데없이 전체를 돌지 않음.
- lint-staged 실행 → 파일 확장자별로
eslint --fix,prettier --write자동수정.- 최종 check → 수정 없이
prettier --check,eslint --max-warnings=0로 “위반만 감지”.- 실패 시 안내 메시지 + 즉시 중단, 성공 시 체크된 파일 개수 출력.
커밋 시 출력 예시
🔎 pre-commit: lint-staged 실행 및 최종 점검 시작 ✅ pre-commit 통과: 3개 파일 점검 완료혹은 실패 시:
❌ ESLint 위반이 남아 있습니다. - 'pnpm lint:fix' 또는 수동 수정 후 다시 커밋하세요.
❌
No files matching the pattern …에러가 계속 뜰 때커밋 훅을 세팅하다 보면, lint-staged까지 설정했는데도 커밋이 자꾸 막히는 경우가 있다. 나도 최근에 겪은 문제인데, 로그를 보면 이렇게 나온다.
Checking formatting... [error] No files matching the pattern were found: "package.json [error] pnpm-lock.yaml [error] src/app/page.tsx [error] src/shared/ui/Icon.tsx".분명 Prettier랑 ESLint 설정은 잘 돼 있는데, 왜 이런 에러가 날까?
🔎 원인: 파일 목록이 한 덩어리로 뭉쳐버림
문제를 뜯어보면 이유는 단순하다.
보통pre-commit 훅에 이런 식의 코드를 넣곤 한다.STAGED=$(git diff --name-only --cached) pnpm prettier --check "$STAGED" pnpm eslint "$STAGED"겉보기엔 맞는 코드 같지만,
$STAGED안에는 파일 목록이 줄바꿈(\n)으로 구분돼 들어 있다.
그런데"$STAGED"로 감싸버리면 줄바꿈까지 포함된 하나의 긴 파일명으로 인식된다.
Prettier와ESLint입장에서는 “그런 파일은 없는데?”라며 에러를 뱉는 거다.
그래서 로그에"package.json\npnpm-lock.yaml\nsrc/app/page.tsx …"같이 줄바꿈이 섞인 이상한 경로가 그대로 찍히는 것이다.
✅ 해결: pre-commit 훅은 단순하게
사실 이 문제는
lint-staged가 이미 해결해둔 영역이다.
스테이징된 파일만 뽑아서Prettier/ESLint에 정상적으로 공백 분리된 인자로 넘기는 기능이lint-staged의 본질이기 때문이다.따라서
pre-commit 훅은 이렇게만 두면 충분하다.pnpm lint-staged나머지 파일 처리 로직은 다
lint-staged가 알아서 해준다.
Husky는 “커밋 전에 lint-staged를 실행해라”라는 트리거 역할만 맡는 게 깔끔하다.
파일 위치: 파일 위치
.husky/commit-msg
#!/bin/sh
pnpm commitlint --edit "$1" || {
echo
echo "❌ 커밋 메시지 규칙 위반!"
echo " 형식: CDP-숫자 type이모지(scope): subject"
echo " 예시: CDP-31 fix🐛(ci): CI error resolution"
echo " 허용: feat✨, fix🐛, refactor🔨, style🎨, chore⚙️, docs📝, test🧪"
exit 1
}
실제로 무슨 일이 일어나나?
git commit이 메시지를 저장하면, Husky가 이 파일을 실행.commitlint가 메시지 헤더를 검사하고, 규칙에 어긋나면 커밋을 거부.- 터미널에 형식/예시/허용 타입을 다시 보여줘서 즉시 수정 가능.
해당 commitlint는 내가 정리한
commitlint + husky 설치 & 연동 가이드 & commitlint + husky 연동기 을 보고 오는게 좋다!
파일 위치: 파일 위치commitlint.config.cjs/** commitlint.config.cjs */ module.exports = { extends: ["@commitlint/config-conventional"], parserPreset: { parserOpts: { // CDP-123 chore⚙️(scope): subject headerPattern: /^(CDP-\d+)\s(feat✨|fix🐛|refactor🔨|style🎨|chore⚙️|docs📝|test🧪)\(([^)]+)\):\s(.+)$/, headerCorrespondence: ["ticket", "type", "scope", "subject"], }, }, rules: { "type-enum": [ 2, "always", ["feat✨", "fix🐛", "refactor🔨", "style🎨", "chore⚙️", "docs📝", "test🧪"] ], "scope-empty": [2, "never"], "subject-empty": [2, "never"], "header-max-length": [2, "always", 120] } };메시지 예시 (성공)
CDP-31 fix🐛(ci): CI error resolution CDP-52 feat✨(ui): add dashboard timeline CDP-77 refactor🔨(auth): extract session handler메시지 예시 (실패)
fix: just message # 티켓 없음, 타입 이모지 없음 CDP-20 feat(ui): wrong type # 타입 이모지 누락 CDP-21 chore⚙️: no scope # scope 누락
git commit -m "..."실행
- pre-commit
- lint-staged → 자동 수정
- Prettier & ESLint 최종 점검
- 실패 시 ❌와 안내 메시지 출력
- commit-msg
- commitlint로 메시지 규칙 검사
- 실패 시 ❌와 규칙 예시 출력
- 둘 다 통과해야 커밋 최종 성공