본문 바로가기
프로젝트/CLI 도구

CommitGen #1 - 왜 커밋 메시지 생성기를 만들었나

by 루까(Luka) 2026. 2. 12.
반응형

CommitGen — 왜 만들었나 + 설계

문제 인식

커밋 메시지를 잘 쓰고 싶다. 매번 git commit -m "fix stuff"를 쓰고 있진 않지만, 그렇다고 매번 정성스럽게 작성하지도 않는다. 특히 변경 사항이 많을 때 diff를 하나하나 읽으면서 메시지를 정리하는 건 꽤 번거로운 작업이다.

Conventional Commits 형식(feat:, fix:, refactor: 등)을 따르고 싶은데, 매번 type을 고르고, scope를 정하고, 한 줄 요약을 만드는 과정이 반복된다. 이 반복 작업을 AI에게 맡기면 어떨까?

그리고 마침 직접 만든 AI API 라이브러리 AIKit이 있다. 라이브러리를 만들어놓고 남의 프로젝트에만 추천할 게 아니라, 직접 실전에서 써보는 프로젝트가 필요했다.


핵심 아이디어

git diff --staged → AI 분석 → Conventional Commits 메시지 생성 → 인터랙티브 확인 → 자동 커밋

이게 전부다. 단순하지만, 이걸 CLI 도구로 잘 포장하는 게 핵심이다.

$ git add .
$ commitgen

📋 Staged Changes:
  added      src/utils/cache.js
  modified   src/index.js

🔍 Analyzing with openai...
✅ Commit message generated!

📝 Generated Commit Message:

  ✨ feat(cache): add TTL-based cache expiration logic

  - Implement isExpired() method for cache entries
  - Set default TTL to 1 hour
  - Add automatic cleanup of expired items on get()

? What would you like to do?
  ✅ Commit with this message
  ✏️  Edit message before committing
  🔄 Regenerate message
  ❌ Cancel

설계 목표

시작 전에 정한 기준이 있다.

목표 기준
실행 속도 commitgen 입력 후 3초 내 메시지 표시
프로바이더 자유 OpenAI, Claude, Gemini 중 선택 가능
설치 편의성 npx @lukaplayground/commitgen 한 줄로 실행
커밋 품질 Conventional Commits 형식 준수
언어 선택 한국어/영어 전환

아키텍처

┌─────────────────────────────────────────────────┐
│  bin/commitgen.js (CLI Entry)                   │
│  └─ Commander.js: 명령어 라우팅                   │
├─────────────────────────────────────────────────┤
│  src/index.js (Main Flow)                       │
│  └─ 9단계 파이프라인 오케스트레이션                  │
├──────────┬──────────┬──────────┬────────────────┤
│ gitDiff  │ prompt   │ ai       │ config         │
│ ──────── │ ──────── │ ──────── │ ────────────── │
│ diff파싱  │ 프롬프트  │ AIKit    │ ~/.commitgen/  │
│ 파일목록  │ 빌드     │ 연동     │ config.json    │
│ 커밋실행  │          │          │ + env vars     │
└──────────┴──────────┴──────────┴────────────────┘

4개의 유틸리티 모듈이 각자 역할을 담당하고, src/index.js가 이들을 조합해서 전체 플로우를 실행한다.


기술 선택

CLI 프레임워크: Commander.js

CLI 프레임워크 후보가 3개 있었다.

항목 Commander.js Yargs Oclif
설치 크기 45KB 160KB 2MB+
서브커맨드 지원 지원 지원
학습 곡선 낮음 중간 높음
주간 다운로드 2억+ 8천만 60만

Commander.js를 선택한 이유는 간단하다. 가장 가볍고, 가장 많이 쓰이고, 서브커맨드 지원이 충분하다. Oclif는 대규모 CLI에 적합하고, 이 프로젝트는 명령어 2개(generate, config)면 끝이다.

// bin/commitgen.js
const program = new Command();

program
  .command('generate', { isDefault: true })
  .option('-l, --lang <language>', 'commit message language (en/ko)', 'en')
  .option('-p, --provider <provider>', 'AI provider (openai/claude/gemini)')
  .option('-m, --model <model>', 'specific model to use')
  .option('--dry-run', 'show generated message without committing')
  .option('--no-emoji', 'disable emoji in commit type')
  .action(async (options) => {
    await run(options);
  });

