Skip to content

기술 공유 git hook과 husky lint 자동화

ChangMyeong Lee edited this page Nov 19, 2022 · 6 revisions

git hook

깃 훅이란 git에 특정 이벤트가 발생했을 때 특정 스크립트를 실행되도록 하는 것을 의미한다.

일반적으로 깃 훅은 0을 반환할 경우 해당 작업을 그대로 수행하고(ex. commit, push ) 그렇지 않을 경우 각 작업을 반려한다.

깃 훅 설정을 위해서는 .git/hooks 경로를 사용하면 된다. 해당 경로에 가보면, git 에서 자동으로 생성한 .sample 로 끝나는 예제 파일들이 존재한다. 이를 적용하고 싶다면, .sample 을 빼면 된다. 깃 훅 로직은 shell, Perl 뿐 아니라 Ruby, python 으로도 생성할 수 있다.

다만 이렇게 작성된 hook은 commit, push 등으로 타인과 공유할 수 없다. 로컬에서 이를 작성해도 원본 repo에 이를 추가할 수 없다는 뜻이다. 그 이유는 .git 이하의 파일은 공유되지 않기 때문이다. 해당 문제를 해결하기 위해 husky 를 사용할 수 있다.

깃 훅은 크게 클라이언트 훅과 서버 훅으로 나뉜다.


클라이언트 훅

