03 — Changesets로 SemVer 자동화: snapshot·prerelease·publish
이 문서가 답하는 질문: 모노레포 안의 7~10개 패키지를 어떻게 기능 단위로 버저닝하고, PR 흐름에 어떻게 녹이며, snapshot/prerelease 같은 비공식 릴리스는 어떻게 다루는가. 한 줄 답 (Pyramid Top): Changesets는 **
(1) 개발자가 PR에 변경 의도 markdown을 동봉 → (2) 메인 머지 시 CI가 자동으로 "Version Packages" PR을 생성 → (3) 그 PR이 머지되면 publish**의 3-스텝 SemVer 자동화다.
Why — 왜 Changesets인가
| 풀려는 문제 | 이전 해법 | 한계 |
|---|---|---|
| 모노레포 패키지별 다른 버전 | Lerna independent mode | publish 단계가 수동·실수 잦음 |
| ”어떤 PR이 어떤 패키지를 깼는가” 추적 | 커밋 메시지 컨벤션(feat:, fix:) + semantic-release | 단일 패키지만 잘 동작 |
| 의존 관계 따라 버전 자동 bump | Lerna의 --conventional-graduate | 토큰 minor bump → react도 minor가 과한 경우 있음 |
| Release notes 자동 생성 | conventional-changelog | 커밋 메시지에 의존 — 사람이 잘 안 씀 |
| 변경 의도와 영향 범위를 PR 단위로 명시 | — | Changesets가 등장 |
Changesets는 **“개발자가 PR마다 ‘이 변경이 어떤 패키지를 어떻게 깰지’ markdown으로 적어둔다”**는 단순한 사회적 합의를 자동화의 입력으로 삼는다.
How — 3-스텝 워크플로
핵심은 두 단계 PR:
- 기능 PR — 코드 + changeset markdown
- Version PR — CI가 자동 생성, package.json·CHANGELOG.md만 변경
이 분리가 **“기능 머지 ≠ 릴리스”**를 강제한다 — 릴리스는 Version PR 머지 시점에만 일어난다.
What — 실제 설정과 명령어
초기 설정
pnpm add -D -w @changesets/cli
pnpm changeset init생성되는 .changeset/config.json:
{
"$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json",
"changelog": [
"@changesets/changelog-github",
{ "repo": "org/design-system" }
],
"commit": false,
"fixed": [],
"linked": [
["@org/react", "@org/tailwind-preset", "@org/panda-preset"]
],
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": ["@org/docs"],
"snapshot": {
"useCalculatedVersion": true,
"prereleaseTemplate": "{tag}-{datetime}"
}
}| 옵션 | 의미 |
|---|---|
fixed | 항상 같은 버전으로 publish할 패키지 그룹 |
linked | bump 시 같이 올라가야 하는 패키지 그룹 (다른 패키지는 영향 없으면 그대로) |
updateInternalDependencies | 워크스페이스 의존성 자동 bump 단위 (patch/minor) |
ignore | publish 제외 (docs 앱처럼 private인 것) |
access: "public" | scoped 패키지(@org/...)의 기본 publish 권한 |
Changeset 생성
pnpm changeset대화형:
🦋 Which packages would you like to include? (Use arrow keys/space)
◯ @org/tokens
◉ @org/react
◯ @org/icons
🦋 Which packages should have a major bump?
◯ @org/react
🦋 Which packages should have a minor bump?
◉ @org/react
🦋 Please enter a summary for this change:
> Button: size prop "xl" 추가생성되는 .changeset/lucky-cats-jump.md:
---
"@org/react": minor
---
Button: size prop "xl" 추가
- 새로운 `size="xl"` 옵션
- BREAKING이 아닌 추가만CI 워크플로
.github/workflows/release.yml:
name: Release
on:
push:
branches:
- main
concurrency: ${{ github.workflow }}-${{ github.ref }}
jobs:
release:
name: Release
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
id-token: write # npm provenance
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 20
cache: pnpm
registry-url: https://registry.npmjs.org
- run: pnpm install --frozen-lockfile
- run: pnpm turbo run build typecheck test
- name: Create Release PR or Publish
id: changesets
uses: changesets/action@v1
with:
publish: pnpm release
version: pnpm version-packages
commit: "chore: version packages"
title: "chore: version packages"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
NPM_CONFIG_PROVENANCE: true루트 package.json:
{
"scripts": {
"version-packages": "changeset version && pnpm install --no-frozen-lockfile",
"release": "pnpm turbo run build && changeset publish"
}
}changesets/action은 두 모드로 동작한다:
- 대기 중인 changeset이 있음 → “Version Packages” PR 생성/업데이트
- 대기 중인 changeset이 없음 (Version PR이 머지된 직후) → publish 실행
CHANGELOG 자동 생성 예시
@org/react/CHANGELOG.md:
# @org/react
## 1.2.0
### Minor Changes
- abc1234: Button: size prop "xl" 추가 ([#142](https://github.com/org/ds/pull/142))
### Patch Changes
- Updated dependencies [def5678]
- @org/tokens@1.1.1updateInternalDependencies: "patch" 덕분에 @org/tokens가 patch bump 됐을 때 @org/react도 자동으로 patch 올라가고, 그 사실이 CHANGELOG에 기록된다.
Snapshot release — PR 미리보기 배포
PR이 머지되기 전에 임시 버전으로 npm에 올려 디자이너·사용자가 테스트할 수 있게 한다.
# PR 브랜치에서
pnpm changeset version --snapshot pr-142
pnpm changeset publish --tag pr-142 --no-git-tag생성되는 버전: @org/react@0.0.0-pr-142-20260519143022
사용자:
pnpm add @org/react@pr-142CI에서 자동화:
# .github/workflows/snapshot.yml
on:
pull_request:
types: [labeled]
jobs:
snapshot:
if: github.event.label.name == 'snapshot'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: pnpm install --frozen-lockfile
- run: pnpm turbo run build
- run: |
pnpm changeset version --snapshot pr-${{ github.event.pull_request.number }}
pnpm changeset publish --tag pr-${{ github.event.pull_request.number }} --no-git-tag
- uses: actions/github-script@v7
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: '📦 Snapshot published: `pnpm add @org/react@pr-${{ github.event.pull_request.number }}`'
})PR에 snapshot 라벨을 붙이면 자동 publish + PR 댓글로 install 명령 게시.
Prerelease mode — 메이저 변경 점진 출시
다음 메이저(2.0.0)를 준비할 때 next 채널로 배포한다.
pnpm changeset pre enter next
# 이제 모든 publish가 2.0.0-next.0, 2.0.0-next.1, ... 로 나감
# 평소처럼 작업
pnpm changeset
git commit ...
# main 머지 → 자동으로 2.0.0-next.X publish
# 모두 끝나면 종료
pnpm changeset pre exit
# 다음 publish는 2.0.0 정식사용자:
pnpm add @org/react@next # prerelease 채널
pnpm add @org/react@latest # 안정 채널 (기본)dist-tag 시스템이 두 채널을 분리해 안정 사용자가 prerelease를 받지 않게 한다.
What-if — 잘못 쓰면 어떻게 깨지는가
- 함정 1 — changeset 누락: PR이 changeset 없이 머지되어 변경 사항이 사라짐. 대응:
.github/workflows/ci.yml에 changeset 검사 추가.
- name: Check for changesets
run: |
if [ -z "$(ls -A .changeset/*.md 2>/dev/null | grep -v README)" ]; then
echo "No changeset found. Run 'pnpm changeset' or add 'no-changeset' label."
exit 1
fi또는 bot으로 자동 댓글 — changesets/bot GitHub App이 PR에 “changeset이 없습니다”를 댓글로 단다.
-
함정 2 —
linked잘못 설정:react와tokens를 linked로 묶었더니,tokens패치마다react도 패치 올라감 → 사용자 update 부담. 대응:linked는 같이 메이저 변화하는 패키지에만. 의존 관계는updateInternalDependencies가 처리. -
함정 3 — provenance 미설정: npm 7+에서 publish 출처 증명(sigstore)이 표준이 되었는데,
NPM_CONFIG_PROVENANCE: true를 빠뜨려 사용자에게 “unverified publisher” 경고. 대응: GitHub Actions에서id-token: write권한 + 환경변수. -
함정 4 — Version PR 머지 후 publish 실패:
NPM_TOKEN만료/권한 부족. CHANGELOG와 package.json은 올라갔는데 npm에는 없는 유령 버전. 대응: 토큰 만료 알림 + 실패 시 Slack alert. Version PR을 revert하지 말고 patch bump로 재배포 (revert 시 사용자가 이미 본 버전을 못 받음). -
함정 5 — snapshot이 latest tag로 publish됨:
--tag옵션 빠뜨림 →latest채널 오염. 대응: snapshot 명령은 항상--tag pr-XXX. CI에서 강제. -
함정 6 — 의존 그래프 무한 bump: A→B→C 의존 시 A의 patch가 B의 patch, B가 C의 patch까지 cascade. 한 번의 변경이 모든 패키지에 영향. 대응: 진짜 런타임 의존만 dependencies로. 빌드 시점만 쓰는 것은 devDependencies로.
Insight — semantic-release를 안 쓰는 이유
| 항목 | semantic-release | Changesets |
|---|---|---|
| 입력 | 커밋 메시지 (Conventional Commits) | .changeset/*.md |
| 모노레포 | semantic-release-monorepo로 부분 지원 | 1급 지원 |
| Release notes | 커밋 메시지에서 추출 | markdown에서 추출 |
| 의도 표현 | ”fix: …” “feat: …” 한 줄 | 자유 형식 markdown (여러 줄) |
| PR 단위 통합 | 어려움 | 자연스러움 |
| 학습 곡선 | 가파름 (Conventional Commits 강제) | 평탄 |
개발자가 왜 이 변경이 minor인지 설명할 수 있는 공간이 Changesets에는 있고, semantic-release에는 없다. 디자인 시스템처럼 의도 설명이 중요한 도메인에서 이 차이가 컸다.
반전: 2022년 Vercel·Atlassian·Shopify·Adobe Spectrum이 동시에 Changesets로 이전했다. Spectrum 팀의 이전 후기에 따르면, “semantic-release 시절엔 commit message 컨벤션 위반으로 release가 깨지는 일이 월 5~6회였는데, Changesets로 옮긴 후 0회”.
요약
- Changesets는
(1) changeset markdown → (2) Version PR → (3) publish의 3-스텝 자동화. - 모노레포에서 영향받은 패키지만 SemVer를 올린다 (의존 관계 따라 cascade).
- Snapshot release는 PR 미리보기 (
--snapshot pr-142), Prerelease는 메이저 채널 분리 (pre enter next). - 핵심 강제 셋: changeset 검사, provenance, 의존 그래프 폭주 방지.