취준에 허덕이는 기간 뭐라도 더 해야하나.. 싶었던 와중 "오픈소스 기여는 어떻게 하는걸까?"라는 고민이 들었고
여러 유명 라이브러리들의 깃 히스토리를 살펴보다가 이전에 제가 사용하던 방식과는 전혀 다른 커밋 메시지 패턴이 보편적으로 쓰인다는 걸 발견했습니다. 🥺
특히 예전부터 개발하면서 쭉 참고하던 좋은 관례들을 모아서 정리를 해둔 RomuloOliveira의 commit messages guide 와는 다르게 많은 프로젝트들이 Conventional Commits라는 규칙을 적극적으로 사용하고 있었습니다.
커밋 메시지 컨벤션은 회사/팀 별로 다를테지만 실제로 기존에 하던 방식보다 낫다는 것을 느끼게 되었고, 새로운 커밋 메시지 스타일을 학습해볼겸
기존에 사용하던 스타일과 Conventional Commits 스타일을 비교하고, 실제 적용 방법과 장점을 정리해보고자 합니다.
RomuloOliveira님이 정리해서 공유한 커밋 메시지 가이드는 간단 요약해보자면 제목은 명령형으로 간결하게 작성하고 본문에는 상세 설명을 작성하는 스타일입니다.
처음 접했던 이 스타일이 그 당시엔 굉장히 일관적이고 협업 할 때 히스토리 파악에 유리한 스타일이라고만 생각했었습니다..
예시)
Add validation for user signup data유효성 검사가 없던 signup 요청에 대해 필수 필드 검증을 추가했습니다
Conventional Commits은 Angular 컨벤션 스타일을 단순화하고 누구나 쉽게 쓸 수 있게 만든 공개 스펙입니다.
RomuloOliveira님이 정리한 스타일과 마찬가지로 커밋 메시지를 일정한 형식으로 작성해서 변경 내역을 명확하게 표현할 수 있을뿐더러 자동화 도구와의 연동도 쉽게 할 수 있습니다.
개인적으로는 이전에 사용하던 스타일보다 이 방식이 더 일관성 있고 체계적이라는 점에서 커밋 메시지의 가독성이 더 높다고 느꼈고
자동화는 아직 크게 와닿지는 않지만 커밋의 목적과 범위, 변경 내용을 명확하게 드러낼 수 있다는 점에서 큰 매력을 느꼈습니다.
Conventional Commits의 기본 구조는 다음과 같습니다.
<type>[(scope)][!]: <description>
[body]
[footer(s)]
[구조 설명]
| 항목 | 설명 | 필수 |
|---|---|---|
<type> | 변경의 목적 (feat, fix 등), 커밋에 기록된 작업의 성격이 무엇인지 알려줌 | 필수 |
<scope> | 변경된 기능/모듈/폴더 등을 소괄호로 표기 | |
| ! | 이 커밋이 기존 코드와의 호환성을 깨뜨리는 breaking change인지 여부 | |
<description> | 한 줄 요약 (제목) | 필수 |
<body> | 변경 이유, 구현 방식, 참고 내용 등 자유롭게 작성 | |
<footer> | breaking change 또는 이슈 번호 등 |
커밋의 목적/유형을 알려주는 키워드입니다.
기본적으로는 fix와 feat를 필수로 사용하지만 앵귤러 컨벤션을 기반으로 하는 타입도 사용하도록 권고하고 있습니다.
(예: docs, style, refactor, test, chore 등)
또 대소문자 구분은 없지만 일관적으로 사용할 것을 권고합니다.
| 항목 | 설명 |
|---|---|
| feat | 새로운 기능 추가 (Minor 버전 변경) |
| fix | 버그 수정 (Patch 버전 변경) |
| docs | 문서 수정 (코드 변경 X) |
| style | 코드 포맷팅 |
| refactor | 리팩토링 (동작 변화 X) |
| test | 테스트 추가, 수정 |
| chore | 기타 잡일 (빌드, 설정, 로그 등등) |
| ci | CI 설정 관련 |
| build | 빌드 시스템, 외부 의존성 변경 |
| perf | 성능 개선 |
| revert | 기존 커밋 되돌림 |
어떤 기능/모듈에 변경이 있었는지 명시하는 부분입니다.
예시)
feat(auth): add signup usecase
fix(signup): prevent duplicate email registration
chore(deps-dev): update eslint and prettier
스코프는 선택 사항이지만 명시하면 커밋 로그의 가독성이 매우매우 좋아집니다.
느낌표 기호(!)는 해당 커밋이 breaking change(기존 코드와의 호환성이 깨지는 변경)임을 명시적으로 강조할 때 사용합니다.
예시)
feat(file)!: change upload API to accept multipart/form-dataBREAKING CHANGE: File upload API now expects 'multipart/form-data' instead of base64 string
이처럼 느낌표를 사용하거나 footer에 BREAKING CHANGE:를 작성하는 방식 중 하나를 반드시 사용하면 breaking change로 쉽게 인식됩니다.
또 semVer에서 Major 버전 변경과 연관되어 있습니다.
변경 내용을 한 줄로 간결하게 요약하는 부분인데 일반적으로 사용되는 간단한 규칙이 존재합니다.
예시)
feat(auth): add signup usecase
fix(signup): prevent duplicate email registration
chore(deps-dev): update eslint and prettier
변경 사항의 배경, 이유, 상세 설명을 여러 줄에 걸쳐 작성하는 부분입니다.
의미와 설명이 중요하기 때문에 내부 프로젝트중에 꼭 영어로 써야 한다는 강제는 없지만 type은 영어로 고정되어 있어야 자동화 도구들이 제대로 인식합니다.
그렇지만 또 만약 오픈소스 프로젝트라면 영어로 작성하는 것이 좋을 것 같습니다.
예시)
fix(signup): prevent duplicate email registration동일한 이메일 주소로 여러 번 회원가입이 가능해서 로그인이나 계정 복구 과정에서 혼란이 발생할 수 있었습니다.
[수정사항]
- signup 서비스에 이메일 중복 여부를 검사하는 로직 추가
- DB에 email 필드에 대한 unique constraint 적용
- 이미 가입된 이메일인 경우 클라이언트에 명확한 에러 메시지 전달이 되도록 개선
이슈 참조나 breaking change 설명을 작성하는 부분입니다.
예시 1)
Closes #456
Refs: #123, #789
예시 2)
BREAKING CHANGE: The signup endpoint now requires a phone number and detailed address.
커밋 메시지만으로도 무엇을/왜/무슨 컨텍스트에서 변경했는지 빠르게 파악할 수 있습니다.
type 으로 커밋의 성격(기능 추가, 버그 수정 등)이 명확히 구분됨scope 를 통해 어느 기능/모듈에 해당하는지 컨텍스트가 명확subject 를 일관된 형식으로 작성하면 커밋 로그 자체가 가독성이 좋은 문서처럼 읽힘예시)
fix(signup): prevent duplicate email registration
"아 회원가입 기능에서 중복 이메일 가입 이슈를 수정했구나"chore(deps): update class-validator to v0.14.1
"class-validator 의존성 업데이트했네"test(auth): add unit tests for signup logic
"회원가입 유닛 테스트를 추가했구나"
커밋 메시지의 형식이 정해져 있기 때문에 다양한 자동화 도구와 쉽게 연동되고 릴리즈와 배포까지 이어지는 기반이 됩니다.

