Git - Git Hook으로 워크플로우 자동화하기

Git Hook이란?

Git Hook은 Git에서 특정 이벤트가 발생했을 때 자동으로 실행되는 스크립트입니다. 커밋, 푸시, 머지 등의 작업 전후에 사용자 정의 스크립트를 실행하여 코드 품질 검사, 테스트 자동화, 메시지 검증 등 다양한 워크플로우를 자동화할 수 있습니다.

Git Hook의 종류

Git Hook은 크게 클라이언트 훅(Client-side Hooks)서버 훅(Server-side Hooks) 으로 나뉩니다.

클라이언트 훅 (Client-side Hooks)

개발자의 로컬 저장소에서 실행되는 훅입니다.

Commit 관련 훅

  • pre-commit: 커밋 메시지 작성 전 실행 (코드 린트, 테스트 실행)
  • prepare-commit-msg: 커밋 메시지 에디터가 열리기 전 실행 (메시지 템플릿 설정)
  • commit-msg: 커밋 메시지 검증 (메시지 형식 확인)
  • post-commit: 커밋이 완료된 후 실행 (알림 전송)

Merge/Rebase 관련 훅

  • pre-merge-commit: 머지 커밋 생성 전 실행
  • post-merge: 머지가 완료된 후 실행 (의존성 업데이트)
  • pre-rebase: 리베이스 전 실행

Push 관련 훅

  • pre-push: 푸시 전 실행 (원격 저장소 업데이트 전 검증)

기타 훅

  • pre-auto-gc: Git의 자동 가비지 컬렉션 전 실행
  • post-checkout: 브랜치 체크아웃 후 실행
  • post-rewrite: 커밋을 재작성하는 명령 후 실행

서버 훅 (Server-side Hooks)

원격 저장소(서버)에서 실행되는 훅입니다.

  • pre-receive: 푸시를 받기 전 실행 (푸시 거부 가능)
  • update: 각 브랜치별로 실행 (브랜치 단위 검증)
  • post-receive: 푸시가 완료된 후 실행 (배포, 알림)

Hook 파일 위치

Git Hook 스크립트는 .git/hooks/ 디렉토리에 저장됩니다.

# Hook 디렉토리 확인
ls -la .git/hooks/

# 샘플 파일들
.git/hooks/pre-commit.sample
.git/hooks/commit-msg.sample
.git/hooks/pre-push.sample

샘플 파일의 .sample 확장자를 제거하면 활성화됩니다.

Git Hook 작성 예시

1. Pre-commit Hook - 코드 품질 검사

커밋 전 자동으로 린트와 테스트를 실행합니다.

#!/bin/sh
# .git/hooks/pre-commit

echo "Running pre-commit checks..."

# JavaScript/TypeScript 프로젝트
if [ -f "package.json" ]; then
echo "Running ESLint..."
npm run lint
if [ $? -ne 0 ]; then
echo "❌ ESLint check failed. Please fix the errors and try again."
exit 1
fi

echo "Running tests..."
npm test
if [ $? -ne 0 ]; then
echo "❌ Tests failed. Please fix the tests and try again."
exit 1
fi
fi

# Python 프로젝트
if [ -f "requirements.txt" ]; then
echo "Running flake8..."
flake8 .
if [ $? -ne 0 ]; then
echo "❌ Flake8 check failed."
exit 1
fi
fi

# Trailing whitespace 제거
git diff --check
if [ $? -ne 0 ]; then
echo "❌ Detected trailing whitespace. Please remove it."
exit 1
fi

echo "✅ All pre-commit checks passed!"

2. Commit-msg Hook - 커밋 메시지 검증

Conventional Commits 형식을 강제합니다.

#!/bin/sh
# .git/hooks/commit-msg

commit_msg_file=$1
commit_msg=$(cat "$commit_msg_file")

# Conventional Commits 패턴
# feat: 새로운 기능
# fix: 버그 수정
# docs: 문서 변경
# style: 코드 포맷팅
# refactor: 리팩토링
# test: 테스트 추가
# chore: 빌드 업무, 패키지 매니저 설정

pattern="^(feat|fix|docs|style|refactor|test|chore|perf|ci|build|revert)(\(.+\))?: .{1,50}"

