모노레포 배포 워크플로우 개선기 TypeScript 마이그레이션 실패에서 배운 교훈
TypeScript 마이그레이션 실패를 통해 배운 모노레포 배포 워크플로우 개선 사례: Beta/Stable 2단계 배포와 Changesets 도입으로 안전한 배포 시스템 구축기
시작: 평범한 TypeScript 마이그레이션이 대참사로
vue3-pivottable 프로젝트를 TypeScript로 마이그레이션하던 날이었습니다. “메인 패키지만 먼저 배포하고 하위 패키지는 나중에 수정하지 뭐” 라고 생각했던 것이 화근이었습니다.
첫 번째 실수: 빌드 실패를 무시하다
✓ vue-pivottable 빌드 성공
✗ plotly-renderer 빌드 실패
✗ lazy-table-renderer 빌드 실패
"아 하위 패키지는 나중에 고치면 되겠지..."
당시 워크플로우 (실제 파일 분석)
관련 워크플로우:
- create-release-pr.yml (main → release 동기화)
- release-vue-pivottable.yml (메인 패키지)
- release-plotly-renderer.yml (하위 패키지)
- release-lazy-table-renderer.yml (하위 패키지)
성공하는 경우
sequenceDiagram
participant F as feature
participant D as develop
participant M as main
participant MTR as main-to-release
participant R as release
participant RTM as release-to-main
participant NPM as npm
F->>D: PR & Merge
D->>M: PR
M->>M: 승인 (1차) ✓
M->>MTR: create-release-pr.yml 실행
MTR->>R: PR 자동 생성
R->>R: PR 승인 (2차) ✓
R->>R: 각 패키지별 워크플로우 실행
R->>NPM: Semantic Release 배포 ✓
R->>RTM: release-to-main PR 생성
RTM->>M: PR
M->>M: PR 승인 (3차) ✓
Note over F,NPM: 총 3회 승인 필요
실패한 경우 (TypeScript 마이그레이션)
sequenceDiagram
participant F as feature/*
participant D as develop
participant M as main
participant R as release
participant NPM as npm
F->>D: PR & Merge
D->>M: PR
M->>M: 승인 1 ✓
M-->>R: create-release-pr.yml<br/>(main-to-release PR 생성)
R->>R: 승인 2 ✓
R->>R: Semantic Release 실행
R->>NPM: 배포 시도 ❌
Note over NPM: 빌드 실패로 배포 안됨
main 브랜치에서 release 브랜치로 동기화 후 배포가 실행되는데, 빌드가 실패해서 배포가 안 되었습니다.
두 번째 실수: 리버트의 함정
“그럼 리버트하면 되겠네?”
PR에서 리버트를 실행했더니 main이 업데이트되고 다시 배포가 시작되었습니다. 그런데…
배포 성공!
그런데... lazy-table-renderer 버전이 1.0.x → 1.1로?
왜 이 패키지만 버전이 올라갔지? 🤔
실제 상황: TypeScript 마이그레이션 PR에 lazy-table-renderer 변경사항이 포함되어 있었고, Semantic Release가 이를 감지하여 버전을 올렸습니다.
GitHub App으로 보호 규칙 우회 시도 (PR #178-189)
왜 GitHub App?
“보호된 main 브랜치에 직접 푸시할 수 있다면 얼마나 편할까?”
이 생각으로 12개의 PR을 만들어 GitHub App 토큰으로 보호 규칙을 우회하려고 시도했습니다.
시도 과정 (30분 간격으로 연속 시도)
# PR #178: APP_INSTALLATION_ID 테스트
# PR #179: APP_ID 1254720으로 수정
# PR #180: 시크릿 길이 디버그
# PR #181: 공식 action으로 변경
# PR #182: PRIVATE_KEY 형식 수정
# PR #183-188: Private Key 형식 검증
# PR #189: APP_ID 재확인
결론: GitHub의 보안 철학
GitHub App 토큰도 브랜치 보호 규칙을 우회할 수 없습니다!
이것은 의도적인 설계입니다:
- Personal Access Token ❌
- GitHub App Token ❌
- OAuth Token ❌
- GITHUB_TOKEN ❌
모든 종류의 토큰이 브랜치 보호 규칙을 준수해야 합니다.
세 번째 실수: 강제 푸시의 나비효과
이제 완전히 꼬이기 시작했습니다. release→main PR이 미승인 상태로 남아있어서 강제 푸시가 더 큰 참사를 일으켰습니다:
sequenceDiagram
participant M as main
participant R as release
participant RTM as release-to-main PR
participant NPM as npm
Note over M,R: 초기 상태
M->>M: JS 버전 (리버트됨)
R->>R: TS 버전 (실패)
RTM->>RTM: release→main PR 미승인
Note over M: 강제 푸시 시도
M->>M: 보호 규칙 해제
M->>M: git push --force<br/>(TS 커밋)
Note over M,R: 예상치 못한 결과
R->>R: 여전히 JS 버전 존재
R->>NPM: JS 버전 재배포 😱
Note over M: TS로 강제 푸시했는데<br/>왜 JS가 배포되지?
“에라 모르겠다. 보호 규칙 끄고 강제 푸시!”
git push --force origin <ts-commit>:main
결과: release→main PR이 미승인이라 release 브랜치의 JS 버전이 다시 배포되었습니다. 😱
완전히 꼬인 상황
graph TB
subgraph "1. 초기 상태"
A1[main: TS 버전]
A2[release: TS 버전 빌드 실패]
end
subgraph "2. 리버트 후"
B1[main: JS 버전으로 리버트]
B2[release: 여전히 TS 버전]
B3[lazy-table-renderer 1.1 배포됨]
end
subgraph "3. 강제 푸시 후"
C1[main: TS 버전 강제 푸시]
C2[release: JS 버전 존재]
C3[release→main PR 미승인]
end
subgraph "4. 최종 결과"
D1[npm: JS 버전 재배포 😱]
D2[완전히 꼬인 Git 히스토리]
end
A1 -->|리버트| B1
A2 --> B2
B1 -->|강제 푸시| C1
B2 -->|충돌| C2
C2 -->|자동 배포| D1
C3 -->|미승인으로 인한 Git 히스토리 불일치| D2
classDef error fill:#ff9999
class A2,D1,D2 error
결국 GitHub Actions를 모두 삭제하고 처음부터 다시 시작했습니다.
개선된 워크플로우: 실패해도 괜찮은 환경 만들기
핵심 개선사항
1. Beta/Stable 2단계 배포
sequenceDiagram
participant F as feature
participant D as develop
participant M as main
participant Beta as npm beta
participant Stable as npm latest
participant R as release/vX.X.X
Note over F,D: 1. 개발 단계
F->>D: PR + Changeset
D->>D: Changeset 소비
D->>D: 버전 + beta suffix
D->>Beta: 자동 Beta 배포
D->>M: PR 생성/업데이트
Note over M,Stable: 2. Production 배포
M->>M: PR 승인
M->>R: 임시 브랜치 생성
R->>R: Beta suffix 제거
R->>Stable: 자동 Stable 배포
R->>M: 버전 동기화 PR
R->>D: 버전 동기화 PR
2. Changesets로 명시적 버전 관리
# 어떤 패키지를 변경했나요?
- vue-pivottable
# 어떤 종류의 변경인가요?
- minor
더 이상 커밋 메시지로 추측하지 않습니다.
3. 패키지 독립성 보장
{
"linked": [], // 연결된 패키지 없음
"fixed": [] // 고정 버전 없음
}
lazy-table-renderer가 멋대로 버전 업되는 일이 없어졌습니다.
4. release 브랜치 전략 변경
- 이전: 영구적인 release 브랜치 (꼬일 수 있음)
- 현재: 임시 release/vX.X.X 브랜치 (배포 후 삭제 가능)
5. 디버깅 및 분석 역량 개선
-
빈 태그 ‘v’ 오류
# 문제: id가 없어서 $이 비어있음 - name: Version packages as beta # id: version <- 이게 없었음!
-
베타 버전 중복 (1.1.1-beta.123-beta.456)
# 문제: 마지막 베타만 제거 sed 's/-beta\.[0-9]*$//' # 해결: 모든 베타 제거 sed 's/-beta\.[0-9]*//g'
-
빌드 순서 문제
# 문제: 하위 패키지가 메인 패키지 타입에 의존 pnpm -r build # 모든 패키지 동시 빌드 # 해결: 순차적 빌드 pnpm build # 메인 먼저 pnpm -r --filter './packages/*' build # 하위 패키지
-
실패 감지 문제
# 문제: 빌드 실패해도 계속 진행 pnpm build || true # 해결: 실패 시 즉시 중단 set -e pnpm build
6. 코드 품질 게이트 (컮밋 전 검사)
// .husky/pre-commit
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
pnpm lint-staged
- 커밋 전 자동 수정: ESLint + Prettier
- 빠른 피드백: 변경된 파일만 검사
- 일관된 코드 품질: 모든 개발자 동일 규칙
개선된 워크플로우 실증: PR #194, #195 사례
베타 배포 후 추가 업데이트가 필요한 상황이 실제로 발생했지만, 새로운 워크플로우로 안전하게 처리되었습니다:
sequenceDiagram
participant D as Developer
participant Dev as develop
participant Beta as npm beta
participant PR194 as PR #194 (main)
participant Stable as npm latest
Note over D,Dev: 1. 초기 개발
D->>Dev: 개발 완료 (PR #194)
Dev->>Beta: 베타 버전 1.1.2-beta.xxx 배포 ✓
Dev->>PR194: main으로 PR 자동 생성
Note over D,Dev: 2. 추가 개선사항 발견 & 적용
D->>Dev: 빌드 순서 및 오류 처리 개선 (PR #195)
Dev->>Beta: 새 베타 버전 배포 ✓
Dev->>PR194: PR #194 자동 업데이트! ⭐
Note over PR194,Stable: 3. 안전한 배포
PR194->>Stable: 승인 후 모든 수정사항 포함 배포 ✓
핵심 차이점
과거 (문제 발생):
- 추가 변경사항 → 수동으로 모든 것 다시 처리
- 복잡한 Git 수술 필요
- 어떤 변경사항이 포함되었는지 추적 어려움
현재 (안전한 해결):
- 추가 변경사항 → PR이 자동으로 최신 상태 유지
- 베타에서 모든 변경사항 검증 완료
- Stable 사용자는 모든 개선사항이 포함된 완성본만 받음
실제 효과
Before: 공포의 배포
- 배포 = 러시안 룰렛
- 실패 시 전체 사용자 영향
- 복잡한 Git 수술 필요
After: 안전한 배포
- Beta에서 먼저 테스트
- 실패해도 Stable 사용자 안전
- 명확한 롤백 경로
주요 교훈
-
실패를 격리하라
- Beta 단계는 “실패해도 괜찮은” 안전망
-
암묵적 → 명시적
- Semantic Release의 커밋 추측 → Changesets의 명시적 선언
-
복잡성을 줄여라
- 영구 release 브랜치 → 임시 브랜치
- 복잡한 다중 워크플로우 → 명확한 3개 워크플로우
-
자동화는 단순해야 한다
- 복잡한 자동화는 디버깅이 어렵다
최종 워크플로우: 안전한 배포 시스템
graph TB
subgraph "개발 단계"
F[feature 브랜치]
D[develop 브랜치]
Beta[npm beta 태그]
DPR[develop→main PR]
F -->|PR + Changeset| D
D -->|자동 베타 배포| Beta
D -->|PR 생성/업데이트| DPR
DPR -->|승인 필요| M
end
subgraph "프로덕션 배포"
M[main 브랜치]
R[release/vX.X.X 브랜치]
Stable[npm latest 태그]
MPR[main 동기화 PR]
M -->|배포 실행 + 임시 브랜치 생성| R
M -->|베타 제거 + stable 배포| Stable
R -->|PR 생성| MPR
MPR -->|승인 필요| M
R -->|자동 동기화| D
end
style Beta fill:#ffcc00
style Stable fill:#00cc00
style DPR fill:#ffcccc
style MPR fill:#ffcccc
핵심 개선사항 대비표
구분 | 이전 (문제 발생) | 현재 (개선됨) |
---|---|---|
배포 단계 | 단일 (release → npm) | 2단계 (develop→beta, main→stable) |
실패 영향 | 전체 사용자 | 베타 사용자만 |
버전 관리 | 커밋 메시지 추측 | Changesets 명시적 선언 |
브랜치 전략 | 영구 release | 임시 release/vX.X.X |
빌드 순서 | 동시 (의존성 문제) | 순차 (main → sub) |
품질 검사 | CI 후 발견 | Husky 전 차단 |
마무리
TypeScript 마이그레이션 실패는 뼈아픈 경험이었지만, 덕분에 더 나은 워크플로우를 만들 수 있었습니다.
현재 패키지들은 안정적으로 배포되고 있습니다:
- vue-pivottable (메인 패키지)
- @vue-pivottable/plotly-renderer
- @vue-pivottable/lazy-table-renderer
이제는:
- Beta 테스트:
npm install vue-pivottable@beta
- 문제 발견: Stable 사용자는 안전
- 수정 후: main 머지로 안전하게 배포
“실패해도 괜찮은” 환경을 만드는 것이 안정적인 배포의 핵심이었습니다.