Git Hooks란?
https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks
Git의 특정 작업이 발생할 때 사용자 정의 스크립트를 실행할 수 있는 방법이다. Git 공식 문서에 따르면 클라이언트 측과 서버측 2가지로 나뉜다.
클라이언트 측
commit, merge 등의 작업 시 실행되는 훅이다.
- pre-commit: 커밋이 실행되기 전에 실행되며, 코드 검사나 테스트 등을 수행할 수 있다
- prepare-commit-msg: 커밋 메시지 편집기가 열리기 전에 실행되며, 기본 커밋 메시지를 수정할 수 있다
- commit-msg: 커밋 메시지를 검증할 때 사용된다
- post-commit: 커밋이 완료된 후 실행된다
서버측
push된 커밋을 수신할 때 실행되는 훅이다.
- pre-receive: 클라이언트의 푸시 요청을 처리할 때 가장 먼저 실행되는 스크립트이며, 푸시를 거부할 수 있다
- update: 각 브랜치마다 한 번씩 실행되며, 브랜치별로 푸시를 제어할 수 있다
- post-receive: 푸시가 완료된 후 실행되며, 알림이나 배포 등을 트리거할 수 있다
대표적인 Hooks 관리자 Husky, Lefthook 비교
npm 다운로드 수를 비교했을 때 Husky가 압도적으로 많았으며, 다른 도구들과는 비교할 수 없는 수준이었다. 최신 업데이트 주기를 보면 Husky는 연 단위로 업데이트되는 반면, Lefthook은 일 단위로 활발하게 업데이트되고 있었다.
여기서 잠깐
Husky는 Git 훅 트리거 기능에만 초점이 맞춰져 있어서, 실무에서는 보통 lint-staged와 함께 조합하여 사용했다. 따라서 여기서는 Husky + lint-staged 조합과 Lefthook을 비교했다.
Husky
https://typicode.github.io/husky/
Husky의 주요 특징은 다음과 같다:
- JavaScript로 작성되었다
- 병렬 실행을 지원했다
- 유연한 설정이 가능했다
- 종속성 없이 2KB의 가벼운 용량이었다
- 단독으로 사용되기보다는 보통 lint-staged와 조합하여 사용되었다
- 단순히 Git hook을 실행하는 러너 역할만 했다
lint-staged
lint-staged는 Husky와 함께 자주 사용되는 도구로, 다음과 같은 특징이 있다:
- staged된 파일만 처리하는 전용 도구였다
- glob 패턴 매칭을 지원했다
- 경로별로 다른 linter를 실행할 수 있었다
- Node.js 기반으로 작성되었다
Lefthook
Lefthook의 주요 특징은 다음과 같다:
- Go 언어로 작성되어 실행 속도가 빨랐다
- 병렬 실행을 쉽게 설정할 수 있었다
- 린트만 실행하는 경우엔 큰 차이가 없지만, 복잡한 작업에서는 유리했다
- 크로스 플랫폼을 지원했다
- 의존성 없는 단일 바이너리 파일로 가볍고 다양한 환경에서 사용 가능했다
- glob 패턴을 지원하여 유연한 설정이 가능했다
- lint-staged 없이도 staged된 파일만 검사할 수 있었다
- 훅 매니저 기능과 lint-staged 기능을 모두 가지고 있었다
stage_fixed: true옵션으로 자동 수정된 파일을 자동으로 stage하는 기능이 있었다
사용 방식 비교 및 정리
기본 Husky 방식
#!/bin/sh
echo "Start Linting and Formatting 💥"
npm run lint
if [ $? -eq 0 ]; then
echo "✅ Complete! 🚀"
else
echo "❌ Error! 🛑"
exit 1
fi
이 방식의 문제점은 커밋된 영역만이 아니라 프로젝트 전체에 대해 lint가 실행된다는 것이었다. Husky는 staged된 파일만 필터링하는 기능이 없었기 때문이다.
해결 방법: lint-staged를 이용해 staged된 파일만 처리하도록 하면 속도를 크게 향상시킬 수 있었다.
husky + lint-staged 조합으로 개선
#!/bin/sh
echo "Start Linting and Formatting 💥"
npx lint-staged
if [ $? -eq 0 ]; then
echo "✅ Complete! 🚀"
else
echo "❌ Error! 🛑"
exit 1
fi
// package.json
{
"lint-staged": {
"*.{js,jsx,ts,tsx,json,css,scss}": [
"eslint --fix",
"prettier --write"
]
}
}
장점
- staged된 파일만 검사해서 빠름
- 내가 수정한 파일만 검사
- 가장 널리 사용되는 방식 (레퍼런스 많음)
- npm/pnpm 생태계와 잘 맞음
- 안정적이고 검증됨
단점
- husky와 lint-staged 두 개의 도구 필요
- 설정이 package.json과 .husky 폴더로 분산
- husky는 bash 스크립트 기반이라 가독성이 떨어짐
lefthook으로 개선
프로젝트 루트에 lefthook.yml 파일을 생성
# lefthook.yml
pre-commit:
parallel: true
commands:
lint:
glob: "*.{js,jsx,ts,tsx,json,css,scss}"
run: npm run lint {staged_files}
장점
- staged된 파일만 검사해서 빠름
- 하나의 도구로 모든 것 관리 (lint-staged 불필요)
- YAML 기반 설정으로 가독성 좋음
- parallel: true로 병렬 실행 가능
- Go로 작성되어 실행 속도가 더 빠름
- 더 많은 기능 (skip, follow, interactive 등)
단점
- husky보다 덜 대중적 (레퍼런스 적음)
- 팀원들이 익숙하지 않을 수 있음
모노레포 설정
# lefthook.yml
pre-commit:
skip: # GUI 도구에서 설정되지 않도록
- run: test -z "$TERM" || test "$TERM" = "dumb"
reason: "Skipping in GUI environment (Fork, SourceTree, etc.)"
parallel: true
commands:
# 프론트엔드 앱
lint-frontend:
glob: "apps/frontend/**/*.{js,jsx,ts,tsx,json,css,scss}"
run: eslint --fix {staged_files}
# 관리자 대시보드
lint-admin:
glob: "apps/admin/**/*.{js,ts,vue,json,css,scss}"
run: pnpm exec prettier --check {staged_files}
# 모바일 웹
lint-mobile:
glob: "apps/mobile/**/*.{js,jsx,ts,tsx}"
root: "apps/mobile/"
run: pnpm exec eslint {staged_files}
# API 서버
lint-api:
glob: "apps/api/**/*.{js,ts,json}"
run: eslint --fix {staged_files}
# 공통 패키지
lint-packages:
glob: "packages/**/*.{js,jsx,ts,tsx,json,css,scss}"
run: eslint --fix {staged_files}
# 루트 설정 파일
lint-root-config:
glob: "*.{json,yml,yaml,md}"
run: prettier --check {staged_files}
pre-push:
skip:
- run: test -z "$TERM" || test "$TERM" = "dumb"
reason: "Skipping in GUI environment (Fork, SourceTree, etc.)"
parallel: true
commands:
# 프론트엔드 테스트
test-frontend:
glob: "apps/frontend/**/*"
run: pnpm --filter frontend test
# 관리자 대시보드 테스트
test-admin:
glob: "apps/admin/**/*"
run: pnpm --filter admin test
# API 서버 테스트
test-api:
glob: "apps/api/**/*"
run: pnpm --filter api test