Git의 pre-commit 훅을 활용한 자동 품질 보증에 대해, 비밀 유출 방지부터 포맷터/린터/테스트 실행, 온보딩과 성능 이슈, 워크플로 권장사항과 대안을 포괄적으로 다룬 글.
Git pre-commit 훅은 소프트웨어 프로젝트에 기여할 때 자동 품질 보증을 위해 흔히 쓰이는 도구다. 사용자가 커밋을 만들 때 기본적으로 실행되며, 실패(즉, 0이 아닌 종료 코드로 종료)하면 커밋 생성을 막는다. pre-commit 훅을 설정하는 일은 .git/hooks 폴더에 pre-commit이라는 스크립트를 넣는 것만큼 간단하다. pre-commit 훅은 강력한 도구가 될 수 있는데, 버전 관리에 민감한 정보를 추적시키는 일을 막을 수 있는 마지막 기회이기 때문이다. 이런 정보가 너무 늦게 발견되면 정리하기가 매우 어려울 수 있다.
이 글은 pre-receive 훅이나 그 밖의 Git 훅에 대해서는 다루지 않는다. 그들에겐 다른 장단점이 있을 수 있다. 여기서는 가장 널리 쓰이고, 필자가 최근에 마주한 pre-commit 훅에만 집중한다.
훅 폴더는 Git의 버전 관리 대상이 아니므로, 프로젝트의 새 기여자가 pre-commit 훅을 설정하는 일은 사소하지 않으며 손쉬운 설정을 위해 몇 가지 도구가 필요하다. 이를 위한 다양한 도구가 있다. Pre-Commit은 독립 실행형 도구이고, Prek는 Pre-Commit을 러스트로 재구현하는 진행 중인 작업이며, Husky는 Node.js와 npm이 필요하고 다른 Git 훅도 지원한다.
Pre-Commit은 자신을 위해 작성·패키징된 훅의 의존성 관리를 포함한다. Gitleaks, ESLint, Nixfmt 같은 일반적인 품질 보증 도구는 Pre-Commit용 매니페스트와 구성을 포함하여, Pre-Commit으로 설치해 사용할 수 있게 한다.
Husky는 사용자가 npm으로 Husky와 그 밖의 의존성을 설치한다는 전제에 기대며, 훅은 주로 JavaScript/TypeScript로 작성된다. 이는 일반적인 소프트웨어 프로젝트 전반의 도구라기보다 생태계 내부의 해결책에 가깝다.
외부 도구 없이 훅을 설정하고 이를 Git 버전 관리로 추적하는 흔하고 간단한 방법은 훅 폴더에서 저장소로 심볼릭 링크를 거는 것이다. 예: ln -s $(pwd)/scripts/pre-commit .git/hooks/pre-commit. 이 심볼릭 링크는 저장소를 클론한 뒤 한 번만 만들면 되므로 장기 기여자들에게는 잘 작동한다. 하지만 온보딩 과정에서는 쉽게 간과되는 단계이기도 하다.
비밀 문자열은 결코 버전 관리에 커밋되어서는 안 되며, pre-commit 훅은 이를 방지할 수 있는 마지막 시점이다. 한 번 커밋되면 제거하기가 어렵고, 실수로 푸시하면 경우에 따라 공개 원격 저장소에도 비밀이 담기게 된다. 숙련된 Git 사용자는 이를 수습할 방법을 찾을 수 있겠지만, 애초에 이런 일이 일어나지 않게 막는 편이 더 안전하고, 경험이 적은 사용자의 실수를 예방하는 데도 도움이 된다. Gitleaks 같은 도구는 변경된 파일을 빠르게 스캔해 API 토큰, SSH 키, GPG 키 등 흔한 비밀 문자열 패턴을 찾아낸다. Gitleaks를 pre-commit 훅에 포함하는 것은 빠르고 방해가 적으며 즉각적인 피드백을 제공하므로, 큰 마찰 없이 막대한 보안 이점을 준다.
이 밖의 변경사항들은 원격 저장소로 푸시되더라도 반드시 문제가 되는 것은 아니지만, 코드 리뷰에서 대개 거절될 것이다. 즉, 포매팅 문제, 린터 경고/오류, 실패하는 테스트다. 포매팅 문제는 pre-commit 훅에서 자동 포매팅을 실행해 예방할 수 있으며, 이때 사용자의 개입은 0이다. 개인적으로 파일을 검토하지 않고 커밋하거나, 심지어 커밋 시 자동으로 변경하는 것이 썩 기분 좋지는 않지만, 포매터의 경우 결과에 대한 논쟁의 여지가 없고 무언가 잘못되더라도 이후 코드 리뷰에서 잡힌다. 린터 문제와 실패하는 테스트는 보통 PR 머지 전 QA 파이프라인에서 발견되므로, 로컬에서 이를 검사할 필요가 엄밀히 말해 없을 수도 있지만, 일찍 잡아내면 피드백 루프가 더 촘촘해진다.
이 도구들은 때때로(프로젝트 크기에 따라) 실행에 시간이 걸릴 수 있으며, 이는 워크플로의 마찰을 키운다. 워크플로를 방해한다면 사용 여부를 재고해야 한다.
대부분의 프로젝트는 푸시 전에 포매팅, 린팅, 테스트를 수행하는 스크립트를 갖고 있다. 하지만 개발자들은(나 역시 예외가 아니다) 이를 실행하는 것을 종종 잊어버리고, 그 결과 2분 뒤 CI 파이프라인이 실패해 머지를 막는 일을 겪는다. pre-commit 훅이 이러한 스크립트를 자동으로 실행하면 인지적 부담을 줄여주고, 코드 리뷰 전에 문제가 잡힐 가능성을 높여준다.
대부분의 개발자는 작업 중 점진적인 변화를 추적하기 위해 Git을 사용한다. 며칠 일을 모아서 한 번만 커밋하는 것이 아니라, 하루에도 여러 번 커밋한다. 좋은 개발자는 나중에 커밋 히스토리를 정리해 커밋 메시지를 개선하고, 커밋이 개별적으로 의미론적으로 완결되도록 만든다. 하지만 그 전 단계의 커밋은 미완성 상태인 경우가 많고, 린터 오류가 포함되거나 무엇보다 테스트가 실패할 수 있다. 테스트 실패 때문에 커밋을 막는 것은 워크플로에 심각한 제동을 걸 수 있으며, 개발자들이 pre-commit 훅을 끄는 습관을 들이게 만든다. 어떤 이들은 이를 위한 별칭까지 만들기도 한다.
pre-commit 훅이 실행되려면 Git에 이를 설치해 인식시키는 단계가 필요하다. 즉, 저장소를 클론한 뒤 추가 설정이 하나 더 필요하며, 이는 모든 온보딩 과정에서 한 단계가 더해진다는 뜻이다. 꼭 문제라고 할 수는 없지만, 주의와 문서화가 필요하다. 설정은 다른 설정 단계와 통합해 단순화할 수 있다. 예를 들어 개발 환경을 설정하는 데 devenv를 사용하면 된다. direnv 같은 것을 사용하면 아예 전적으로 자동화할 수도 있다.
작은 커밋을 자주 만드는 워크플로라면, 오래 걸리는 훅은 정말 성가시며 앞서 설명한 상황—개발자들이 습관적으로 pre-commit 훅을 비활성화하는—을 초래할 수 있다.
가장 큰 이점인 “비밀 유출을 완전히 차단”은 pre-commit 훅을 설정할 만한 충분한 가치가 있다. 하지만 실제로 사람들이 사용하게 만들려면, 훅을 빠르게 유지하고 개발 워크플로의 마찰을 줄이는 방식으로 설정해야 한다. 구체적으로 무엇을 의미하는지는 프로젝트와 팀에 따라 다르다. 포매터는 빠르고, 대부분의 린터도 그러하며 수동 개입 없이 문제를 고치거나 나머지는 무시하도록 설정할 수 있다. 즉, 커밋이 차단되는 일은 없고 어느 정도의 QA가 자동으로 보장된다. 훅에서 테스트를 실행하는 것은 아마 좋지 않은 생각인데, 작업 중인 커밋을 막고 진짜 골칫거리가 될 수 있기 때문이다.
최근 나는 개발 환경을 설정하는 데 devenv를 쓰기 시작했다. Devenv는 Pre-Commit을 통합하고 pre-commit 훅의 설치와 관리를 매우 쉽게 만든다. 덕분에 온보딩이 아주 쉬워지고, Pre-Commit의 패키지 관리를 제거하여 Nix로 패키지 관리를 통합할 수 있으며, pre-commit 훅을 중앙의 devenv test 명령에까지 통합해준다.
지금까지는 꽤 괜찮은 워크플로를 제공한다. pre-commit 훅이 포매팅 문제와 일부 린터 문제를 자동으로 고쳐주는 것은 꽤 만족스럽다. 아직 대형 프로젝트에서 이 모든 것을 충분히 시험해 보지는 못했고, 코드가 많아지면 마찰이 훨씬 커질 것이라 의심하지만, 실제로 시험해 본 뒤 재평가할 생각이다.
어떤 프로젝트에서는 make reviewable이라는 명령이나 스크립트를 두어 전체 QA 파이프라인을 실행하고 현재 저장소 상태가 리뷰 가능 상태인지 사용자에게 알려주도록 한다. 이는 훨씬 수동적이지만, 일반적인 개발 워크플로를 방해하지 않으며 더 명시적이다. 다만 사용자에게 이를 알려야 하고, 사용자가 이를 능동적으로 사용해야 한다. 이런 프로젝트의 코드 리뷰는 “이 스크립트를 실행해 주세요” — “아, 이게 있는지 몰랐어요/잊었어요” 같은 피드백 루프로 시작하는 경우가 많다. 자동화가 더 많을수록 이 과정은 더 매끄러워진다.