if ! echo "$commit_msg" | grep -qE "$pattern"; then
echo "❌ Invalid commit message format!"
echo ""
echo "Commit message must follow Conventional Commits format:"
echo " <type>(<scope>): <subject>"
echo ""
echo "Examples:"
echo " feat: add user authentication"
echo " fix(api): resolve CORS issue"
echo " docs: update README"
echo ""
echo "Types: feat, fix, docs, style, refactor, test, chore, perf, ci, build, revert"
exit 1
fi

echo "✅ Commit message format is valid"

3. Pre-push Hook - 푸시 전 검증

원격 저장소에 푸시하기 전 모든 테스트를 실행합니다.

#!/bin/sh
# .git/hooks/pre-push

echo "Running pre-push checks..."

protected_branch='main'
current_branch=$(git symbolic-ref HEAD | sed -e 's,.*/\(.*\),\1,')

# main 브랜치 직접 푸시 방지
if [ "$current_branch" = "$protected_branch" ]; then
echo "❌ Direct push to main branch is not allowed!"
echo "Please create a feature branch and open a pull request."
exit 1
fi

# 전체 테스트 실행
echo "Running full test suite..."
npm run test:ci
if [ $? -ne 0 ]; then
echo "❌ Tests failed. Push aborted."
exit 1
fi

# 빌드 테스트
echo "Testing build..."
npm run build
if [ $? -ne 0 ]; then
echo "❌ Build failed. Push aborted."
exit 1
fi

echo "✅ All pre-push checks passed!"

4. Post-merge Hook - 머지 후 의존성 업데이트

브랜치 머지 후 자동으로 의존성을 설치합니다.

#!/bin/sh
# .git/hooks/post-merge

echo "Checking for dependency changes..."

# package.json 또는 package-lock.json 변경 확인
changed_files="$(git diff-tree -r --name-only --no-commit-id ORIG_HEAD HEAD)"

check_run() {
echo "$changed_files" | grep --quiet "$1" && eval "$2"
}

# Node.js 의존성 업데이트
check_run package.json "echo '📦 package.json changed. Running npm install...' && npm install"

# Python 의존성 업데이트
check_run requirements.txt "echo '📦 requirements.txt changed. Running pip install...' && pip install -r requirements.txt"

# Go 모듈 업데이트
check_run go.mod "echo '📦 go.mod changed. Running go mod download...' && go mod download"

echo "✅ Post-merge checks completed!"

5. Prepare-commit-msg Hook - 이슈 번호 자동 추가

브랜치 이름에서 이슈 번호를 추출하여 커밋 메시지에 자동으로 추가합니다.

#!/bin/sh
# .git/hooks/prepare-commit-msg

commit_msg_file=$1
commit_source=$2

# 머지 커밋이면 스킵
if [ "$commit_source" = "merge" ]; then
exit 0
fi

# 브랜치 이름에서 이슈 번호 추출 (예: feature/ISSUE-123-description)
branch_name=$(git symbolic-ref --short HEAD)
issue_number=$(echo "$branch_name" | grep -o -E '[A-Z]+-[0-9]+' | head -1)

if [ -n "$issue_number" ]; then
# 기존 메시지 읽기
commit_msg=$(cat "$commit_msg_file")

# 이미 이슈 번호가 있으면 스킵
if ! echo "$commit_msg" | grep -q "$issue_number"; then
# 이슈 번호 추가
echo "$commit_msg" | sed "1s/^/[$issue_number] /" > "$commit_msg_file"
echo "✅ Added issue number: $issue_number"
fi
fi

Hook 파일 실행 권한 부여

Hook 스크립트를 작성한 후 실행 권한을 부여해야 합니다.

chmod +x .git/hooks/pre-commit
chmod +x .git/hooks/commit-msg
chmod +x .git/hooks/pre-push
chmod +x .git/hooks/post-merge

Hook 공유하기

.git/hooks/ 디렉토리는 Git으로 추적되지 않아 팀원과 공유할 수 없습니다. 다음 방법으로 Hook을 공유할 수 있습니다.

방법 1: 스크립트를 저장소에 포함

# 프로젝트 루트에 hooks 디렉토리 생성
mkdir -p .githooks

# Hook 스크립트를 .githooks에 작성
# .githooks/pre-commit
# .githooks/commit-msg

# Git 설정 변경
git config core.hooksPath .githooks

# 팀원도 동일하게 설정

방법 2: 설치 스크립트 제공

#!/bin/sh
# scripts/install-hooks.sh

