모노레포 체인지로그: 하나로 합칠까, 패키지마다 따로 둘까
4분 분량
모노레포는 하나의 저장소 안에 별도로 배포되는 여러 가지를 담고 있으며, 체인지로그는 먼저 하나의 질문에 답해야 한다. 독자가 신경 쓰는 것이 저장소 자체인지, 아니면 그 안의 특정 패키지인지. 대부분의 팀은 이것을 의도적으로 결정한 적이 없다. 저장소가 하나이기 때문에 체인지로그도 하나로 시작하고, 시간이 지나면서 패키지를 계속 추가하다가, CLI를 쓰는 사람이 자신의 수정 사항이 담긴 항목을 찾으려고 관련 없는 백엔드 항목 마흔 개를 지나쳐야 하는 로그로 끝나게 된다. 올바른 형태를 결정하는 것은 저장소의 구조가 아니라, 누가 그 로그를 읽고 이미 무엇을 찾으려 하는지다.
모노레포의 체인지로그는 단일 저장소의 것과 무엇이 다른가
단일 저장소의 체인지로그에는 암묵적인 독자층이 있다. 그 저장소가 만드는 유일한 것을 사용하는 모든 사람이다. 모노레포의 독자층은 패키지별로 나뉘고, 같은 저장소 안의 패키지들은 흔히 서로 다른 일정으로, 서로 다른 소비자에게, 서로 다른 안정성 수준으로 출시된다. 레지스트리에 게시되는 라이브러리와 내부 관리 도구가 같은 모노레포에 함께 살면서도, 체인지로그를 읽는 사람 입장에서는 거의 공통점이 없을 수 있다.
| 저장소 형태 | 전형적인 독자 | 어울리는 체인지로그 |
|---|---|---|
| 배포 가능한 앱 하나 | 그 제품을 쓰는 모든 사람 | 저장소 전체를 위한 로그 하나 |
| 라이브러리 워크스페이스 (여러 개의 게시된 패키지) | 특정 패키지에 의존하는 사람 | 패키지마다 로그 하나 |
| 앱 더하기 내부 도구 | 겹치지 않는 두 독자층 | 폴더가 아니라 독자층에 따라 분리 |
| 앱 더하기 자체 SDK | 제품 사용자, 그리고 SDK 통합자 | 두 개의 로그: 제품용, SDK용 |
모든 패키지가 자기만의 체인지로그를 가져야 하는가
독립된 독자층을 가진 패키지만 그렇다. 레지스트리에 게시되는 패키지는 자기만의 로그가 필요한데, 그것을 설치하는 사람은 저장소의 다른 무언가를 읽을 이유가 없고, Lerna나 Changesets 같은 모노레포 릴리스 도구는 패키지마다 CHANGELOG.md를 그 package.json 옆에 작성하기 때문이다. 같은 저장소에 이미 존재하는 앱이라는 소비자 하나만 있는 내부 유틸리티는 별도의 로그가 필요하지 않다. 그 변경 사항을 그 앱의 항목에 포함시키는 것이 팀 밖의 누구도 열어보지 않는 두 번째 파일보다 더 유용하다.
테스트는 어떤 항목이든 애초에 체인지로그에 속하는지를 결정하는 것과 동일하다. 독자가 그것을 알아채거나 신경 쓸 것인가, 그리고 그것을 알고 나서 행동할 수 있는가. 이것을 폴더가 아니라 패키지 단위로 적용하면, 패키지 열두 개짜리 저장소가 진짜 체인지로그 두 개와 아예 필요 없는 패키지 열 개로 끝날 수도 있다.
어떤 패키지가 어떤 체인지로그 항목을 일으켰는지 어떻게 아는가
각 항목을 커밋이 어떤 파일을 건드렸는지 나중에 조사하는 것이 아니라, 그 항목이 쓰이는 순간에 해당 패키지로 표시하라. 공유되는 내부 라이브러리를 고치는 커밋은 거기에 의존하는 모든 패키지에서 체인지로그 항목을 만들어낼 수 있으며, 파일 경로만으로는 이 하위 항목들 중 어떤 것을 독자가 정말로 봐야 하는지 말해줄 수 없다. “이것은 패키지 A를 쓰는 사람에게는 보이고 패키지 B를 쓰는 사람에게는 보이지 않는다”라고 결정할 수 있는 것은 사람뿐이다. Conventional commits는 각 커밋에서 패키지를 명시함으로써 여기서 기계적으로 도움을 주지만, 스코프는 여전히 초안 하나만 만들어낸다. 그 글과 같은 두 단계 규칙이 패키지 단위로도 적용된다. 올바른 스코프를 가진 초안도 그 패키지의 실제 독자를 위해 표현되기 전에는 사람의 손길이 여전히 필요하다.
공유 체인지로그가, 단일 저장소의 체인지로그에는 필요 없는 무엇을 필요로 하는가
각 항목의 맨 앞, 설명 이전에 오는 패키지 라벨이다. 그래야 로그를 훑어보는 독자가 한 번 훑는 것만으로 자신과 관계없는 모든 것을 건너뛸 수 있다. 그 라벨이 없으면 공유 로그는 무작위 피드처럼 읽히고, 하나의 패키지에 관심 있는 독자는 어떤 줄이 중요한지 외우는 것 말고는 그것을 걸러낼 방법이 없는데, 첫 주가 지나면 아무도 그렇게 하지 않는다.
## 2026-09-07
### [cli] 추가됨
- `acme push --dry-run`이 실제로 보내지 않고 무엇이
보내질지 보여준다.
### [core] 수정됨
- 빈 본문을 반환하는 성공한 요청에서 재시도 백오프가 더 이상
초기화되지 않는다.
두 개의 항목, 두 개의 독자층, 구분하는 데 한 번 훑어보면 충분하다. Changesets 방식의 워크플로는 이 표시 작업을 릴리스 프로세스에 직접 내장한다. 기여자가 자신의 변경 사항 옆에 패키지 스코프를 가진 짧은 노트를 쓰고, 도구는 릴리스 시점에 그 노트들로부터 패키지별 체인지로그와 버전 상승을 조립한다. 통합된 커밋 이력에서 나중에 패키지 경계를 재구성하려 시도하는 대신 말이다.
버전 관리는 모노레포 체인지로그와 어떻게 연결되는가
독립적으로 버전이 매겨지는 패키지들은 자기만의 버전 번호를 갖기 때문에 자기만의 체인지로그가 필요하며, 공유 체인지로그는 “패키지 A는 2.1에서 2.2로 올라갔는데 패키지 B는 1.4에 머물렀다”를 하나의 파일 안에 두 개의 로그가 되지 않고서는 표현할 수 없다. 시맨틱 버저닝과 당신의 체인지로그가 버전 번호 자체가 체인지로그 카테고리에 어떻게 매핑되어야 하는지를 다룬다. 모노레포에서는 그 매핑을 패키지 단위로 적용해야 하는데, 한 패키지의 breaking change가 그것에 의존하지 않는 자매 패키지에는 breaking change가 아니기 때문이다.
여러 내부 패키지로 만들어졌더라도 하나의 제품을 하나의 배포 단위로 출시하는 저장소는 이런 문제를 겪지 않는다. 패키지들은 항상 함께 출시되기 때문에 버전을 공유하며, 체인지로그 하나면 충분하다.
git 태그는 모노레포에 어떻게 맞아 들어가는가
git 태그, 릴리스, 그리고 당신의 체인지로그에 나온 것과 같은 규칙이 패키지 단위로 적용되어 그대로 성립한다. 자기만의 버전을 가진 패키지는 자기만의 태그 접두사가 필요하며, 보통 어떤 패키지에 속하는지 말해줄 수 없는 벌거벗은 v1.4.0 대신 패키지이름@1.4.0이 쓰인다. 벌거벗은 버전 번호로만 태그가 붙은 모노레포는 나중에 “cli가 2.2를 출시했을 때 core에는 무엇이 있었는가”에 답할 수 없는데, 그 태그가 실제로 어느 패키지에 속했는지를 디스크에 아무것도 기록해두지 않았기 때문이다.
FAQ
모노레포 안의 모든 패키지마다 별도의 체인지로그가 필요한가? 독립된 독자층을 가진 패키지만 그렇다, 보통 레지스트리에 게시되는 것은 무엇이든 해당한다. 같은 저장소에 이미 존재하는 내부 소비자가 하나뿐인 패키지는 자기만의 로그를 유지하는 대신 그 소비자의 로그에 합류할 수 있다.
체인지로그 항목을 올바른 패키지로 표시하는 것은 무엇인가? 변경된 파일 경로에 대한 자동 스캔이 아니라, 항목을 쓰는 사람이 그것을 쓰는 그 순간이다. 공유 라이브러리의 변경은 거기에 의존하는 각 패키지에서 서로 다른 항목을 만들어낼 수 있으며, 그 하위 항목 각각이 실제로 무엇을 말해야 하는지는 오직 사람만이 결정할 수 있다.
모노레포는 모든 것에 하나의 버전 번호를 써야 하는가? 모든 패키지가 항상 나머지와 함께 출시되는 경우에만 그렇다. 패키지들이 언젠가 독립적으로 게시된다면 독립된 버전이 필요하고, 독립된 버전은 의미가 통하려면 독립된 체인지로그가 필요하다.
모노레포 체인지로그 도구가 사람이 편집하는 단계를 대체하는가? 아니다. Changesets 같은 도구는 릴리스 시점에 패키지별 노트를 모으고 조립하는 작업을 자동화한다. 노트 자체는, 기여자가 아니라 독자의 언어로 쓰인 채로, 다른 어떤 체인지로그 파이프라인에서와 마찬가지로 여전히 사람의 몫으로 남는다.
이 글의 기술적 내용은 독립적으로 검토되지 않았습니다. 잘못된 부분이 있으면 알려 주세요. 바로잡겠습니다.