워크플로우 자동화: Git 클라이언트 측 훅 실전 가이드

Git 클라이언트 측 훅을 사용하여 빠른 로컬 검사, 공유 설정, 커밋 메시지 규칙 및 안전한 병합 후 자동화를 구현하세요.

워크플로우 자동화: Git 클라이언트 측 훅 실전 가이드

Git 클라이언트 측 훅은 Git이 워크플로우의 특정 지점에 도달했을 때 로컬 머신에서 실행되는 작은 스크립트입니다. pre-commit 훅은 커밋이 생성되기 전에 실행됩니다. commit-msg 훅은 메시지를 작성한 후 Git이 승인하기 전에 실행됩니다. post-merge 훅은 병합이 완료된 후 실행됩니다. 잘 사용하면 훅은 지루한 실수를 조기에 잡아냅니다: 포맷팅 누락, 깨진 생성 파일, 누락된 종속성 설치, 또는 팀 규칙과 일치하지 않는 커밋 메시지 등입니다.

중요한 제한 사항은 클라이언트 측 훅이 로컬에만 존재한다는 점입니다. 저장소를 클론할 때 자동으로 함께 전송되지 않습니다. 따라서 빠른 피드백과 로컬 편의성에는 좋지만, 팀 규칙을 강제하는 유일한 수단으로는 부족합니다. 검사가 메인 브랜치를 진정으로 보호해야 한다면 CI나 서버 측 규칙에도 추가하세요.

모든 저장소에는 .git/hooks 디렉토리에 훅 디렉토리가 있습니다:

ls .git/hooks

새 저장소에는 일반적으로 pre-commit.sample과 같은 샘플 파일이 포함되어 있습니다. 샘플 훅은 .sample 접미사 없이 실행 가능한 파일을 만들 때까지 아무 작업도 수행하지 않습니다:

cp .git/hooks/pre-commit.sample .git/hooks/pre-commit
chmod +x .git/hooks/pre-commit

훅은 셸 스크립트, Python 스크립트, Ruby 스크립트, Node 스크립트 또는 머신이 실행할 수 있는 모든 것이 될 수 있습니다. 첫 번째 줄은 인터프리터를 가리켜야 합니다:

#!/usr/bin/env bash

대부분의 팀에게 더 나은 장기적 패턴은 모든 노트북에서 .git/hooks를 수동으로 편집하는 것이 아닙니다. 훅 스크립트를 저장소에 저장한 다음 Git이 해당 디렉토리를 사용하도록 구성하세요:

git config core.hooksPath .githooks
mkdir -p .githooks

이제 .githooks/pre-commit의 훅을 일반 프로젝트 코드처럼 커밋하고 검토할 수 있습니다. 각 개발자는 여전히 core.hooksPath 설정이 필요하지만, 설정을 부트스트랩 스크립트에 추가하거나 온보딩 문서에 포함시킬 수 있습니다.

유용한 Pre-Commit 훅

좋은 pre-commit 훅은 빠르고 집중적이어야 합니다. 모든 커밋에 2분이 걸리면 사람들은 git commit --no-verify로 우회할 것이고, 훅은 잡음이 될 것입니다. 프로젝트가 충분히 작아서 정말 빠르지 않는 한 전체 테스트 스위트는 CI에 맡기세요.

다음은 스테이징된 파일만 검사하는 실용적인 셸 훅입니다. 이 구분이 중요합니다. 작업 트리에 아직 테스트하고 싶지 않은 미완성 작업이 있을 수 있습니다. 커밋은 스테이징된 내용으로 판단되어야 합니다.

.githooks/pre-commit 생성:

#!/usr/bin/env bash
set -u

changed_files=$(git diff --cached --name-only --diff-filter=ACMR)

if [ -z "$changed_files" ]; then
  exit 0
fi

if git diff --cached --check; then
  :
else
  echo "커밋 전에 공백 오류를 수정하세요."
  exit 1
fi

secret_matches=$(git diff --cached --name-only --diff-filter=ACMR | xargs grep -nE 'AKIA[0-9A-Z]{16}|BEGIN RSA PRIVATE KEY' 2>/dev/null || true)
if [ -n "$secret_matches" ]; then
  echo "스테이징된 파일에서 비밀 정보가 발견되었습니다:"
  echo "$secret_matches"
  exit 1
fi