컨벤셔널 커밋 스타일을 사용해서 커밋 메시지만 잘 작성하면 봇이 changelog를 자동 생성하고 버전 관리도 자동화해준다고 하는데… 어떻게 하는걸까요
NodeJS 기준으로 설명을 해보자면 semantic-release 패키지를 설치하고 .releaserc 파일과 github action yaml 파일, 이 2가지만 잘 설정해둔 뒤, 커밋메시지만 잘 작성한다면 계속 버전관리 자동화, changelog 자동 생성이 가능합니다.
npm i -D semantic-release @semantic-release/git @semantic-release/changelog
@semantic-release/git : CHANGELOG.md, package.json 가 변경되었다면 파일을 다시 커밋하고 push@semantic-release/changelog : 커밋 기록을 분석해서 changelog 자동 생성.releaserc는 semantic-release의 동작을 정의하는 설정입니다.
// ./.releaserc
{
"branches": [
"main",
{
"name": "dev",
"prerelease": "beta"
}
],
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
"@semantic-release/changelog",
[
"@semantic-release/git",
{
"assets": [
"CHANGELOG.md",
"package.json"
],
"message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
}
]
]
}
| 옵션 | 설명 |
|---|---|
| branches | 릴리즈가 발생할 브랜치 지정 |
| plugins | semantic-release 플러그인 목록 |
| assets | 릴리즈 시 자동 커밋에 포함할 파일 목록 (package.json, CHANGELOG.md 등) |
| message | 릴리즈 커밋 메시지 형식 ([skip ci]는 릴리즈 커밋에서 CI 재실행 방지용) |
릴리즈 자동화를 CI 환경에서 실행하기 위한 설정입니다.
원하는 릴리즈용 브랜치(보통 main)에 push 이벤트가 발생했을 때 감지해서 semantic-release를 실행하는 구조입니다.
# ./github/workflows/release.yml
name: Release
on:
push:
branches:
- main # main에 push가 됐을 때 실행
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
persist-credentials: false # 기본 깃헙 토큰 제거. false를 안하면 에러 발생....
- uses: actions/setup-node@v3
with:
node-version: 18
- name: Install dependencies
run: npm ci
- name: Run semantic-release
env:
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} # 리포지터리 설정에 저장해둔 secrets로 깃헙 토큰 설정
run: npx semantic-release # CI 환경에서(깃헙액션 호스트) 자동화 실행
여기서 한가지 주의할 점은 semantic-release 가 릴리즈 관련 커밋과 태그를 push하려면 GitHub Personal Access Token이 필요합니다. 없으면 봇이 권한을 얻지 못해서 원격 리포지터리에 push를 할 수 없습니다.
생성한 토큰은 깃헙 리포 secrets로 미리 등록을 해둬야하고, GitHub Actions는 GITHUB_TOKEN 으로 인증을 시도하는데 그때 push 권한이 제한되기 때문에 persist-credentials: false 옵션을 설정해서 기본 토큰을 제거해줘야 secrets으로 설정한 토큰변수가 제대로 부여됩니다.

