pnpm Workspace + Changesets로 모노레포 릴리즈 자동화 — GitHub Actions CI 파이프라인 구성까지
모노레포를 처음 도입했을 때 가장 머리가 아팠던 게 뭔지 아세요? 빌드도 아니고, 테스트도 아니고, 바로 "이 패키지 버전을 올렸는데 저 패키지도 올려야 하나?" 하는 릴리즈 타이밍의 혼란이었어요. @myorg/ui를 0.5.0으로 올리면 @myorg/utils는? @myorg/app은? 팀원 각자가 package.json을 수동으로 고치다 보면 어느 순간 CHANGELOG는 형식도 제각각이고, "이 변경이 왜 들어갔지?"를 추적하는 게 git blame 탐정 놀이가 되어버려요.
이 글은 pnpm workspace를 이미 쓰고 있고, 릴리즈 자동화를 처음 고민하는 분을 위해 pnpm Workspace + Changesets 조합을 실전 관점에서 정리한 내용이에요. 이 글을 읽고 나면 changeset 파일 하나로 버전 bump, CHANGELOG 생성, npm 배포까지 자동화되는 GitHub Actions 파이프라인을 직접 구성할 수 있어요.
저도 Lerna를 쓰던 시절엔 설정 파일 하나 잘못 건드리면 반나절을 날리기 일쑤였는데, Changesets로 넘어온 뒤로는 그런 공포가 많이 줄었어요. 솔직히 "왜 더 일찍 안 바꿨지?"를 몇 번이나 생각했는지 모릅니다.
핵심 개념
pnpm Workspace — 내부 패키지를 하나로 묶는 기반
pnpm Workspace는 단일 저장소 안에서 여러 패키지를 함께 관리할 수 있게 해주는 pnpm 내장 기능이에요. 루트에 pnpm-workspace.yaml 파일 하나만 두면 패키지들을 하나의 워크스페이스로 연결할 수 있어요.
# pnpm-workspace.yaml
packages:
- 'packages/*'
- 'apps/*'내부 패키지 간 의존성을 선언할 때는 workspace:* 프로토콜을 사용해요. 나중에 npm에 publish할 때 이 workspace:*가 자동으로 실제 버전 번호(^1.2.0 같은 형태)로 치환돼요. 직접 버전을 맞춰줄 필요가 없는 거죠.
{
"name": "@myorg/app",
"dependencies": {
"@myorg/ui": "workspace:*",
"@myorg/utils": "workspace:*"
}
}workspace: 프로토콜 치환*:
pnpm changeset publish또는pnpm -r publish실행 시, pnpm이 자동으로 현재 패키지 버전 번호로 치환해요.workspace:^나workspace:~로 range를 좁힐 수도 있지만, 내부 패키지라면 보통*로 최신을 따라가는 게 편해요. 단,changeset publish보다pnpm -r publish가 치환을 더 안정적으로 처리하는 엣지 케이스가 있어서, 이슈가 생기면 publish 커맨드를 바꿔보는 게 좋아요.
Changesets — "변경 의도"와 "릴리즈"를 분리하는 철학
@changesets/cli의 핵심 철학은 하나예요. 변경 의도를 기록하는 시점과 실제 릴리즈 시점을 분리하자. 기능을 만드는 PR에서 "이 변경이 어떤 패키지에 어느 정도 영향을 주는지"를 changeset 파일로 함께 커밋하고, 릴리즈할 준비가 됐을 때 그 파일들을 일괄 소비해서 버전을 올리고 CHANGELOG를 만드는 방식이에요.
워크플로는 세 단계로 정리돼요:
| 단계 | 명령어 | 설명 |
|---|---|---|
| 변경 기록 | pnpm changeset |
.changeset/*.md 파일 생성 |
| 버전 반영 | pnpm changeset version |
changeset 파일 소비 → package.json 버전 bump + CHANGELOG 생성 |
| 배포 | pnpm changeset publish |
변경된 패키지를 npm에 게시 |
changeset 파일은 이런 형태예요:
---
"@myorg/ui": minor
"@myorg/utils": patch
---
Button 컴포넌트에 variant prop 추가, 유틸 함수 버그 수정프론트매터에 영향 받는 패키지와 semver 범위를 선언하고, 본문에는 사람이 읽을 변경 설명을 써요.
semver 기준: major는 breaking change, minor는 하위 호환 기능 추가, patch는 버그 수정이에요. Changesets는 이 판단을 PR 작성자에게 위임해요. 커밋 메시지를 자동 분석하는 semantic-release와의 가장 큰 차이점이기도 해요.
실무에서 자주 마주치는 상황인데, 복수의 changeset이 같은 패키지에 동시에 존재하는 경우 changeset version은 가장 높은 semver 단계로 합산해요. 예를 들어 @myorg/ui에 minor 한 건과 patch 한 건이 동시에 있으면 minor로 취합돼요. PR 여러 개가 동시에 쌓일 때 걱정하지 않아도 되는 이유예요.
changeset version을 실행하면 changeset 파일은 사라지고, 그 내용이 각 패키지의 CHANGELOG.md로 취합돼요. 결과물은 이런 모습이에요:
# @myorg/ui
## 0.6.0
### Minor Changes
- Button 컴포넌트에 variant prop 추가
## 0.5.1
### Patch Changes
- Updated dependencies개념을 이해했으니, 이제 실제로 적용해볼게요.
실전 적용
처음 셋업하기
새 모노레포이든 기존 pnpm workspace이든 Changesets 도입은 생각보다 간단해요. 루트에서 아래 두 명령어면 끝이에요.
# 루트에서 실행 (워크스페이스 루트에 devDependency로 설치)
pnpm add -Dw @changesets/cli
# 초기화
pnpm changeset init
# → .changeset/config.json 생성됨생성된 .changeset/config.json 전체 모습이에요:
{
"$schema": "https://unpkg.com/@changesets/config/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [],
"linked": [],
"access": "restricted",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": []
}주목할 옵션들을 짚어볼게요:
| 옵션 | 기본값 | 설명 |
|---|---|---|
access |
"restricted" |
npm 패키지 공개 여부. 스코프 패키지(@myorg/...)를 공개 npm으로 올리려면 "public"으로 변경 필수 |
updateInternalDependencies |
"patch" |
upstream 패키지 업데이트 시 downstream 패키지도 자동 patch bump (cascade bump) |
ignore |
[] |
버전 관리에서 제외할 패키지 목록 |
commit |
false |
changeset version 실행 시 자동 커밋 여부 |
cascade bump:
updateInternalDependencies: "patch"설정 시, 예를 들어@myorg/utils를 patch 업데이트하면@myorg/utils에 의존하는@myorg/ui도 자동으로 patch version이 올라가요. 내부 패키지 간 버전 불일치 문제를 자동으로 방지해주는 설정이에요.
access 옵션은 처음 셋업할 때 꼭 확인하면 좋아요. 저는 이걸 "restricted"에서 "public"으로 바꾸는 걸 깜빡해서 publish가 안 된다고 한 시간 동안 헤맸던 기억이 납니다. 기본값이 "restricted"라 공개 패키지를 올릴 때 자주 놓쳐요.
CHANGELOG에 PR 번호와 기여자 링크까지 자동으로 넣고 싶다면 @changesets/changelog-github를 추가로 설치할 수 있어요:
pnpm add -Dw @changesets/changelog-github그리고 config.json 전체를 이렇게 업데이트하면 돼요:
{
"$schema": "https://unpkg.com/@changesets/config/schema.json",
"changelog": ["@changesets/changelog-github", { "repo": "myorg/myrepo" }],
"commit": false,
"fixed": [],
"linked": [],
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": []
}개발 중 changeset 추가하기
기능 개발이 끝나고 PR을 올리기 전, 터미널에서 pnpm changeset을 실행하면 대화형 CLI가 떠요.
pnpm changeset어떤 패키지가 영향을 받는지 선택하고, major/minor/patch 중 어느 범위인지 고르고, 변경 내용을 한 줄 입력하면 .changeset/ 디렉터리에 랜덤한 이름의 마크다운 파일이 생성돼요. 이 파일을 코드와 함께 PR에 포함해서 커밋하면 돼요.
---
"@myorg/ui": minor
---
Add `size` prop to Button component for xs/sm/md/lg variantschangeset 파일을 빠뜨리는 PR이 생기면 자동화가 무너지는데, 이때 changesets-bot이 도움이 돼요. GitHub App으로 설치하면(github.com/apps/changeset-bot) PR에 changeset 파일이 없을 때 봇이 자동으로 코멘트로 알려줘요. 팀에 도입할 때 이 봇을 먼저 설치해두는 걸 추천해요.
CI에서 더 강하게 강제하고 싶다면 pnpm changeset status를 활용할 수도 있어요:
# PR CI 워크플로에 추가 — changeset 누락 시 CI 실패
- name: Check changesets
run: pnpm changeset status --since=origin/mainchangeset 파일이 하나도 없으면 이 명령어가 에러를 반환해서 PR이 통과되지 않아요.
GitHub Actions로 릴리즈 완전 자동화
이 부분이 가장 강력한 부분이에요. changesets/action을 사용하면 main 브랜치에 changeset 파일이 들어올 때마다 자동으로 "Version Packages" PR을 만들어주고, 그 PR을 머지하는 순간 npm 배포까지 자동으로 처리돼요.
# .github/workflows/release.yml
name: Release
on:
push:
branches: [main]
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # git 히스토리 전체가 필요 — 없으면 action이 오작동함
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 20
cache: pnpm
- run: pnpm install --frozen-lockfile
- uses: changesets/action@v1
with:
publish: pnpm changeset publish
version: pnpm changeset version
title: "chore: release packages"
commit: "chore: version packages"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}NPM_TOKEN은 npmjs.com 계정의 Access Tokens 페이지에서 발급한 후, GitHub 저장소의 Settings → Secrets and variables → Actions에 NPM_TOKEN 이름으로 등록해두면 돼요.
이 워크플로가 실제로 하는 일을 정리하면:
| 상황 | 액션 동작 |
|---|---|
main에 .changeset/*.md 파일이 있을 때 |
"Version Packages" PR 자동 생성 (버전 bump + CHANGELOG 미리보기 포함) |
| "Version Packages" PR이 머지됐을 때 | pnpm changeset publish 실행 → npm 자동 배포 |
팀원은 PR에 changeset 파일만 포함해서 올리면 돼요. 릴리즈 타이밍은 "Version Packages" PR을 머지하는 것으로 결정하면 되니, 릴리즈 매니저 역할이 굉장히 단순해지는 거죠.
알파/베타 프리릴리즈 운영
새 메이저 버전을 안정화하기 전에 알파 채널로 먼저 내보내고 싶을 때 사용하는 패턴이에요.
# 알파 채널 진입 (pre.json이 생성됨)
pnpm changeset pre enter alpha
# 이 상태에서 변경 사항을 changeset으로 기록
pnpm changeset
# 버전 반영 (1.0.0-alpha.0 형태로 bump)
pnpm changeset version
# 충분히 검증됐으면 채널 탈출
pnpm changeset pre exit
# 정식 버전으로 전환
pnpm changeset version프리릴리즈 상태는 .changeset/pre.json에 저장되며, 이 파일을 Git에 커밋해두면 팀 전체가 동일한 상태를 공유할 수 있어요. beta, rc 등 다른 채널명도 자유롭게 사용 가능해요.
장단점 분석
장점
| 항목 | 내용 |
|---|---|
| PR 단위 변경 문서화 | 기능 개발 시점에 변경 이유를 기록하므로, 릴리즈 직전에 기억을 더듬을 필요가 없어요 |
| 독립 버전 관리 | 패키지별로 독립적인 semver를 유지해요. @myorg/ui의 major 업데이트가 @myorg/utils에 영향을 주지 않아요 |
| 내부 의존성 자동 cascade bump | upstream 패키지 업데이트 시 downstream도 자동 patch bump돼서 버전 불일치를 방지해요 |
| 도구 중립성 | Turborepo, Nx, 단순 pnpm workspace 어느 환경과도 조합 가능해요. 빌드 시스템에 종속되지 않아요 |
단점 및 주의사항
| 항목 | 내용 | 대응 방안 |
|---|---|---|
| 팀 습관 의존 | PR마다 changeset을 빠뜨리면 자동화가 무너져요 | changesets-bot GitHub App 설치 + CI에서 changeset status로 강제 확인 |
| workspace:* 치환 이슈 | changeset publish가 workspace: 프로토콜을 처리 못하는 엣지 케이스가 있어요 |
pnpm -r publish 사용 또는 config에 "bumpVersionsWithWorkspaceProtocolOnly": true 설정 |
| 프리릴리즈 복잡성 | pre.json 상태 파일을 Git으로 관리해야 하며, 채널 전환 중 충돌 위험이 있어요 |
팀 내 프리릴리즈 절차를 문서로 명확히 정의 |
| 의도치 않은 버전 bump | 프리릴리즈 모드에서 변경 없는 패키지도 bump될 수 있어요 | ignore 옵션으로 특정 패키지 제외 |
실무에서 가장 흔한 실수
-
changeset 없이 PR을 올리는 습관 — 처음 도입할 때 이 흐름을 팀 전체가 이해하지 못하면 빠뜨리는 PR이 생겨요. changesets-bot을 먼저 설치해두면 봇이 코멘트로 친절하게 알려줘서 습관 형성에 도움이 돼요.
-
access: "restricted"그대로 두기 — 스코프 패키지(@myorg/...)를 공개 npm에 배포하려면"public"으로 변경이 필요해요. 기본값이"restricted"라서 초기 셋업 시 가장 먼저 확인하는 게 좋아요. -
프리릴리즈 채널에서
pre exit없이 정식 버전 배포 시도 —pre.json이 남아있는 상태에서 일반changeset version을 실행하면 버전이 예상과 다르게 나와요. 프리릴리즈 운영 중에는pre exit후 정식 버전으로 전환하는 절차를 팀 모두가 알고 있으면 좋아요.
마치며
pnpm Workspace + Changesets 조합은 "릴리즈를 이벤트가 아닌 프로세스로 만드는" 도구예요. 셋업 이후에는 팀원이 changeset 파일 하나만 PR에 포함하면 되고, 릴리즈는 "Version Packages" PR 머지 하나로 단순해져요.
지금 바로 시작해볼 수 있는 3단계:
-
pnpm add -Dw @changesets/cli && pnpm changeset init실행 — 5분이면.changeset/config.json이 생겨요.access옵션만 팀 상황에 맞게 조정해두면 기본 셋업 완료예요. -
다음 PR부터
pnpm changeset으로 changeset 파일 함께 포함 — 처음엔 낯설 수 있지만 두세 번 하다 보면 자연스럽게 손에 익어요. 혼자 먼저 써보면서 흐름을 익히는 것도 좋은 방법이에요. -
위의 GitHub Actions 워크플로를
.github/workflows/release.yml에 추가하고NPM_TOKEN시크릿 등록 — 이후 changeset 파일이 main에 머지될 때마다 "Version Packages" PR이 자동으로 올라와요.
막히는 부분이 생기면 Changesets GitHub Discussions에서 커뮤니티 도움을 받을 수 있어요. 생각보다 활발하게 운영돼서 비슷한 케이스의 논의가 이미 올라와 있는 경우가 많아요.
다음 글: Changesets로 릴리즈를 자동화했다면, 이제 빌드 시간을 줄일 차례예요 — Turborepo의 원격 캐싱과 태스크 파이프라인으로 모노레포 CI 빌드 시간을 단축하는 실전 가이드를 다룰 예정이에요.
참고 자료
핵심 참고
- Using Changesets with pnpm | pnpm 공식 문서
- changesets/changesets | GitHub
- Changesets 공식 문서
- changesets/action — GitHub Actions 공식 액션 | GitHub
설정 & 심화 읽기
- config-file-options.md — 설정 파일 옵션 레퍼런스 | GitHub
- prereleases.md — 프리릴리즈 가이드 | GitHub
- Changesets for Versioning | Vercel Academy
- Guide to version management with changesets | LogRocket Blog
실전 사례
- Complete Monorepo Guide: pnpm + Workspace + Changesets (2025) | jsdev.space
- Monorepo Architecture with pnpm Workspace, Turborepo & Changesets | DEV Community
- Why we stopped using Lerna for monorepos | DEV Community
트러블슈팅