python_files=$(printf '%s\n' "$changed_files" | grep '\\.py$' || true)
if [ -n "$python_files" ]; then
  printf '%s\n' "$python_files" | while IFS= read -r file; do
    [ -f "$file" ] || continue
    python3 -m py_compile "$file" || exit 1
  done
fi

exit 0

이 훅은 세 가지 간단한 작업을 수행합니다: Git이 공백 오류를 감지하도록 하고, 스테이징된 파일에서 몇 가지 명백한 비밀 패턴을 확인하며, 변경된 Python 파일을 컴파일합니다. 실제 비밀 스캐너나 테스트 스위트를 대체하지는 않습니다. 빠른 경보 장치입니다.

일반적인 실수는 파일 내용 대신 파일 이름에 grep을 사용하는 것입니다. 이 잘못된 패턴은 경로에 TODO가 포함되어 있는지만 확인하고 파일에 포함되어 있는지는 확인하지 않습니다:

git diff --cached --name-only | grep TODO

TODO 주석을 차단하려면 대신 스테이징된 diff를 검사하세요:

if git diff --cached -U0 | grep -E '^\\+.*TODO:'; then
  echo "스테이징된 TODO 주석이 발견되었습니다."
  exit 1
fi

그래도 주의하세요. 일부 팀은 TODO 주석을 책임감 있게 사용합니다. 모든 TODO를 차단하는 것은 도움보다는 방해가 될 수 있습니다.

커밋 메시지 훅

commit-msg 훅은 첫 번째 인수로 임시 커밋 메시지 파일의 경로를 받습니다. 이는 "모든 커밋은 티켓 ID로 시작해야 함" 또는 "Conventional Commits 사용"과 같은 규칙에 유용합니다.

간단한 예:

#!/usr/bin/env bash
set -u

message_file="$1"
first_line=$(head -n 1 "$message_file")

if printf '%s' "$first_line" | grep -Eq '^(feat|fix|docs|test|refactor|chore)(\\(.+\\))?: .+'; then
  exit 0
fi

echo "커밋 메시지는 다음과 같아야 합니다: fix(api): handle empty token"
exit 1

이것은 릴리스 노트나 체인지로그가 커밋에서 생성될 때 유용합니다. 팀이 스쿼시 병합을 사용하고 PR 제목을 다시 작성하는 경우에는 덜 유용합니다. 실제 사용하는 워크플로우에 맞게 훅을 조정하세요.

Post-Merge 훅

post-merge 훅은 작업 트리가 변경된 후 로컬 정리에 가장 적합합니다. 전형적인 예는 lockfile이 변경된 후 종속성을 새로 고치는 것입니다.

#!/usr/bin/env bash
set -u

previous_head="HEAD@{1}"

if git diff --name-only "$previous_head" HEAD | grep -Eq '(^package-lock\\.json$|^pnpm-lock\\.yaml$|^yarn\\.lock$)'
then
  if command -v npm >/dev/null 2>&1 && [ -f package-lock.json ]; then
    echo "Lockfile이 변경되었습니다. npm install을 실행합니다."
    npm install
  fi
fi

if git diff --name-only "$previous_head" HEAD | grep -q '^\\.gitmodules$'; then
  echo "서브모듈 구성이 변경되었습니다. 서브모듈을 동기화합니다."
  git submodule sync --recursive
  git submodule update --init --recursive
fi

이 훅은 놀라운 변경을 해서는 안 됩니다. 종속성을 설치하는 경우 무엇을 하고 있는지 출력하세요. 설치가 실패하면 개발자에게 복구 방법을 알려주세요. 작업 트리를 조용히 변경하는 훅은 신뢰하기 어렵습니다.

훅 공유하기: 문제 없이

훅을 공유하는 세 가지 일반적인 방법이 있습니다.

가장 간단한 방법은 core.hooksPath를 사용하는 것입니다. 저장소에 .githooks/를 포함하고 설정 시 Git이 이를 사용하도록 합니다. 이는 투명하며 추가 패키지 관리자가 필요하지 않습니다.

JavaScript 프로젝트는 종종 Husky를 사용합니다. 이는 npm, pnpm 또는 yarn 설치 흐름과 통합되기 때문입니다. 모든 기여자가 이미 Node 도구 체인을 사용하는 경우 좋은 선택이 될 수 있습니다.

