commitlint 사용해보기

이희제·2024년 8월 21일
post-thumbnail

얼마 전에 commitlint에 대해서 알게 되었다. 앞으로 업무를 진행할 때 팀 내 도입을 제안하려고 하는데 먼저 어떻게 사용할 수 있는지 알아보고자 한다.

현재 팀 내에서 intellij 플러그인 Git Commit Template를 사용해서 타입에 맞게 커밋 메세지를 작성하고 있다. (지라 연동을 위해 지라의 이슈 번호도 넣어주고 있다)

commitlint를 사용하면 지라 이슈 번호 체크 및 작업의 타입 명시가 잘 되어 있는지 명확하게 확인할 수 있을 것이다.

1. 설치

현재 pnpm을 사용 중이기 때문에 pnpm 기반으로 설치한다.

pnpm add --save-dev @commitlint/{cli,config-conventional}

설치 후에 설정 파일을 생성해주자.

module.exports = {
  extends: ['@commitlint/config-conventional']
};

2. 적용

나의 적용 목적은 로컬에서 커밋 시에 내가 정한 룰 기반으로 커밋 메세지가 작성되어 있는지 체크하는 것이다.

현재 lint-stagedhusky를 통해서 코드의 스타일(prettier 사용)과 eslint rule을 커밋 시에 검사하고 있다.

husky 사용

마찬가지로 husky를 통해서 커밋 메세지를 체크하면 될 것이다.

commitlint를 통해 커밋 메세지를 검사하는 명령어를 husky hook에 추가해주자.

echo "pnpm dlx commitlint --edit \$1" > .husky/commit-msg

→ $1은 커밋 메시지가 저장된 임시 파일의 경로를 나타낸다.

rule 정의하기

팀 내 사용되고 있는 커밋 메세지 컨벤션은 다음과 같다.

"프로젝트코드-이슈번호 작업 타입: 작업 내용"
ex) "WEBTEAM-283 fix: 버그 수정"

작업 타입은 다음과 같이 사용하고 있다. commitlint 공식 문서에서 아래 타입을 기본값으로 가지고 있다.

  • build
  • chore
  • ci
  • docs
  • feat
  • fix
  • perf
  • refactor
  • revert
  • style
  • test

커스텀하게 팀에서 사용할 커밋 메세지 룰을 만들어야 하기 때문에 local plugin을 활용해서 커스텀 룰을 정의했다.

설정 파일은 다음과 같다.

[커스텀 룰]

  • JIRA 티켓 타입 확인(ticket-pattern)
  • JIRA 티켓의 대문자, 비어 있음 확인(ticket-case, ticket-empty)
const config = {
  extends: ['@commitlint/config-conventional'],
  parserPreset: {
    parserOpts: {
      headerPattern: /^(\w+-\d+)\s(\w+):\s(.+)$/,
      headerCorrespondence: ['ticket', 'type', 'subject'],
    },
  },
  rules: {
    'header-match-team-pattern': [2, 'always'],
    'header-max-length': [2, 'always', 100],
    'ticket-empty': [2, 'never'],
    'ticket-case': [2, 'always', 'upper-case'],
    'type-empty': [2, 'never'],
    'type-case': [2, 'always', 'lower-case'],
    'subject-empty': [2, 'never'],
    'subject-full-stop': [2, 'never', '.'],
    'scope-empty': [2, 'always'],
  },
  plugins: [
    {
      rules: {
        'header-match-team-pattern': (parsed) => {
          const { ticket, type, subject } = parsed;
          if (ticket && type && subject) {
            return [true];
          }
          return [
            false,
            '커밋 메시지가 올바른 형식이 아닙니다. 예: PROJ-123 feat: 새 기능 추가',
          ];
        },
        'ticket-empty': (parsed) => {
          const { ticket } = parsed;
          if (ticket && ticket.trim().length > 0) {
            return [true];
          }
          return [false, '티켓이 비어있습니다'];
        },
        'ticket-case': (parsed) => {
          const { ticket } = parsed;
          if (!ticket) return [true]; // 값이 없는 경우 바로 위의 룰에서 걸리기 때문에 해당 룰은 패스하도록 처리했다. 
          if (ticket && ticket === ticket.toUpperCase()) {
            return [true];
          }
          return [false, '티켓 형식은 대문자여야 합니다'];
        },
        'ticket-pattern': (parsed) => {
          const { ticket } = parsed;
          const ticketPattern = /^[A-Z]+-\d+$/;
          if (ticket && ticketPattern.test(ticket)) {
            return [true];
          }
          return [
            false,
            '티켓 번호는 "프로젝트 코드-숫자" 형식이어야 합니다. 예: PROJ-123',
          ];
        },
      },
    },
  ],
};

export default config;

commitlint 규칙에서 사용되는 숫자는 다음과 같은 의미를 가진다

  • 0: 규칙 비활성화 (disable)
  • 1: 경고 (warning)
  • 2: 오류 (error)

always랑 never의 뜻은 다음과 같다.

  • always: 항상 이 조건을 만족해야 한다
  • never: 이 조건을 절대 만족하면 안 된다

3. 확인

이제 룰이 정상적으로 동작하는지 확인해보자.

  • 입력: "test-1 any: test" → 티켓이 소문자로 구성되어 있고 any는 작업 타입에 포함되지 않기 때문에 린트에서 오류가 발생한다.
  • 입력: "fix: 버그 수정" → 티켓 정보가 없다

  • 입력: "TEST-123 refactor: 컴포넌트 구조 변경" → 포맷에 맞는 정상적인 메세지다.

profile
그냥 하자

0개의 댓글