echo "Installing Git hooks..."

# Hooks 복사
cp scripts/hooks/* .git/hooks/

# 실행 권한 부여
chmod +x .git/hooks/*

echo "✅ Git hooks installed successfully!"
// package.json
{
"scripts": {
"prepare": "sh scripts/install-hooks.sh"
}
}

방법 3: Husky 사용 (Node.js 프로젝트)

Husky는 Git Hook을 쉽게 관리할 수 있는 도구입니다.

# Husky 설치
npm install --save-dev husky

# Husky 초기화
npx husky init

# Hook 추가
npx husky add .husky/pre-commit "npm run lint"
npx husky add .husky/commit-msg 'npx --no -- commitlint --edit "$1"'
// package.json
{
"scripts": {
"prepare": "husky install"
}
}

다양한 언어로 Hook 작성

Bash

#!/bin/sh
echo "Running bash hook"

Python

#!/usr/bin/env python3
import sys

def main():
print("Running Python hook")
return 0

if __name__ == "__main__":
sys.exit(main())

Node.js

#!/usr/bin/env node

console.log('Running Node.js hook');
process.exit(0);

Ruby

#!/usr/bin/env ruby

puts "Running Ruby hook"
exit 0

Hook 우회하기

테스트나 긴급한 상황에서 Hook을 일시적으로 우회할 수 있습니다.

# --no-verify 또는 -n 옵션 사용
git commit --no-verify -m "urgent fix"
git commit -n -m "urgent fix"

# 푸시 시
git push --no-verify

주의: 프로덕션 환경에서는 Hook을 우회하지 않는 것이 좋습니다.

실전 활용 예시

보안 검사 - Gitleaks 통합

#!/bin/sh
# .git/hooks/pre-commit

echo "Running Gitleaks security scan..."

gitleaks protect --verbose --redact --staged

if [ $? -eq 1 ]; then
echo "❌ Gitleaks detected sensitive information."
echo "Please remove secrets before committing."
exit 1
fi

echo "✅ No secrets detected"

코드 포맷팅 - Prettier 자동 실행

#!/bin/sh
# .git/hooks/pre-commit

files=$(git diff --cached --name-only --diff-filter=ACMR | grep -E '\.(js|jsx|ts|tsx|json|css|md)$')

if [ -n "$files" ]; then
echo "Formatting files with Prettier..."
echo "$files" | xargs npx prettier --write
echo "$files" | xargs git add
echo "✅ Files formatted"
fi

브랜치 네이밍 검증

#!/bin/sh
# .git/hooks/pre-push

branch_name=$(git symbolic-ref --short HEAD)
valid_pattern="^(feature|bugfix|hotfix|release)/[a-z0-9-]+$"

if ! echo "$branch_name" | grep -qE "$valid_pattern"; then
echo "❌ Invalid branch name: $branch_name"
echo "Branch name must follow pattern: type/description"
echo "Valid types: feature, bugfix, hotfix, release"
echo "Example: feature/user-authentication"
exit 1
fi

echo "✅ Branch name is valid"

Hook 디버깅

Hook이 제대로 동작하지 않을 때 디버깅 방법:

# Hook 스크립트에 디버그 모드 추가
#!/bin/sh
set -x # 모든 명령어 출력
set -e # 에러 발생 시 즉시 중단

# 로그 파일 생성
exec 2> /tmp/git-hook-debug.log
set -x

# 환경 변수 확인
echo "PATH: $PATH"
echo "PWD: $PWD"

# 스크립트 내용...

베스트 프랙티스

  1. 빠른 실행: Hook은 자주 실행되므로 빠르게 동작해야 합니다
  2. 명확한 에러 메시지: 실패 시 개발자가 문제를 쉽게 파악할 수 있도록 작성
  3. 점진적 도입: 처음에는 경고만 출력하고, 팀이 적응한 후 강제 적용
  4. 문서화: 프로젝트에 어떤 Hook이 설정되어 있는지 README에 명시
  5. 우회 가능: 긴급 상황을 위해 --no-verify 옵션 사용 가능하도록 유지
  6. 팀 공유: Husky 등의 도구를 사용하여 팀원 모두가 동일한 Hook 사용
  7. 적절한 범위: Pre-commit은 빠른 검사만, 전체 테스트는 CI/CD에서 수행

참고 자료

Share