클라이언트 훅은 종류가 정말 다양하다.

  • 커밋 워크플로우 훅(git commit 시)

    커밋 전후로 훅을 작동시킬 수 있다. 이를 통하면 커밋 시 테스트를 강제하고, 테스트 실패 시 커밋을 취소 시킬 수도 있다. 클라이언트 훅에서는, 당장 커밋 워크플로우 훅만 필요하기 때문에 이하의 이메일 워크플로우나 기타 훅은 따로 다루지 않겠다.

    • pre-commit ( commit 시 가장 먼저 실행)

      해당 훅은 커밋 이벤트 발생 시 가장 먼저 실행되는 훅이다. 해당 훅에서는 커밋하는 snapshot 을 점검한다. 해당 훅에서 0을 반환할 경우 commit 은 그대로 진행되고, 0이 아닌 다른 값을 반환했을 경우 commit 은 실행되지 않는다. 따라서 해당 훅에서 커밋 전 lint 룰을 검사하는 등의 작업이 이루어진다.

      예시

         #!/bin/sh
         #
         # An example hook script to verify what is about to be committed.
         # Called by "git commit" with no arguments.  The hook should
         # exit with non-zero status after issuing an appropriate message if
         # it wants to stop the commit.
         #
         # To enable this hook, rename this file to "pre-commit".
       
         if git rev-parse --verify HEAD >/dev/null 2>&1
          then
       	against=HEAD
          else
         # Initial commit: diff against an empty tree object
       	against=$(git hash-object -t tree /dev/null)
         fi
         
         # If you want to allow non-ASCII filenames set this variable to true.
         allownonascii=$(git config --type=bool hooks.allownonascii)
         
         # Redirect output to stderr.
         exec 1>&2
         
         # Cross platform projects tend to avoid non-ASCII filenames; prevent
         # them from being added to the repository. We exploit the fact that the
         # printable range starts at the space character and ends with tilde.
         if [ "$allownonascii" != "true" ] &&
         	# Note that the use of brackets around a tr range is ok here, (it's
         	# even required, for portability to Solaris 10's /usr/bin/tr), since
         	# the square bracket bytes happen to fall in the designated range.
         	test $(git diff --cached --name-only --diff-filter=A -z $against |
         	  LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0
         then
         	cat <<\EOF
         Error: Attempt to add a non-ASCII file name.
         
         This can cause problems if you want to work with people on other platforms.
         
         To be portable it is advisable to rename the file.
         
         If you know what you are doing you can disable this check using:
         
           git config hooks.allownonascii true
         EOF
         	exit 1
         fi
         
         # If there are whitespace errors, print the offending file names and fail.
         exec git diff-index --check --cached $against --
      

      위 파일은 기본적으로 추가되어 있는 pre-commit.sample 파일이다. 해당 파일에는 staging 된 파일 중 파일 명이 Ascii 문자로 이루어지지 않은 파일이 존재할 경우 커밋을 반려하는 로직이 포함되어 있다. 이를 테스트하기 위해 파일명을 pre-commit.samplepre-commit 로 수정하자.

      이 때 다음의 파일을 추가한다면? 다음과 같이 에러를 뱉으며 커밋을 실행하지 않는다.


    • prepare-commit-msg (git 이 커밋 메시지를 생성하고 편집기를 실행하기 직전 수행)

      해당 훅은 사람이 커밋 메시지를 수정하기 전에 실행되는 훅으로, 사람이 수정하기 전 먼저 프로그램으로 손보고 싶을 때 싶을 때 이용할 수 있다. 즉, 커밋 메시지에 템플릿을 적용하고 싶을 때 사용할 수 있고, 그게 아니라면 메시지가 자동으로 생성되는 이벤트에(ex. merge) 특정 메시지를 적용하고 싶을 때 사용할 수 있다.

    • commit-msg (commit 이 발생하기 직전에 실행)

      해당 훅은 커밋 메시지까지 작성이 된 후 커밋이 발생하기 직전에 실행된다. 따라서 커밋 메시지의 점검 등에 쓰일 수 있다. 해당 훅에서 0을 반환할 경우 commit 이 일어나고, 그렇지 않을 경우 commit 은 반려된다.

    • post-commit (commit 이 발생한 직후 실행)

      해당 훅의 발생 시점은 커밋 이벤트가 발생한 후이다. 즉, 해당 훅에서는 커밋의 반려 권한이 없다.

  • 이메일 워크플로우 훅(git am 시)

    • applypatch-msg
    • pre-applypatch
    • post-applypatch
  • 기타 훅

    • pre-rebase (git rebase 시)
    • post-rewrite (git commend —amend, git rebase 시)

서버 훅

서버 훅은 push 전, 후로 발생한다. 이를 이용하면 복잡한 push 룰도 구현이 가능하다.

  • pre-receive (push 시 가장 먼저 실행)

    해당 훅은 push 이벤트가 발생했을 때 가장 먼저 실행된다. push 이벤트의 모든 대상 branch 들의 목록을 ref로 가지며, 만약 해당 훅에서 0이 아닌 값을 반환한다면 모든 branch 들로의 push가 반려된다.

  • update (push 대상 브랜치에 push 직전 실행)

    해당 훅은 push 의 각 대상 branch 들에 대해 각각 수행된다는 점을 제외하면 pre-receive 와 거의 비슷하다. 즉 push 의 대상이 2개의 branch 면 해당 훅은 각각 2번 수행되고, push 제어도 2개의 branch 에 대해 각각 일어난다.

  • post-receive (push 완료 후 실행)

    해당 훅은 push 가 완료된 후 수행된다. 이를 통해 push 시 자동으로 이메일을 보내는 등의 작업을 할 수 있다.



Husky를 통한 git hook 적용

git hook 을 사용하는데 husky 를 이용하면 협업에 있어 굉장히 편하게 git hook을 강제할 수 있다. .git/hooks 경로 내부에서 관리되는 git hook 은 repo를 통해 다른 사용자와 공유할 수 없는데, husky를 이용하면 이를 간단히 해결할 수 있다.

다음 명령어를 실행하여 husky의 설치와 설정을 수행한다.

npx husky-init -y && npm install

위 명령어를 수행하면 생성되는 .husky/pre-commit 파일은 다음과 같다.

#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

npm test

이 때 npm testhusky 가 자동으로 추가한 명령어이다. 이 후 git hook 을 추가하기 위해 다음의 명령어를 수행한다.

npx husky add .husky/pre-commit "echo 'Hello, Husky!'"

그 후 .husky/pre-commit 파일에 들어가면 다음과 같이 수정되어 있다.

#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

npm test
echo 'Hello, Husky!'

이제 commit 을 발생시키면 위 로직이 실행된다. 다만, 초기 package.jsonnpm test scripts는 exit 1 이 수행되기 때문에, echo 문이 실행되지 않고 곧바로 커밋이 반려된다.

우리 프로젝트에 적용 (최종 목표가 `husky` 와 더불어 `lint-staged` 까지 적용시키는 것이라면 해당 토글 말고 글의 바닥에 있는 토글을 확인)

우리 프로젝트는 package.json 과 .git 폴더가 서로 다른 위치에 존재한다. 따라서 위의 방식으로는 husky 가 작동하지 않는다. 아래는 우리 프로젝트에 husky 를 적용하는 방법이다.

명령어는 root 디렉터리 (.git 이 존재)를 기준으로 한다.

cd server && npm install -D husky
# 디렉터리는 server or client 중 원하는 곳으로 선택.
# 여기에서는 빌드 시 문제가 발생할 수도 있을 것 같다는 개인의 판단에 따라 server에 설치
# 선택된 디렉터리에 husky 가 설치됨
cd .. && npx husky install server/.husky
# 해당 코드가 실행될 때 현재 디렉터리는 /server 이다.

그 후 /server/package.json 에 다음을 추가

{
  "scripts": {
    ...,
    "prepare": "cd .. && husky install server/.husky"
  }
}

그 후 git hook 추가

npx husky add .husky/pre-commit "cd server && npm test && cd .."
npx husky add .husky/pre-commit "cd client && npm test" 

이제 커밋 수행 시 먼저 /server 디렉터리와 /client 디렉터리에서 npm test 를 수행한다. 만약 이 때 0이 아닌 값이 반환될 경우, 커밋은 반려된다.

또한 위 설정은 최초 개발자만 수행하면 되며, 해당 코드가 포함된 브랜치를 클론할 시엔, 그저 npm i 만 해주어도 위 깃 훅이 작동한다.



Husky & Lint-staged 를 통한 lint 적용 자동화

lint-staged 는 현재 스테이징 된 코드에 대해 lint 를 적용해주는 툴이다. 이와 husky를 이용하면 커밋 시 자동으로 eslint —fix 가 실행되도록 할 수 있다.

다음 명령어를 입력하여 husky와 lint-staged를 설치해준다. 명령어를 수행하기 전에 eslint, prettier가 설치되어 있어야한다.

npx mrm@2 lint-staged

해당 명령어를 사용하면 자동으로 husky와 lint-staged의 초기 설정을 수행해준다.

이후 package.json 을 보면 다음과 같이 수정되어 있다.

{
	"scripts": {
    ...,
    "prepare": "husky install"
  },
  "lint-staged": {
    "*.js": 
      "eslint --cache --fix"
  }
}

이중 lint-staged 는 .js 파일에 대해 eslint —cache —fix 를 진행한다는 뜻이다.

또한 .husky/pre-commit 에 가보면 다음과 같이 파일이 작성되어 있다.

#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

npx lint-staged

즉, 이는 커밋이 될 때마다 npx lint-staged ↔ eslint —cache —fix 를 수행한다는 것이다. 이 때 lint 에서 에러가 발생하면 커밋은 반려된다.

우리 프로젝트에 husky + lint-staged 적용

우선 우리 프로젝트는 .git 과 package.json 의 경로가 다르기 때문에 npx mrm@2 lint-staged 를 통한 자동 설정이 불가능하다. 따라서 먼저 아래를 참고하여 husky 세팅을 해준다.

Web 08에 husky 적용

우리 프로젝트는 package.json 과 .git 폴더가 서로 다른 위치에 존재한다. 따라서 위의 방식으로는 husky 가 작동하지 않는다. 아래는 우리 프로젝트에 husky 를 적용하는 방법이다.

명령어는 root 디렉터리 (.git 이 존재)를 기준으로 한다.

cd server && npm install -D husky
# 디렉터리는 server or client 중 원하는 곳으로 선택.
# 여기에서는 빌드 시 문제가 발생할 수도 있을 것 같다는 개인의 판단에 따라 server에 설치
# 선택된 디렉터리에 husky 가 설치됨
cd .. && npx husky install server/.husky
# 해당 코드가 실행될 때 현재 디렉터리는 /server 이다.

그 후 /server/package.json 에 다음을 추가

{
  "scripts": {
    ...,
    "prepare": "cd .. && husky install server/.husky"
  }
}

그 후 git hook 추가

npx husky add .husky/pre-commit "cd server && npm test && cd .."
npx husky add .husky/pre-commit "cd client && npm test" 

이제 커밋 수행 시 먼저 /server 디렉터리와 /client 디렉터리에서 npm test 를 수행한다. 만약 이 때 0이 아닌 값이 반환될 경우, 커밋은 반려된다.

또한 위 설정은 최초 개발자만 수행하면 되며, 해당 코드가 포함된 브랜치를 클론할 시엔, 그저 npm i 만 해주어도 위 깃 훅이 작동한다.

프로젝트에 husky 세팅이 완료되면, 이제 lint-staged 를 설치해야한다. 그전에 먼저 /server , /client 각 디렉터리에 eslintprettier 설정을 완료해야 한다. 설정이 완료 되었다면, 이제 /server, /client 경로에 모두 다음의 과정을 수행한다.

  • lint-staged 설치

    npm i -D lint-staged
  • package.jsonlint-staged 명령어 설정

    {
      ...,
    	"lint-staged": {
        "*.{ts,tsx}": [
          "eslint --cache --fix",
          "git add"
        ]
      }
    }
  • ./husky/pre-commit 내 다음과 같이 작성

    #!/usr/bin/env sh
    . "$(dirname -- "$0")/_/husky.sh"
    
    cd client && echo 'Hello, client' && npx lint-staged && cd ..
    cd server && echo 'Hello, server' && npx lint-staged && cd ..

여기까지 설정이 완료되면, 이제부터 commit 명령어 실행 시 eslint 가 작동하며, fix 할 수 있는 오류들은 알아서 fix 후 커밋에 포함시킨다. 알아서 fix 할 수 없는 오류들이 있을 때는 커밋을 반려하게 된다.

Clone this wiki locally