개발자 홍창기

Software Developer

Gitmoji와 commitlint, husky로 커밋 메시지 규칙 검증하기

작성 · 수정

이모지와 이슈 번호를 포함한 팀의 커밋 메시지 규칙을 검증하고, 코드 검사와 메시지 검사의 Git 훅을 구분해 구성한 기록입니다.

팀에서 합의한 커밋 메시지를 검증하기

2024년 해커톤 프로젝트를 준비하면서 함께 개발하는 동료와 커밋 메시지 규칙을 정했다. 변경 유형을 빠르게 구분하고 관련 이슈를 찾을 수 있도록, 이모지와 유형, 이슈 번호를 함께 적는 방식이었다.

🚨 Fix(#41): commitlint 설정 수정

Gitmoji는 커밋의 의도를 나타내는 이모지와 의미를 정리한 가이드다. 이모지를 추가하는 것만으로 메시지가 명확해지는 것은 아니므로, 팀에서 각 이모지의 의미와 사용 범위를 합의하는 일이 먼저였다.

이 글은 당시의 적용 경험을 바탕으로, 퇴고 시점에 다시 검증한 설정 예제를 정리한다. 아래 매핑은 당시 팀의 규칙을 단순화한 예제이며 Gitmoji의 전체 의미 체계를 그대로 적용한 것은 아니다.

유형팀에서 사용하는 이모지
Feat✨
Fix🚨
Refactor🔨
Chore📝

역할을 구분하기

  • commitlint는 커밋 메시지가 규칙에 맞는지 검사한다.
  • husky는 프로젝트에서 Git 훅을 관리한다.
  • lint-staged는 스테이징된 파일에 코드 검사나 포맷팅 명령을 실행한다. 커밋 메시지 검사에는 필요하지 않다.

예제는 Node.js 22.18.0, husky 9.1.7, @commitlint/cli와 @commitlint/config-conventional 19.8.1에서 확인했다. 선택 사항인 파일 포맷팅은 lint-staged 16.4.0과 Prettier 3.9.9에서 확인했다. 다른 버전에서는 해당 버전의 공식 문서를 함께 확인해야 한다.

설치와 훅 등록

Git 저장소 루트에서 개발 의존성을 설치한다.

pnpm add -D husky@9 @commitlint/cli@19 @commitlint/config-conventional@19

package.json의 기존 scripts에 prepare를 추가한다. 이미 prepare 작업이 있다면 기존 작업을 보존하면서 연결한다.

{
  "scripts": {
    "prepare": "husky"
  }
}

한 번 실행해 훅 경로를 등록한다. 이후 의존성을 설치하는 협업자의 환경에서도 prepare가 실행된다.

pnpm run prepare

.husky/commit-msg 파일을 만들고 다음 내용을 작성한다.

pnpm exec commitlint --edit "$1"

$1은 Git이 전달한 커밋 메시지 파일 경로다. Husky가 생성하는 .husky/_의 내부 파일을 옮기거나 직접 수정할 필요는 없다. Husky의 수동 설정 안내와 commitlint의 로컬 설정 안내를 참고했다.

팀의 형식을 파싱하고 검증하기

🚨 Fix(#41): 내용에는 일반적인 Conventional Commits 형식에 없는 이모지와 # 접두사가 들어간다. 단순히 허용 유형만 나열하기보다, 메시지를 어떻게 나눌지 먼저 정의해야 한다.

루트에 commitlint.config.cjs를 작성한다. CJS 확장자를 사용해 프로젝트의 ESM 설정과 관계없이 CommonJS 구성을 읽을 수 있도록 했다.

const emojiByType = {
  Feat: '✨',
  Fix: '🚨',
  Refactor: '🔨',
  Chore: '📝',
}

const headerPattern = /^(\S+) ([A-Za-z]+)\(#([1-9]\d*)\): (.+)$/

module.exports = {
  extends: ['@commitlint/config-conventional'],
  parserPreset: {
    parserOpts: {
      headerPattern,
      headerCorrespondence: ['emoji', 'type', 'scope', 'subject'],
    },
  },
  plugins: [
    {
      rules: {
        'team-header': ({ header }) => {
          const match = headerPattern.exec(header ?? '')
          const valid = Boolean(
            match &&
            match[1] === emojiByType[match[2]] &&
            match[4].trim().length > 0,
          )
          return [
            valid,
            '이모지 Type(#이슈번호): 내용 형식과 이모지 매핑을 확인하세요.',
          ]
        },
      },
    },
  ],
  rules: {
    'team-header': [2, 'always'],
    'type-enum': [2, 'always', Object.keys(emojiByType)],
    'type-case': [2, 'always', 'pascal-case'],
    'scope-empty': [2, 'never'],
    'subject-empty': [2, 'never'],
    'subject-case': [0],
  },
}

커스텀 파서는 이모지, 유형, 이슈 번호, 내용을 분리한다. 로컬 규칙은 이모지와 유형의 매핑, 양의 정수 이슈 번호, 비어 있지 않은 내용을 함께 검증한다. 유형의 대소문자는 type-case로, 허용 목록은 type-enum으로 검사한다.

설정 방식은 commitlint의 파서 설정과 로컬 플러그인 문서를 참고했다. 공유 설정의 나머지 기본 규칙과 기본적으로 무시되는 병합 메시지 등의 동작도 유지된다.

실제 커밋을 만들기 전에 표준 입력으로 확인할 수 있다.

# 통과: 합의한 매핑과 형식을 사용한다.
echo '🚨 Fix(#41): commitlint 설정 수정' | pnpm exec commitlint

# 실패: Fix에 매핑되지 않은 이모지다.
echo '✨ Fix(#41): commitlint 설정 수정' | pnpm exec commitlint

# 실패: 이슈 번호가 없다.
echo '🚨 Fix: commitlint 설정 수정' | pnpm exec commitlint

메시지 검사는 commit-msg에서 실행하기

초기에는 메시지 검사 명령을 pre-commit에 넣었다. 커밋 실패 후 이전 메시지가 다시 검사되는 현상을 보고 .git/COMMIT_EDITMSG 파일을 직접 비우는 방법도 시도했다.

이 접근은 훅의 실행 시점과 인자를 제대로 구분하지 못한 것이었다. Git 공식 문서에 따르면 pre-commit은 제안된 커밋 메시지를 얻기 전에 실행되고 인자도 받지 않는다. 반면 commit-msg는 검사할 메시지 파일의 경로를 인자로 받는다.

따라서 메시지 검사는 commit-msg에서 --edit "$1"로 실행해야 한다. Git이 관리하는 메시지 파일을 수동으로 삭제하거나 비울 이유가 없다.

당시에는 lint-staged를 추가한 뒤 문제가 사라졌다는 관찰을 근거로, lint-staged의 임시 파일 정리가 원인이라고 설명했다. 그러나 같은 과정에서 검사 명령을 commit-msg로 옮겼으므로 그 관찰만으로 lint-staged의 효과를 분리할 수 없었다. 이 예제에서는 lint-staged 없이도 잘못된 메시지가 거부되고, 이어서 올바른 메시지로 커밋하는 것을 확인했다.

선택 사항: 스테이징된 파일 포맷팅

코드 포맷팅도 함께 적용하려면 lint-staged를 별도로 사용한다.

pnpm add -D lint-staged@16 prettier@3

package.json에 실제 실행할 작업을 추가한다.

{
  "lint-staged": {
    "*.{js,jsx,ts,tsx,json,md,mdx}": "prettier --write"
  }
}

.husky/pre-commit에는 다음 명령을 작성한다.

pnpm exec lint-staged

이렇게 구성하면 pre-commit은 스테이징된 파일의 포맷팅을, commit-msg는 메시지 검사를 담당한다. 빈 작업 목록을 메시지 파일 정리를 위한 우회 수단으로 사용하지 않는다. lint-staged의 동작은 공식 README를 참고했다.

적용하면서 배운 점

커밋 메시지의 일관성을 지키려면 팀의 규칙을 명시하고, 그 규칙을 파싱할 방법과 검증할 시점을 맞춰야 한다. 도구를 추가한 뒤 증상이 사라졌다는 사실만으로 해결 원인을 단정해서도 안 된다.

이번 퇴고에서는 유효한 메시지와 잘못된 매핑·대소문자·이슈 번호·빈 내용을 포함한 메시지를 검사했다. 실제 Git 훅에서도 실패한 커밋 다음에 올바른 메시지로 커밋할 수 있는지 확인해, 설정 예제와 원인 설명을 함께 검증했다.