Conventional Commits가 왜 잘못된 기준이며, 커밋 메시지는 타입보다 스코프를 우선해야 하는지 설명합니다.
Conventional Commits를 이전에 거의 확실히 접해 보셨을 것입니다. 사용해 본 오픈 소스 프로젝트의 변경 로그에서 그 흉측한 모습을 드러냈을 수도 있습니다. 기여했던 오픈 소스 프로젝트에서 강제된 커밋 형식이었을 수도 있습니다. 이것을 강하게 옹호하는 사람도 많습니다. 저는 이것에 대해 욕을 합니다.
비록 많은인기오픈소스프로젝트에서사용되고있지만, Conventional Commits는 잘못된 것에 집중하도록 부추기는 동시에 약속한 바를 지키지 못하는 적극적으로 해로운 표준입니다.
Conventional Commits는 커밋 메시지에 의미론적 뜻을 부여해 개발자와 최종 사용자가 커밋에서 이루어진 변경을 이해하는 데 도움을 주겠다고 약속합니다. 그러나 Conventional Commits는 이 일을 놀라울 정도로 형편없이 수행합니다. 이를 보여주기 위해, conventional commit의 구조를 살펴봅시다. Conventional Commit 웹사이트에 따르면 커밋 메시지는 다음과 같은 형식이어야 합니다:
<type>[optional scope]: <description>
[optional body]
[optional footer(s)]
커밋의 제목 줄에는 변경 유형을 설명하는 <type>(fix, feat, chore, docs, refactor 같은 것들1)이 있습니다. 그 뒤에는 선택적인 스코프가 오고, 그 다음에 설명이 옵니다.
이 형식에는 중대한 결함이 있습니다. 타입이 스코프보다 우선시된다는 점입니다. 이것은 완전히 거꾸로입니다.
변경의 스코프(변경의 주제)는 커밋에서 가장 중요한 부분입니다. 이를 보여주기 위해, 다음 이해관계자 각각이 왜 변경의 타입보다 변경의 스코프를 더 중요하게 여기는지 생각해 봅시다:
기여자: 프로젝트의 기여자라면, 코드베이스의 특정 영역과 관련된 변경을 식별하기 위해 커밋 로그를 읽어야 하는 경우가 자주 있습니다. 여기에는 여러 이유가 있습니다:
커밋 로그를 읽을 때 당신은 어떤 영역들 이 수정되었는지를 보고 있습니다. 일어나는 변경의 타입 은 정말로 중요하지 않고, 중요한 것은 변경의 스코프 입니다.
디버거: 버그를 조사할 때, 버그가 드러난 컴포넌트와 관련된 영역을 건드렸을 수 있는 변경을 보기 위해 커밋 로그를 뒤지는 경우가 많습니다. 다시 한 번, 스코프가 가장 중요한 정보입니다. 변경 타입은 완전히 쓸모없습니다. 버그는 타입과 상관없이 어떤 변경에서도 도입될 수 있기 때문입니다. (버그를 고치려다 다른 버그를 만든 경험은 우리 모두에게 있을 것입니다.)
인시던트 대응자: 운영 환경이 다운되었을 때, 장애가 발생한 시점 전후에 이루어진 변경을 찾기 위해 커밋 로그를 훑어보는 것은 어떤 영역이 문제를 일으키는지 식별하는 효과적인 방법입니다. 이 시점에서도 스코프는 다시 한 번 가장 중요한 정보입니다. 예를 들어, 유입 API 오류가 급증한 시점의 끝자락에 auth 스코프와 관련된 커밋이 보인다면, 그것이 문제의 유력한 원인일 가능성이 큽니다. 그리고 다시 한 번, 타입은 무관합니다. 버그는 어떤 변경으로도 추가될 수 있기 때문입니다.
그렇다면 Conventional Commits는 무엇을 할까요? 스코프의 우선순위를 너무 낮게 둬서 아예 선택 사항 으로 만들어 버립니다! 도대체 왜 스코프 가 선택 사항이어야 하나요? 스코프 없는 커밋은 주어 없는 문장과 같습니다! 거기에 더해, Conventional Commits는 타입 을 커밋 메시지 맨 앞으로 끌어올립니다. Conventional Commits는 스코프와 타입의 우선순위를 완전히 잘못 잡고 있습니다.
아마 이렇게 생각하실 수도 있습니다. “그래도 순서가 뒤바뀐 것뿐이지, 커밋 타입 자체는 여전히 중요하지 않나?” 제 대답은 “아니오”입니다. 커밋의 설명은 거의 항상 변경의 타입을 이미 말해 줍니다! 예시로 이 커밋 메시지를 보겠습니다:
fix(compiler): prevent namespaced SVG <style> elements from being stripped
설명만 봐도 이것이 버그 수정이었다는 점은 분명합니다! 커밋 제목 줄의 공간은 원래도 매우 귀한데, 타입에 글자 수를 낭비하는 것은 도움이 되지 않습니다! 하지만 종종 이것은 쓸모없는 정도를 넘어 제약적이기까지 합니다. 예시로 이 커밋 메시지를 보겠습니다:
refactor(core): Update webmcp support to use document.modelContext
이 커밋은 core 컴포넌트의 webmcp 기능을 업데이트하여 document.modelContext와 navigator.modelContext를 모두 지원하게 했습니다. 그렇다면 이것은 버그 수정일까요, 리팩터링일까요, 아니면 새 기능일까요? 제 생각에는 셋 다입니다! 하지만 다시 말하지만, 정말 중요한 유일한 점은 이것이 core/webmcp 컴포넌트에 대한 변경이었다는 사실입니다.
Conventional Commits는 근본적으로 잘못된 것(커밋 타입)에 집중하고, 사람들이 실제로 신경 쓰는 스코프의 가치를 깎아내립니다.
이제 Conventional Commits의 형식이 형편없다는 점은 확인했지만, 그래도 어떤 이점은 제공하겠지요. Why Use Conventional Commits 섹션을 읽고 그 이유들이 말이 되는지 봅시다.
이것이 Conventional Commits의 가장 큰 약속입니다. git-cliff나 conventional-changelog 같은 도구를 실행해 마지막 릴리스 이후의 커밋으로부터 변경 로그를 생성할 수 있다는 것입니다. 그런데 이게 정말 좋은 생각일까요? 아닙니다! 변경 로그의 독자층은 커밋 로그의 독자층과 완전히 다릅니다!
변경 로그는 사용자 대상이며, 사용자는 버전 간의 기능적 차이를 이해하는 데 관심이 있습니다. 그들은 비즈니스/기능적 관점에서 무엇이 바뀌었는지에 관심이 있습니다.
커밋 로그는 개발자 대상이며, 개발자는 코드베이스가 시간에 따라 어떻게 바뀌어 왔는지에 대한 이야기를 읽는 데 관심이 있습니다. 그들은 스코프 관점에서 무엇이 바뀌었는지에 관심이 있습니다.
보시다시피 이 둘은 완전히 다른 입도이며, 이를 결합하려는 어떤 시도도 수준 이하의 결과를 낳습니다. 그 이유는 여러 가지입니다:
* 조금만 복잡한 프로젝트에서도 눈에 띄는 기능 하나를 반영하려면 여러 커밋이 필요합니다. 그 기능이 반영되는 과정(커밋 로그에 기록되는 대로)은 개발자와 기여자에게는 가치가 있지만 최종 사용자에게는 쓸모없습니다. 최종 사용자는 새 기능만 신경 쓰지, 그것이 어떻게 만들어졌는지는 신경 쓰지 않습니다!
* [Rich가 지적했듯이](https://richvdh.org/conventional-commits-considered-harmful.html#reverts-are-hard "(opens in new tab)"), 되돌리기(revert)는 Conventional Commits에 문제를 일으킵니다. revert 커밋은 개발자에게는 커밋 로그의 이야기라는 관점에서 중요하지만, 최종 사용자에게는 되돌려진 변경은 애초에 이루어지지 않은 변경과 같습니다.
듣기에는 좋아 보이지만, 소프트웨어 엔지니어링의 현실은 이 작업을 정확하게 수행하는 가능성을 크게 방해하곤 합니다. 다음 상황들을 생각해 보세요:
* **되돌리기:** 당신이 도입한 호환성 파괴 변경이 너무 치명적이어서 결국 되돌려야 하는 상황을 상상해 보세요. 당신의 도구는 호환성 파괴 변경을 감지하고 메이저 버전을 올리겠지만, 실제로 그 파괴는 되돌려졌으므로 호환성 파괴 변경은 존재하지 않습니다.
* **우발적 파손:** 파손이 미묘해서, 변경을 만들 때는 그것이 호환성 파괴 변경이라는 사실을 깨닫지 못할 수도 있습니다. 나중에 돌아보고서야 그것이 파괴적이었다는 사실을 알게 됩니다. 이 경우 메이저 버전 증가가 필요함에도 잘못해서 마이너/패치 버전을 올리게 됩니다.
* **사후적 비파괴화:** 나중에 추가한 커밋이 이전의 파괴적 커밋과 조합되어, 결과 diff가 더 이상 파괴적이지 않게 되는 경우를 생각해 보세요. revert 상황과 비슷하게, 도구는 잘못해서 호환성 파괴 변경이 있다고 판단할 것입니다.
이런 상황에서는 rebase로 히스토리를 다시 쓸 수 있겠지만, 그것은 종종 워크플로에 의해 막히거나 문제를 일으킵니다. 또한 이는 프로젝트에 기여하려는 기여자들에게 수정주의적 히스토리를 제시하게 되어, 커밋 로그가 들려주는 이야기의 신뢰성을 떨어뜨립니다.
지금까지 확인했듯이, 동료와 대중은 변경 로그와 커밋 로그에 대해 매우 다른 필요를 가집니다. Conventional Commits는 둘 다 해결하지 못합니다.
이건 그냥 나쁜 생각입니다. 예를 들어 코드에 영향을 주는 커밋에만 자동 보안 검사를 수행한다고 해 봅시다. 그런데 누군가 docs: fix typos라는 제목의 트로이 목마 커밋을 만들어 실제로는 인증 서브시스템에 취약점을 도입한다면 어떨까요? 물론 그런 악의적인 활동은 이상적으로는 코드 리뷰에서 잡히기를 바라겠지만, 자동화 도구는 우회되었고, 문제를 식별하는 책임이 사람에게 떠넘겨집니다.
연산 자원은 저렴합니다. git diff를 사용해 변경된 파일(다시 한 번, 스코프)을 식별하고, 그것을 바탕으로 빌드/배포 프로세스를 실행하세요.
더 구조화되어 있는 것은 맞습니다. 하지만 기여를 더 쉽게 만든다고요? 전혀 아닙니다(이미 충분히 길게 입증했습니다).
Conventional Commits의 “판매 포인트” 가운데 실제로 설득력 있는 것은 단 하나도 없습니다.
Conventional Commits는 프로젝트에 적용하기도 매우 어렵습니다. 자체적인 “타입” 집합을 정의하라고 하지만, 사실상 거의 모든 사람이 commitlint의 기본값을 그대로 가져오며, 이것은 개별 프로젝트의 특성에 잘 맞지 않는 경우가 많습니다. 이 문제는 변경 관리와 감사 요구사항 때문에 모든 커밋 메시지에 티켓 번호를 넣어야 하는 기업 환경에서 특히 심각합니다. <scope> 필드는 그것을 넣기에 가장 뻔한 자리이지만, 그렇게 되면 Conventional Commit에서 유일하게 유용한 메타데이터가 완전히 쓸모없는 티켓 번호로 대체되어 버립니다.
그렇다면 대신 무엇을 해야 할까요? Linux, FreeBSD, Git, Go, NixOS 같은 진정으로 성공한 소프트웨어 프로젝트들을 따르세요! 이 프로젝트들의 공통점은 무엇일까요? 모두 스코프 접두사 기반 커밋 메시지를 사용한다는 점입니다(여기서 “스코프”는 실제 프로젝트와 관련되도록 정의됩니다). 보통 특정 프로젝트에서 어떤 스코프를 써야 하는지는 자명합니다. Linux 커널에서는 서브시스템이 자연스러운 스코프입니다. Go 프로젝트에서는 패키지 경로가 자연스러운 스코프입니다. 마이크로서비스 아키텍처를 사용하는 프로젝트에서는 마이크로서비스 이름이 자연스러운 스코프입니다.
다음은 몇몇 프로젝트와 그들의 커밋 형식 가이드라인 예시입니다.
| Project | Format | Example |
|---|---|---|
| Linux | subsystem: description | i2c: virtio: mark device ready before registering the adapter |
| FreeBSD | prefix: Description | linuxulator: Return EINVAL for invalid inotify flags |
| Git | area: description | gitlab-ci: update macOS image |
| Go | package: description | net/http/cookiejar: add godoc links |
| nixpkgs | pkg-name: description | xwayland: 24.1.11 -> 24.1.12 |
| Node.js | subsystem: description | stream: fast-path stateless transform flush results |
안타깝게도, 지금까지 만들어진 가장 성공적인 오픈 소스 프로젝트들 일부에서 사용되고 있음에도 불구하고, 이 커밋 스타일은 브랜딩 경쟁에서 밀린 것처럼 보입니다. 저는 그것을 바꾸려 합니다. scopedcommits.com을 소개합니다. 이 웹사이트는 제정신인 커밋 메시지로 돌아가자는 주장을 펼치고, 변경 로그 생성의 관심사와 커밋 로그 관리의 관심사를 분리하는 데 전념하고 있습니다.
Conventional Commits가 내세우는 장점들은 사실 환상에 불과하며, 업계는 이것을 표준으로 사용함으로써 어떤 실질적인 이득도 보지 못했습니다. 그러나 안타깝게도 Conventional Commits는 오픈 소스 프로젝트들에서 꽤 인기를 얻게 된 듯하며, 그 결과 AI들이 커밋 메시지에 이것을 기본값으로 사용하는 습관을 가진 것처럼 보입니다. 이것은 안티패턴으로 가득한 커밋 메시지가 프로젝트 전반에 퍼지게 만들었습니다.
이 글에서 제 목표는 Conventional Commits의 지배력에 맞서 싸우고, 커밋 메시지를 구조화하는 더 나은 방법들이 있음을 보여주는 것입니다. 하지만 이 글이 당신을 설득해 Conventional Commits 사용을 멈추게 하지 못했다면, 댓글란에서 벌어질 불꽃 튀는 논쟁을 기대하겠습니다.
fix와 feat만 정의하고, 추가 타입은 각 프로젝트가 정하도록 남겨 둡니다. 하지만 대부분의 프로젝트는 결국 commitlint가 정의한 타입들을 사용하게 되므로, 저는 이 목록에 그중 일부를 포함했습니다.↩︎