개발자 홍창기

Web Developer

Gitmoji로 심심한 커밋 메시지에 귀여움 한 방울 + 가독성 한 방울을 제공해보자.

보통 커밋 메시지를 작성한다고 하면 아래와 같이 순수 텍스트로만 작성하게 된다.

feat: 머시기머시기 추가

커밋의 유형을 구분하는 접두사와 커밋의 내용. 익숙해진다면 작성하기도 쉬워지고 알아보기도 나쁘지 않다.

여기에 표준 유니코드 문자 중 하나인 '이모지'를 곁들여보자.

 feat: 머시기머시기 추가

단순히 그림 문자 하나가 추가됐을 뿐인데 가독성 측면에서 상당히 개선된 것 처럼 보인다. 이모지와 접두사간 매핑 관계가 있다면, 이모지만 보고도 커밋의 유형을 파악할 수 있게 되기 때문이다.

어려울 것 없이 이모지를 깃 커밋 메시지 작성 시 활용하기 쉽게 만들어주는 도구가 Gitmoji이다. 기본적으로 CLI 기반의 도구지만, VSCode Extension으로 설치하면 GUI에서도 편하게 활용할 수 있다.

gitmoji_1

gitmoji_usage

이모지는 그림 문자지만, 각자 매핑된 이름이 있다.

✨는 :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]

그리고 테스트해보면 아래와 같이 커밋 메시지에 대한 린트가 잘 작동하는 것을 확인할 수 있다.

test_1

마주친 문제

실패한 커밋 이후 .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를 다시 초기화해준다.

그리고 커밋을 해보면 아래와 같이 커밋을 남길 때마다 이전 커밋내역이 갱신되는 것을 확인할 수 있다.

commitlint_fixed

이게 어떻게 문제를 해결하는걸까?

로그에 따르면 lint-staged는 작업을 실행하기 전에 아래와 같이 Git과 관련된 임시 파일들을 정리하는 작업을 실행한다.

lint-staged_log

이 때 문제가 되었던 .git/COMMIT_EDITMSG 파일도 정리하기 때문에 변경된 커밋 메시지가 정상적으로 commitlint에게 전달될 수 있던 것이다.

결과

gitmoji-commitlint-result

결과적으로, 아래와 같이 협의한 커밋 메시지 컨벤션을 강제하는 린트 시스템이 잘 만들어졌다.