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]
그리고 테스트해보면 아래와 같이 커밋 메시지에 대한 린트가 잘 작동하는 것을 확인할 수 있다.
마주친 문제
실패한 커밋 이후 .git/COMMIT_EDITMSG가 남아 commitlint 입력이 갱신되지 않는 문제가 있었다.
커밋 메시지를 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에게 전달될 수 있던 것이다.
결과
결과적으로, 아래와 같이 협의한 커밋 메시지 컨벤션을 강제하는 린트 시스템이 잘 만들어졌다.