{ isDefault: true }로 설정해서 commitgen만 입력해도 generate가 실행된다. 사용자가 매번 commitgen generate를 입력할 필요 없다.

AI 연동: AIKit

외부 AI SDK를 직접 쓰지 않고 AIKit을 쓴 이유:

// AIKit 없이 — 프로바이더마다 별도 코드 필요
if (provider === 'openai') {
  const res = await fetch('https://api.openai.com/v1/chat/completions', {
    headers: { 'Authorization': `Bearer ${key}` },
    body: JSON.stringify({ model: 'gpt-4o-mini', messages: [...] })
  });
  return res.choices[0].message.content;
} else if (provider === 'claude') {
  // ... 완전히 다른 코드
} else if (provider === 'gemini') {
  // ... 또 다른 코드
}

// AIKit 사용 — 한 줄이면 끝
const ai = new AIKit({ provider, apiKey });
const response = await ai.chat(prompt);
return response.text;

프로바이더 분기 코드가 사라진다. 사용자가 commitgen -p claudecommitgen -p gemini든, 내부 코드는 동일하다.

인터랙티브 프롬프트: Inquirer.js

커밋 메시지를 생성한 후 바로 커밋하지 않는다. 사용자에게 4가지 선택지를 준다.

// src/index.js
const { action } = await inquirer.prompt([{
  type: 'list',
  name: 'action',
  message: 'What would you like to do?',
  choices: [
    { name: '✅ Commit with this message', value: 'commit' },
    { name: '✏️  Edit message before committing', value: 'edit' },
    { name: '🔄 Regenerate message', value: 'regenerate' },
    { name: '❌ Cancel', value: 'cancel' },
  ],
}]);

"편집" 선택 시 시스템 에디터($EDITOR)가 열린다. "재생성"은 run() 함수를 재귀 호출해서 새로운 메시지를 받는다.


git diff 파싱 설계

git diff를 AI에 넘기기 전에 처리할 게 있다.

문제: diff가 너무 크면?

대규모 리팩토링이나 의존성 업데이트에서 diff가 수만 줄이 될 수 있다. AI API에는 토큰 제한이 있으므로, diff를 적절히 잘라야 한다.

// src/utils/gitDiff.js
export function truncateDiff(diff, maxLength = 8000) {
  if (diff.length <= maxLength) return diff;

  const truncated = diff.substring(0, maxLength);
  const lastNewline = truncated.lastIndexOf('\n');
  return truncated.substring(0, lastNewline) +
    '\n\n... (diff truncated for AI analysis)';
}

8000자로 제한한다. 줄 단위로 자르기 때문에 코드가 중간에 끊기지 않는다. AI에게 "잘렸다"는 사실도 알려줘서 불완전한 분석에 대한 환각을 줄인다.

파일 상태 파싱

git diff --staged --name-status 출력을 파싱해서 구조화한다.

// git 출력: "M\tsrc/index.js"
// 파싱 결과: { status: 'modified', path: 'src/index.js' }

export function getStagedFiles() {
  const files = execSync('git diff --staged --name-status', {
    encoding: 'utf-8',
  });
  return files.trim().split('\n').filter(Boolean).map((line) => {
    const [status, ...pathParts] = line.split('\t');
    return {
      status: parseStatus(status),
      path: pathParts.join('\t'),
    };
  });
}

pathParts.join('\t')를 쓴 이유는 rename 시 R100\told.js\tnew.js처럼 탭이 2개 들어오기 때문이다.


설정 시스템 설계

API 키를 매번 입력하는 건 불편하다. 3단계 우선순위 시스템을 설계했다.

CLI 옵션 (최우선) → 설정 파일 (~/.commitgen/config.json) → 환경 변수 (폴백)
// src/utils/config.js — API 키 탐색 순서
export function getApiKey(provider) {
  const config = loadConfig();

  // 1순위: 설정 파일
  if (config.apiKeys?.[provider]) {
    return config.apiKeys[provider];
  }

  // 2순위: 환경 변수
  const envMap = {
    openai: 'OPENAI_API_KEY',
    claude: 'ANTHROPIC_API_KEY',
    gemini: 'GEMINI_API_KEY',
  };
  return process.env[envMap[provider]] || null;
}