많은 혼합 언어 팀은 pre-commit 프레임워크를 사용합니다. 이는 .pre-commit-config.yaml에 정의된 훅을 설치하고 실행하며, 포매터, 린터, 파일 검사와 같은 도구의 고정된 버전을 사용합니다. 또 다른 도구를 추가하지만, "모든 곳에 동일한 훅을 어떻게 설치할까?" 문제를 위키 페이지보다 더 잘 해결합니다.

제가 피하는 것은 큰 스크립트를 수동으로 .git/hooks에 복사하는 것입니다. 아무도 검토하지 않고, 어떤 버전이 설치되었는지 알 수 없으며, 디버깅은 개인적인 고고학이 됩니다.

훅 디버깅

훅이 실행되지 않을 때는 다음 순서로 확인하세요:

git config --get core.hooksPath
ls -l .git/hooks .githooks 2>/dev/null

core.hooksPath가 설정되면 Git은 .git/hooks를 무시하고 구성된 디렉토리를 사용합니다. macOS나 Linux에서 훅 파일이 실행 가능하지 않으면 Git은 실행하지 않습니다:

chmod +x .githooks/pre-commit

훅이 실행되지만 신비롭게 실패하는 경우 임시 추적을 추가하세요:

set -x
pwd
env | sort

훅은 일반적인 Git 사용에서는 저장소 루트에서 실행되지만, GUI 클라이언트와 IDE는 경로나 환경 차이를 노출할 수 있습니다. 린터나 패키지 관리자가 사용 가능하다고 가정하기 전에 훅 내부에서 command -v toolname을 사용하세요.

또한 우회 스위치를 기억하세요:

git commit --no-verify

이것은 그 자체로 보안 허점이 아닙니다. Git이 작동하는 방식입니다. 심각한 강제는 CI나 보호된 브랜치 규칙에 속한다는 또 다른 이유입니다.

합리적인 훅 정책

빠르고 결정적이며 설명하기 쉬운 검사에 훅을 사용하세요. 스테이징된 파일 포맷팅, 공백 오류 잡기, 커밋 메시지 검증, 개발자에게 종속성 설치 알림 등이 좋은 후보입니다. 네트워크 액세스가 필요하거나, 오래 걸리거나, 취약한 로컬 상태에 의존하는 훅은 피하세요.

훅이 커밋을 차단하는 경우, 메시지는 정확히 무엇이 실패했는지와 해결 방법을 알려야 합니다. "훅 실패"는 충분하지 않습니다. 병합이나 프로덕션 핫픽스 중인 개발자는 명확한 다음 명령이 필요합니다.

클라이언트 측 Git 훅은 지역 관료제보다는 도움이 되는 가드레일처럼 느껴질 때 가장 잘 작동합니다. 작게 유지하고, 버전을 관리하며, 최종 권한은 CI에 두세요.

긴급 상황에서 훅을 친근하게 유지하기

훅은 긴급 수정 중에 누군가를 가두지 않고 정상 작업 중에 도움이 되어야 합니다. 즉, 모든 차단 훅은 명확한 실패 메시지와 현실적인 탈출구가 필요합니다. Git은 이미 커밋 및 푸시 훅에 대해 --no-verify를 제공하지만, 팀은 우회가 허용되는 시기를 결정해야 합니다. 프로덕션 핫픽스는 개발자가 서두르기 때문에 포맷팅을 건너뛰는 것과 다릅니다.

좋은 훅 메시지는 무엇이 실패했는지, 어디서 실패했는지, 그리고 다음에 무엇을 실행해야 하는지 알려줍니다:

echo "ESLint가 스테이징된 JavaScript 파일에서 실패했습니다."
echo "실행: npm run lint -- --fix"
exit 1

나쁜 메시지는 단지 failed라고만 하거나 컨텍스트 없이 도구 출력 페이지를 덤프합니다. 사람들은 그런 종류의 훅을 무시하는 법을 배웁니다.

훅이 파일을 수정하는 경우 특히 주의하세요. 포매터는 pre-commit에서 유용할 수 있지만, 파일의 스테이징되지 않은 부분을 변경할 때 혼란을 일으킬 수도 있습니다. 많은 팀은 훅에서 포맷팅을 확인하고 개발자가 수동으로 포매터를 실행하는 것을 선호합니다. 다른 팀은 스테이징된 청크만 포맷하는 도구를 사용합니다. 하나의 동작을 선택하고 저장소에 문서화하세요. 사라지는 채팅 스레드가 아닌 저장소에 문서화하세요.

