발행일
- 읽는 데 13 분
Gitmoji와 commitlint, husky로 가독성 높은 커밋 메시지의 일관성 지키기

Gitmoji로 심심한 커밋 메시지에 귀여움 한 방울 + 가독성 한 방울을 제공해보자.
보통 커밋 메시지를 작성한다고 하면 아래와 같이 순수 텍스트로만 작성하게 된다.
feat: 머시기머시기 추가
커밋의 유형을 구분하는 접두사와 커밋의 내용. 익숙해진다면 작성하기도 쉬워지고 알아보기도 나쁘지 않다.
여기에 표준 유니코드 문자 중 하나인 ‘이모지’를 곁들여보자.
✨ feat: 머시기머시기 추가
단순히 그림 문자 하나가 추가됐을 뿐인데 가독성 측면에서 상당히 개선된 것 처럼 보인다. 이모지와 접두사간 매핑 관계가 있다면, 이모지만 보고도 커밋의 유형을 파악할 수 있게 되기 때문이다.
어려울 것 없이 이모지를 깃 커밋 메시지 작성 시 활용하기 쉽게 만들어주는 도구가 Gitmoji이다. 기본적으로 CLI 기반의 도구지만, VSCode Extension으로 설치하면 GUI에서도 편하게 활용할 수 있다.
이모지는 그림 문자지만, 각자 매핑된 이름이 있다.
✨는 :sparkles:
, ♻️는 :recycle:
과 같은 식이다.
그래서 커밋 메시지 컨벤션 룰을 정할 수 있는 commitlint와 commitlint-config-gitmoji
라는 라이브러리를 사용하면 커밋 메시지 컨벤션에 이모지를 포함시킬 수 있게 되고, husky를 활용하면 pre-commit 단계에서 컨벤션 린팅을 돌려줄 수 있다.
만들어봅시다!
최근에 해커톤을 위한 프로젝트 개발이 시작됐는데, 같이 협업하게 된 개발자분과 논의해서 아래와 같은 커밋 메시지 컨벤션을 적용하기로 했다.
이모지 접두사(#이슈번호): 커밋 내용
//ex) 🚨 Fix(#41): commitlint 이슈 해결
접두사 다음에 이슈번호를 추가한 이유는, 해당 이슈 및 이슈와 연결된 PR에 자동으로 reference되기 때문이다. Task 추적에 굉장히 유리하다.
husky
pnpm 기준으로 아래와 같이 설치한다.
pnpm add -D husky // 설치
pnpm dlx husky // 초기화
그리고 협업하는 개발자 분도 패키지를 설치할 때 자동으로 위 명령어를 실행하도록 아래처럼 설정해주자.
// package.json
"scripts": {
// ...
"prepare": "husky"
// ...
}
husky 초기화 후에 .husky/_/
내부를 살펴보면 여러가지 Git Hooks 파일을 확인할 수 있는데, pre-commit 훅을 이용할 것이므로 이걸 상위 경로로 꺼내오자.
그리고 원래 있던 내용을 지우고 commitlint를 실행하는 명령어를 적어준다.
npx --no-install commitlint --edit "$1"
commitlint
역시 마찬가지로 아래와 같이 설치해준다.
pnpm add -D @commitlint/{cli,config-conventional}
// Windows 사용자는 아래 명령어로 설치
pnpm add -D @commitlint/config-conventional @commitlint/cli
commitlint는 루트 디렉토리에 위치하는 commitlint.config.js
파일을 활용해서 컨벤션 룰 등을 관리한다.
아래 명령어로 초기화해주자. 간단한 유닉스 명령어이다.
echo "export default { extends: ['@commitlint/config-conventional'] };" > commitlint.config.js
commitlint-config-gitmoji
이모지를 컨벤션에 포함시킬 수 있도록하는 라이브러리이다. npm에서 사용방법을 자세히 확인할 수 있다.
pnpm add -D commitlint-config-gitmoji
설치가 끝나면 생성된 commitlint.config.js
를 열고 extends 배열에 ‘gitmoji’를 추가하면 된다.
이 라이브러리의 docs를 보면 컨벤션을 다음과 같은 구조로 보는데, commitlint에 기반하고 있다.
:gitmoji: type(scope?): subject
// ex)
:sparkles: feat(changelog): support chinese title
:bug: fix(config): fix a subject bug
:memo: docs: update README.md
:bulb: docs(plugin): update comments
우리가 설정하려는 컨벤션과 유사한 구조이지만, scope 자리에 이슈 넘버가 들어간다는 차이가 있다.
이 부분을 포함해서 이제 commitlint의 컨벤션 룰을 하나씩 설정해보자.
컨벤션 룰 설정
commitlint의 컨벤션 룰의 기본 구조는 룰 이름과 내용인데, 내용은 Level, Appllicable, Value로 구성된다.
예를 들면, type에 작성할 수 있는 접두사의 종류를 enum으로 한정하는 룰은 다음과 같이 작성한다.
"type-enum": [
2,
"always",
[
'build',
'chore',
'ci',
'docs',
'feat',
'fix',
'perf',
'refactor',
'revert',
'style',
'test',
],
],
// ...
Level에는 0, 1, 2가 올 수 있고 각각 룰 비활성화, 경고, 에러를 발생시킨다. Applicable은 always나 never가 올 수 있는데, never는 룰의 통과 조건을 always와 다르게 설정한다. 자세한건 룰마다 다르기 때문에 docs를 참고해야한다. 마지막으로 Value는 룰을 위한 값이고, 룰마다 요구하는 값의 형태가 다르다.
gitmoji의 이름을 이 type-enum 룰의 value로 넘겨주면 접두사에 관한 컨벤션 룰이 완성된다.
"type-enum": [
2,
"always",
[
":sparkles: Feat",
":rotating_light: Fix",
":hammer: Refactor",
":tada: Init",
":memo: Chore",
":libstick: Style",
":wastebasket: Remove",
":recycle: Format"
],
],
구조상 scope에 해당하는 이슈 넘버는 어떻게 검증할 수 있을까?
여기서 유용한 플러그인을 하나 소개하겠다.
pnpm add -D commitlint-plugin-function-rules
commitlint-plugin-function-rules
는 Value로 값을 넘겨줄 때 커밋 메시지를 매개변수로 받아올 수 있는데, 간단한 정규식으로 메시지의 형식이 컨벤션과 맞는지 검사해줄 수 있다.
이 플러그인을 쓰려면 아래와 같이 plugins 배열에 이름을 추가해주고
export default {
// ...
plugins: ['commitlint-plugin-function-rules'],
// ...
}
룰을 정의할 때 function-rules에서 제공하는 룰을 사용하면 된다. 이슈 넘버 형식(#n)을 검사하는 정규식을 적용한 결과는 아래와 같다.
// ...
'function-rules/scope-enum': [
2,
'always',
(parsed) => {
const { header } = parsed;
const issueNumberRegex = /\(#(\d+)\)/;
if (issueNumberRegex.test(header)) {
return [true];
}
return [
false,
`커밋 메시지의 이슈 넘버 형식이 올바르지 않습니다!. got ${header}`
];
}
]
// ...
그런데 현재 이 function-rules 라이브러리에서 제공하는 룰은 commitlint 19버전 이상에서 인식이 안되는 버그가 있다.(2024-03-15 기준) 그래서 이 라이브러리를 사용해야한다면 메이저 버전 18 이하에서 사용해야한다.
또 다른 문제는 type-enum 룰이 gitmoji 코드 뒤에 오는 접두사의 Pascal Case를 인식하지 못해서 type must be lower-case [type-case]
라는 에러를 발생시킨다는 것이다. 아래 룰을 추가해서 해결할 수 있다.
'type-case': [2, 'always', 'pascal-case'],
그리고 추가적으로 아래와 같은 룰을 비활성화해주자. (에러 방지)
// ...
'type-empty': [0],
'scope-enum': [0],
'scope-empty': [0],
'subject-empty': [0]
// ...
그리고 테스트해보면 아래와 같이 커밋 메시지에 대한 린트가 잘 작동하는 것을 확인할 수 있다.
마주친 문제
린트 테스트를 하고 성공 케이스를 테스트해보려고 제대로 작성했는데, 커밋 메시지 input이 바뀌지 않는 문제가 있었다.
커밋 메시지를 asdf로 입력하고 커밋 실패를 해서 다시 제대로 입력하면, commitlint가 제대로 입력한 커밋 메시지가 아니라 asdf를 계속해서 검사하는 문제인 것이다.
좀 찾아보니까 원인은 .git/COMMIT_EDITMSG
파일에 실패한 이전 커밋 내역이 남아있고, commitlint는 이것을 읽어서 input으로 넘기기 때문에 발생하는 문제란 것을 알았다.
그래서 해본 삽질.. pre-commit 파일에 아래처럼 .git/COMMIT_EDITMSG
의 내용을 지우는 명령어도 넣어보고…
#!/usr/bin/env sh
npx --no-install commitlint --edit "$1"
echo "" > "$(dirname -- "$(dirname -- "$0")")/.git/COMMIT_EDITMSG"
commitlint 명령이 실패하니까 아래 명령이 실행이 안돼서 조건문으로도 만들어보고…
#!/usr/bin/env sh
npx --no-install commitlint --edit "$1"
if [ $? -ne 0 ]; then
echo "" > "$(dirname -- "$(dirname -- "$0")")/.git/COMMIT_EDITMSG"
fi
이것도 안돼서 명령어 순서를 바꿔보니까 commitlint가 제대로 작동하지 않았다.
그래서 결국 도달한 해결법은 lint-staged와 commit-msg 훅이었다.
lint-staged
lint-staged는 git staged 상태의 코드에만 린트를 적용할 수 있도록 도와주는 라이브러리이다. eslint --fix
나 prettier 포맷팅을 husky와 조합해서 사용할 수 있는데, 그냥 사용하면 커밋을 하나 할 때마다 모든 소스코드에 대해 린트와 포맷팅을 진행하기 때문에 매우 비효율적이다. lint-staged는 그런 상황을 해결하기 위해 등장한 도구인 것이다.
그런데 나는 이 라이브러리를 본래의 목적과는 조금 다르게 응용해서 문제를 해결했다.
일단 lint-staged는 아래 명령으로 설치할 수 있다.
pnpm add -D lint-staged
그리고 package.json
파일에 lint-staged가 처리할 작업을 설정할 수 있는데, 나는 이렇게 아무것도 할당하지 않았다. (물론 해도 된다.)
// ...
"lint-staged": {
"*.{js,jsx}": []
}
// ...
그리고 .husky 내부의 pre-commit 파일과 commit-msg 파일에 각각 아래 명령을 작성해준다.
// pre-commit
npx lint-staged
// commit-msg
npx --no-install commitlint --edit "$1"
이후 터미널에 npx husky
명령으로 husky를 다시 초기화해준다.
그리고 커밋을 해보면 아래와 같이 커밋을 남길 때마다 이전 커밋내역이 갱신되는 것을 확인할 수 있다.
이게 어떻게 문제를 해결하는걸까?
로그에 따르면 lint-staged는 작업을 실행하기 전에 아래와 같이 Git과 관련된 임시 파일들을 정리하는 작업을 실행한다.
이 때 문제가 되었던 .git/COMMIT_EDITMSG
파일도 정리하기 때문에 변경된 커밋 메시지가 정상적으로 commitlint에게 전달될 수 있던 것이다.
결과
결과적으로, 아래와 같이 협의한 커밋 메시지 컨벤션을 강제하는 린트 시스템이 잘 만들어졌다.