로컬 개발에서는 commitgen config로 설정 파일에 저장하고, CI/CD에서는 환경 변수를 쓰는 식으로 분리된다.

설정 파일 구조

{
  "provider": "openai",
  "model": null,
  "language": "en",
  "emoji": true,
  "apiKeys": {
    "openai": "sk-...",
    "claude": "sk-ant-...",
    "gemini": "AIza..."
  }
}

model: null이면 프로바이더별 기본 모델을 사용한다.

프로바이더 기본 모델 선택 이유
OpenAI gpt-4o-mini 빠르고 저렴, 커밋 메시지에 충분
Claude claude-3-5-sonnet 코드 이해력 우수
Gemini gemini-1.5-flash 무료 티어 활용 가능

프롬프트 엔지니어링

AI에게 커밋 메시지를 생성시키는 건 단순해 보이지만, 프롬프트 설계가 결과 품질을 결정한다.

프롬프트 구조

// src/utils/prompt.js
export function buildPrompt(diff, files, stats, options = {}) {
  return `You are a commit message generator. Analyze the following git diff
and generate a commit message following the Conventional Commits specification.

## Rules
1. Use the format: type(scope): subject
2. Types: feat, fix, refactor, docs, test, chore, style, perf, ci, build
3. Scope is optional — use it only when clearly identifiable
4. Subject line must be under 72 characters
5. If there are multiple significant changes, add a body with bullet points
6. ${langInstruction}
7. ${emojiInstruction}
8. Focus on the "why" and "what", not the "how"
9. Do NOT wrap the result in markdown code blocks

## Changed Files
${filesSummary}

## Diff Stats
${stats}

## Diff Content
\`\`\`
${diff}
\`\`\`

Generate ONLY the commit message. No explanations, no markdown formatting.`;
}

핵심 설계 결정 3가지:

  1. 파일 목록을 diff보다 먼저 제공 — AI가 전체 맥락을 파악한 후 상세 diff를 읽는다.
  2. "Generate ONLY"로 출력 제한 — 설명이나 마크다운 래핑 없이 순수 메시지만 받는다.
  3. temperature 0.3 — 커밋 메시지는 창의적일 필요 없다. 일관성이 더 중요하다.

한국어 처리

const langInstruction =
  language === 'ko'
    ? 'Write the commit message in Korean. The type prefix (feat, fix, etc.)
       should remain in English.'
    : 'Write the commit message in English.';

한국어 모드에서도 feat:, fix: 같은 타입 프리픽스는 영어로 유지한다. Conventional Commits 표준을 지키면서 본문만 한국어로 쓰는 방식이다.

// 영어 모드
✨ feat(cache): add TTL-based cache expiration logic

// 한국어 모드
✨ feat(cache): TTL 기반 캐시 만료 로직 추가

기술 스택 정리

기술 역할 대안 선택 이유
AIKit AI API 호출 OpenAI SDK 직접 사용 멀티 프로바이더 지원, 자체 라이브러리
Commander.js CLI 프레임워크 Yargs, Oclif 가볍고 충분
Inquirer.js 인터랙티브 프롬프트 Prompts, Clack 에디터 지원, list 선택
Chalk 터미널 색상 Colorette ESM 네이티브, 표준
Ora 스피너 애니메이션 cli-spinner 현대적 API
child_process git 명령 실행 simple-git 의존성 최소화

마무리

이 글에서는 CommitGen의 동기와 설계를 다뤘다. git diff를 파싱하고, AI로 분석하고, 인터랙티브하게 확인하는 전체 구조를 잡았다.

다음 편에서는 Commander.js CLI 설정, Inquirer.js 인터랙션 패턴, AIKit 연동, 프롬프트 엔지니어링의 실제 구현 코드를 다룬다.


기술 스택: Node.js, AIKit, Commander.js, Inquirer.js, Chalk, Ora
소스 코드: GitHub

반응형

'프로젝트 > CLI 도구' 카테고리의 다른 글

CommitGen #3 - NPM 배포 & 회고  (0) 2026.02.13
CommitGen #2 - 핵심 구현  (0) 2026.02.13