Git Hook을 관리하는 도구 husky

husky는 commit, push 같은 git 이벤트가 발생할 때 특정 스크립트를 실행할 수 있게 도와주는 도구다. 예를 들어 커밋 전에 eslint를 돌리는 등등.

pre-commit, post-merge 등 13가지의 git hook을 모두 지원한다고 한다. git hook 리스트는 여기에서 https://git-scm.com/docs/githooks

설치와 기본 설정 #

설치는 당연히 npm으로. yarn이나 pnpm도 당연히 가능(난 pnpm을 썼었다)

npm install --save-dev husky

npx husky initpnpm exec husky init등의 명령어로 초기화를 할 수 있다. .husky 디렉토리가 생기고 그 안에 pre-commit 파일이 생긴다. 당연히 이름대로 pre-commit 파일은 커밋 전에 실행되는 스크립트가 들어가는 곳이다. 그리고 package.json에 prepare 스크립트가 추가된다.

pnpm add --save-dev husky
pnpm exec husky init

pre-commit 스크립트엔 npm test 가 들어 있다.

그리고 공식 문서에서는, 일반적으로 pre-commit 스크립트에는 몇 줄의 npx 명령어 정도만 넣게 된다고 한다. 실제로 그렇다. 공식 문서에서는 스테이징된 파일에 대해서만 린팅을 수행하는 2줄짜리 pre-commit 스크립트를 예시로 보여준다.

# .husky/pre-commit
prettier $(git diff --cached --name-only --diff-filter=ACMR | sed 's| |\\ |g') --write --ignore-unknown
git update-index --again

물론 스테이징된 파일들에 대한 더 다양한 처리가 필요하면 lint-staged 같은 패키지를 이용 가능. pre-commit 스크립트에서는 npx lint-staged 만 명령어를 실행하면 된다. 이에 대해서는 lint-staged 공식 문서 참고. 간단하게는 package.json에 이런 식으로 설정하면 된다.

{
  "lint-staged": {
    "*.{ts,tsx}": ["eslint --fix", "prettier --write"],
    "*.{json,css,md}": ["prettier --write"]
  }
}

그리고 npx lint-staged 명령어를 pre-commit 스크립트에 추가하면 precommit 시에 스테이징된 파일 중 위 형식을 만족하는 파일들에 대해서만 eslint와 prettier가 실행된다. lint-staged 문서에서는 더 다양한 설정 방법들이 있는데 나중에 필요해지면 찾아보도록 하자.

설정하기 #

훅을 더하는 것도 간단하다. ./husky/pre-commit 파일을 열어서 그냥 원하는 스크립트를 추가하면 된다. 예를 들어 npx eslint --fix 같은 명령어를 추가할 수 있다.

echo "npx eslint --fix" >> .husky/pre-commit

그리고 훅 파일의 스크립트 실행 전에 로컬 커맨드를 실행한다. 다음과 같은 파일 경로에서 가져온다. ~/.huskyrc 파일은 deprecated

hook skip하기 #

몇몇 경우 훅을 skip하고 싶을 수 있다. 당장 빨리 커밋을 해야 하는 게 있다든지 여러 이유가 있을 수 있겠다. 그럴 땐 대부분의 git 커맨드가 훅을 skip하는 --no-verify 옵션을 지원한다. 예를 들어 git commit --no-verify 같은 식으로. -n이라는 shorthand도 있다.

또는 HUSKY=0이라고 커맨드에 환경 변수를 설정해도 훅을 skip할 수 있다. 예를 들어 HUSKY=0 git commit 같은 식으로. 일시적으로 훅을 완전히 비활성화하고 나중에 다시 활성화할 때는 이렇게

export HUSKY=0
# do some git commands without hooks
unset HUSKY # 다시 훅 enabled 상태로 돌아감

물론 Git GUI 같은 걸 쓰고 있다면 이렇게 터미널에서 비활성화하기 어렵다. 그럴 땐 위에 있는 설정을 이용한다. ~/.config/husky/init.sh 파일에 export HUSKY=0을 추가한다. 그러면 모든 git hook 커맨드 실행 전에 저 설정 스크립트가 실행되므로 HUSKY=0도 실행되고 따라서 훅 비활성화가 된다.

ci에서의 실행 등 다른 옵션들은 husky의 How To 에서 확인 가능하다.

root dir이 아닌 곳에서 설정 #

모노레포 등의 환경에서는 package.json의 prepare 스크립트에서 디렉토리 변경 가능. https://typicode.github.io/husky/how-to.html#project-not-in-git-root-directory

.
├── .git/
├── backend/  # No package.json
└── frontend/ # Package.json with husky

이런 구조의 모노레포라고 해보자. 그럼 frontend/package.jsonprepare 스크립트에 다음과 같이 디렉토리 변경 명령어를 넣으면 된다.

"prepare": "cd .. && husky frontend/.husky"

pre-commit 등의 훅 스크립트에선 이렇게 디렉토리 변경 후 수행

# frontend/.husky/pre-commit
cd frontend
npm test

node version manager 이슈 #

이 문제를 해결하는 과정이 이 정리글을 쓰는 계기가 됐다. husky를 언제 한번 정리해야겠다는 생각도 있었고.

nvm 등의 node 관리도구를 쓰고 있고 git hook을 GUI로 쓰고 있다면 PATH 환경 변수를 찾지 못해서 나오는 command not found 문제가 뜰 수 있다.

PATH는 디렉토리 목록을 포함하는 환경 변수다. 그리고 shell은 커맨드 실행을 위해 PATH에 있는 디렉토리들을 순서대로 검색한다. 만약 그 과정에서 커맨드를 찾지 못하면 command not found 에러가 뜬다.

버전 매니저 동작 방식은 이렇다.

근데 gitKraken같은 GUI는 쉘이 아니기 때문에 .zshrc 같은 쉘 설정 파일이 실행되지 않는다.

해결법은 앞서 말했던 husky 설정이다. husky는 git hook이 실행되기 전에 ~/.config/husky/init.sh 파일을 실행한다. 따라서 여기에 node manager의 초기화 코드를 넣으면 GUI에서도 위와 같은 방식으로 node의 경로가 PATH에 추가된다.

nvm의 경우에는 공식에서 이렇게 만들어 줬다. 이걸 ~/.config/husky/init.sh 파일에 추가하면 된다.

# ~/.config/husky/init.sh
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm

만약 쉘 설정 파일이 가볍다면 그냥~/.config/husky/init.sh. ~/.zshrc`을 추가해도 됨

잡다한 설정 #

참고 #

husky get started https://typicode.github.io/husky/get-started.html

lint-staged https://github.com/lint-staged/lint-staged