(persist-credentials: false 를 해주지 않았을 때 permission 관련 에러 발생)
이것들을 잘 설정한 이후엔 컨벤셔널 커밋 스타일에 맞춰서 커밋한 뒤, PR올리고 dev 브랜치에서 main브랜치로 머지를 한다면 봇이 추적해서 알맞은 버전관리와 Changelog 갱신을 해줍니다.
과거 제 프로젝트 커밋 히스토리엔 아래와 같은 커밋들이 다수 존재했습니다..


이 커밋 메시지들을 본 내 표정: ㄱ-.....
CI/CD 관련
| 과거 버전 | 개선 버전 |
|---|---|
| Update cd script | chore(ci): update CD script |
| Update ci script | chore(ci): update CI script |
| Update dockerfile | chore(ci): update Dockerfile |
| Solve cd issue | fix(ci): resolve CD issue |
| Implement dev CI/CD | chore(ci): implement dev CI/CD pipeline |
| Update ci.dev.yaml | chore(ci): update ci.dev.yaml configuration |
| Add Dockerizing to ci.dev.yaml | chore(ci): add Dockerfile steps to ci.dev.yaml |
usecase 관련
| 과거 버전 | 개선 버전 |
|---|---|
| Delete useless property in CreateUserDto | refactor(user): remove unused property in CreateUserDto |
| Fix gender, job property type to enum | fix(user): change gender, job type to enum |
| Fix model job, birth, gender property to nullable | fix(user): make job, birth, gender nullable in User model |
| Add new column, property in model, entity(birth, gender, job) | feat(user): add birth, gender, job to User entity/model |
| Rename enum to simplify | refactor(user): simplify enum names |
| Add JobEnum, GenderEnum | feat(user): introduce JobEnum, GenderEnum types |
| Rename exception to fit convention | refactor(core): rename exception classes to match convention |
| Add comment for dev | chore: add comments for dev |
| Add UserNotRegisteredException | feat(auth): add UserNotRegisteredException for unregistered users |
의존성 관련
| 과거 버전 | 개선 버전 |
|---|---|
| Add csurf | chore(deps): add CSRF middleware |
| Add helmet | chore(deps): add Helmet for HTTP header protection |
속이 뻥!!
일관적이라 가독성도 좋고 스코프 덕분에 무슨 컨텍스트의 어떤 변경사항인지 쉽게 알 수가 있다.
틀린 부분이 있으면 지적 부탁드립니다!