Git Hook 스크립트
팀 프로젝트를 하다 보면 이런 일이 생깁니다. 누군가 console.log("디버깅 중")을 지우지 않은 채로 커밋을 올립니다. 다른 팀원이 TODO가 가득한 미완성 코드를 main 브랜치에 병합합니다. 커밋 메시지는 제각각이어서 히스토리를 봐도 무슨 변경인지 알 수 없습니다.
Git Hook이 이 문제를 해결합니다. 커밋, 푸시, 체크아웃 같은 Git 이벤트가 발생하는 순간 자동으로 스크립트를 실행해 규칙을 강제합니다.
Git Hook이란
Git Hook은 Git 이벤트에 반응해 자동으로 실행되는 쉘 스크립트입니다. 프로젝트의 .git/hooks/ 디렉토리에 저장됩니다.
# .git/hooks/ 디렉토리 내용 확인ls -la .git/hooks/
-rwxr-xr-x applypatch-msg.sample
-rwxr-xr-x commit-msg.sample
-rwxr-xr-x pre-commit.sample
-rwxr-xr-x pre-push.sample
-rwxr-xr-x post-update.sample
...
.sample 확장자가 붙은 예제 파일들이 있습니다. 이 파일들은 실제로 실행되지 않습니다. Hook을 활성화하려면 .sample을 제거하고 실행 권한을 부여하면 됩니다.
클라이언트 Hook 종류
로컬 작업에서 실행되는 Hook입니다. 커밋, 브랜치 전환 등 개발자의 로컬 Git 작업에 반응합니다.
| Hook 이름 | 실행 시점 | 0이 아닌 종료 코드 효과 |
|---|---|---|
pre-commit |
git commit 실행 직후, 메시지 입력 전 |
커밋 취소 |
prepare-commit-msg |
커밋 메시지 편집기 열리기 전 | 커밋 취소 |
commit-msg |
커밋 메시지 입력 후 | 커밋 취소 |
post-commit |
커밋 완료 후 | 영향 없음 (알림 용도) |
post-checkout |
git checkout 완료 후 |
영향 없음 |
post-merge |
git merge 완료 후 |
영향 없음 |
pre-push |
git push 직전 |
푸시 취소 |
서버 Hook 종류
원격 저장소(GitHub, GitLab 등)의 서버에서 실행되는 Hook입니다.
| Hook 이름 | 실행 시점 |
|---|---|
pre-receive |
푸시 수신 직전 |
post-receive |
푸시 수신 완료 후 |
update |
각 브랜치 업데이트 직전 |
서버 Hook은 보통 관리자가 설정합니다. GitHub의 경우 서버 Hook 대신 GitHub Actions를 사용합니다.
Hook 스크립트 작성법
Hook 스크립트는 일반 쉘 스크립트와 동일하게 작성합니다. 파일명이 Hook 이름과 정확히 일치해야 하고, 실행 권한이 있어야 합니다.
# Hook 파일 생성touch .git/hooks/pre-commit# 실행 권한 부여 (필수!)chmod +x .git/hooks/pre-commit
Hook 스크립트가 0을 반환하면 Git 작업이 계속됩니다. 0이 아닌 값을 반환하면 Git 작업이 중단됩니다. pre-commit, commit-msg, pre-push에서 이 특성을 활용해 문제가 있을 때 커밋이나 푸시를 막습니다.
실습: pre-commit Hook 작성
TODO/FIXME가 남아 있거나, 디버그용 출력 코드가 있거나, 대용량 파일을 커밋하려 할 때 자동으로 막는 pre-commit Hook을 작성합니다.
# 새 파일: .git/hooks/pre-commit#!/bin/bash# pre-commit: 커밋 전 코드 품질 검사set -euo pipefail# 컬러 출력RED='\033[0;31m'YELLOW='\033[1;33m'GREEN='\033[0;32m'NC='\033[0m'# 스테이지된 파일 목록 (삭제된 파일 제외)STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM)if [ -z "$STAGED_FILES" ]; then exit 0fiERRORS=0echo "커밋 전 코드 품질 검사를 시작합니다..."echo ""# ─── 1. TODO/FIXME 검사 ───────────────────────────────────────────────────────echo "▶ TODO/FIXME 검사 중..."TODO_FILES=""while IFS= read -r file; do # 바이너리 파일 건너뜀 if file "$file" | grep -q "binary"; then continue fi if grep -nE "(TODO|FIXME|HACK|XXX)" "$file" > /dev/null 2>&1; then TODO_FILES="$TODO_FILES\n $file" fidone <<< "$STAGED_FILES"if [ -n "$TODO_FILES" ]; then echo -e "${YELLOW} 경고: 다음 파일에 TODO/FIXME가 남아 있습니다.${NC}" echo -e "$TODO_FILES" echo "" echo " 해결하거나, 의도적이라면 --no-verify 옵션으로 건너뛸 수 있습니다." echo "" # 경고만 출력 (커밋 막지 않음)fi# ─── 2. 디버그 코드 감지 ──────────────────────────────────────────────────────echo "▶ 디버그 코드 감지 중..."DEBUG_PATTERNS="console\.log|print\(|var_dump|debugger|binding\.pry|byebug"DEBUG_FOUND=0while IFS= read -r file; do if file "$file" | grep -q "binary"; then continue fi MATCHES=$(grep -nE "$DEBUG_PATTERNS" "$file" 2>/dev/null || true) if [ -n "$MATCHES" ]; then if [ $DEBUG_FOUND -eq 0 ]; then echo -e "${RED} 오류: 디버그 코드가 발견되었습니다.${NC}" DEBUG_FOUND=1 ERRORS=$((ERRORS + 1)) fi echo " 파일: $file" echo "$MATCHES" | while IFS= read -r line; do echo " $line" done fidone <<< "$STAGED_FILES"if [ $DEBUG_FOUND -eq 0 ]; then echo -e "${GREEN} 통과${NC}"fiecho ""# ─── 3. 파일 크기 제한 ───────────────────────────────────────────────────────echo "▶ 파일 크기 검사 중..."MAX_SIZE_KB=500 # 500KB 제한MAX_SIZE_BYTES=$((MAX_SIZE_KB * 1024))SIZE_ERROR=0while IFS= read -r file; do if [ ! -f "$file" ]; then continue fi FILE_SIZE=$(wc -c < "$file") if [ "$FILE_SIZE" -gt "$MAX_SIZE_BYTES" ]; then FILE_SIZE_KB=$((FILE_SIZE / 1024)) echo -e "${RED} 오류: $file (${FILE_SIZE_KB}KB) — ${MAX_SIZE_KB}KB 제한 초과${NC}" SIZE_ERROR=1 ERRORS=$((ERRORS + 1)) fidone <<< "$STAGED_FILES"if [ $SIZE_ERROR -eq 0 ]; then echo -e "${GREEN} 통과 (모든 파일이 ${MAX_SIZE_KB}KB 이하)${NC}"fiecho ""# ─── 결과 ────────────────────────────────────────────────────────────────────if [ $ERRORS -gt 0 ]; then echo -e "${RED}커밋이 취소되었습니다. 위 오류를 수정하세요.${NC}" echo "수정 없이 커밋하려면: git commit --no-verify" exit 1fiecho -e "${GREEN}모든 검사를 통과했습니다. 커밋합니다.${NC}"exit 0
Hook을 설치하고 테스트해봅니다.
# 실습 환경 준비mkdir ~/hook-test && cd ~/hook-testgit init# Hook 파일 생성 (위 코드 내용 작성 후)chmod +x .git/hooks/pre-commit# 테스트 1: 디버그 코드가 있는 파일 커밋 시도echo 'console.log("debug")' > test.jsgit add test.jsgit commit -m "test"
커밋 전 코드 품질 검사를 시작합니다...
▶ TODO/FIXME 검사 중...
▶ 디버그 코드 감지 중...
오류: 디버그 코드가 발견되었습니다.
파일: test.js
1:console.log("debug")
▶ 파일 크기 검사 중...
통과 (모든 파일이 500KB 이하)
커밋이 취소되었습니다. 위 오류를 수정하세요.
수정 없이 커밋하려면: git commit --no-verify
디버그 코드를 제거하면 커밋이 성공합니다.
# 정상 파일로 교체echo 'function greet(name) { return "Hello, " + name; }' > test.jsgit add test.jsgit commit -m "add greet function"
커밋 전 코드 품질 검사를 시작합니다...
▶ TODO/FIXME 검사 중...
▶ 디버그 코드 감지 중...
통과
▶ 파일 크기 검사 중...
통과 (모든 파일이 500KB 이하)
모든 검사를 통과했습니다. 커밋합니다.
[main (root-commit) a1b2c3d] add greet function
Hook 공유하기
.git/hooks/ 디렉토리는 Git으로 추적되지 않습니다. 팀원과 Hook을 공유하려면 프로젝트 루트에 별도 디렉토리를 만들고 심볼릭 링크나 설치 스크립트를 사용합니다.
# 프로젝트에 hooks 디렉토리 만들기mkdir scripts/hookscp .git/hooks/pre-commit scripts/hooks/# 팀원이 실행할 설치 스크립트cat > scripts/install-hooks.sh << 'EOF'#!/bin/bashHOOKS_DIR="$(git rev-parse --show-toplevel)/scripts/hooks"GIT_HOOKS_DIR="$(git rev-parse --show-toplevel)/.git/hooks"for hook in "$HOOKS_DIR"/*; do hook_name=$(basename "$hook") ln -sf "$hook" "$GIT_HOOKS_DIR/$hook_name" echo "설치: $hook_name"doneecho "Git Hook 설치 완료."EOFchmod +x scripts/install-hooks.sh
팀원은 저장소를 클론한 후 ./scripts/install-hooks.sh를 한 번만 실행하면 됩니다.