팀의 경우, 훅 변경을 애플리케이션 코드처럼 검토하세요. 훅은 모든 커밋을 느리게 하거나, 환경 세부 정보를 로그에 유출하거나, Bash 전용 동작을 가정하는 경우 Windows 기여자에게 문제를 일으킬 수 있습니다. 프로젝트에 Windows 기여자가 있는 경우 Git Bash에서 훅을 테스트하거나 크로스 플랫폼 훅 러너를 사용하세요. 프로젝트에 컨테이너나 개발 셸이 있는 경우 앱과 동일한 환경에서 훅을 실행하여 모든 사람이 동일한 도구 버전을 사용하도록 고려하세요.

최고의 훅은 모든 것이 정상일 때는 거의 보이지 않고, 문제가 있을 때는 매우 구체적입니다. 그것이 목표로 삼아야 할 기준입니다.

훅을 제품 코드처럼 버전 관리하기

훅 스크립트는 개발자 경험의 일부가 됩니다. 고장나면 모든 기여자가 느낍니다. 스크립트를 작게 유지하고, 도우미 함수를 명확하게 이름 짓고, 간단한 명령으로 충분할 때 영리한 셸 트릭을 피하세요. 훅이 한두 화면을 넘어가면 실제 로직을 테스트된 프로젝트 스크립트로 옮기고 훅이 해당 스크립트를 호출하도록 하세요.

예를 들어, .githooks/pre-commit에 긴 린트 루틴을 포함하는 대신 다음을 호출하세요:

./scripts/check-staged-files.sh

이 스크립트는 개발자, 훅, CI에서 실행할 수 있습니다. 또한 개발자가 커밋을 가장하지 않고 실패를 재현할 수 있습니다. 재현 가능성은 도움이 되는 훅과 신비로운 로컬 장애물의 차이입니다.

가능하면 도구 버전을 고정하세요. PATH에서 먼저 발견되는 black, eslint 또는 prettier를 호출하는 훅은 머신마다 다르게 동작할 수 있습니다. 프로젝트 로컬 종속성, lockfile, 컨테이너 또는 버전 관리자는 훅 출력을 더 예측 가능하게 만듭니다.

마지막으로, 훅을 저장소 범위로 유지하세요. 전역 훅은 편리해 보이지만, 몇 달 후 관련 없는 저장소가 오래된 개인 규칙으로 인해 실패하기 시작할 때 종종 놀라게 합니다. 전역 훅은 팀 정책이 아닌 진정한 개인적 선호에만 사용하세요.

마지막 실용적인 규칙: 훅이 명령이 존재하는 유일한 장소가 되지 않도록 하세요. 훅이 스테이징된 Python 파일을 검사하는 경우 해당 명령을 스크립트나 태스크 러너에도 유지하세요. 개발자는 Git이 방해하기 전에 의도적으로 동일한 검사를 실행할 수 있어야 합니다.

오픈 소스 프로젝트의 경우, 기여자가 아직 전체 도구 체인을 설치하지 않았을 수 있다고 가정하세요. 친숙한 설정 메시지와 함께 실패하는 훅은 괜찮습니다. 누락된 로컬 바이너리에서 스택 추적을 던지는 훅은 고장난 것처럼 느껴집니다. 더 무거운 명령을 실행하기 전에 전제 조건을 확인하고 프로젝트에서 사용하는 설정 명령을 알려주세요.

또한 부분 커밋에 대해 생각하세요. 많은 경험 많은 개발자는 파일의 일부만 스테이징합니다. 전체 파일을 포맷하는 훅은 의도치 않게 스테이징되지 않은 작업을 커밋으로 가져올 수 있습니다. 팀이 부분 커밋을 자주 사용하는 경우 스테이징된 diff를 읽는 검사나 스테이징된 콘텐츠용으로 설계된 도구를 선호하세요.

훅이 계속 우회된다면 그것을 피드백으로 처리하세요. 검사가 너무 느리거나, 실패 메시지가 불명확하거나, 규칙이 로컬 커밋 경로 대신 CI에 속하는 것입니다. Git이 제공하는 우회를 사용하는 개발자를 비난하기보다는 마찰을 수정하세요.