들어가며
개발 도구를 바꾸는 작업은 package.json의 명령어 몇 개를 바꾸는 것으로 끝나지 않습니다.
특히 CI/CD가 연결된 웹 프로젝트에서는 application code, dependency lockfile, automation script, GitHub Actions, 실제 배포 runner가 모두 같은 runtime contract를 이해해야 합니다.
이번에는 실제 운영 중인 dasomel.github.io를 기존 Node.js + npm 중심 환경에서 Bun 1.4 중심 toolchain으로 전환했습니다.
최종 구조만 보면 단순합니다.
npm
↓
Bun 1.4하지만 실제 migration은 한 번에 끝나지 않았습니다. 첫 시도에서는 GitHub Pages 배포가 bun: not found로 실패했고, 이후 runtime provisioning을 보완했습니다. 그 다음 npm → bun install, package-lock.json → bun.lock, bun test, bun build까지 단계적으로 전환했습니다.
마지막에는 PR CI와 Pages Deploy가 같은 Next.js production build를 각각 수행하던 구조까지 정리했습니다.
이 글은 최종 성공 결과만 보여주는 글이 아니라 실패 → 원인 분석 → 수정 → 실증 → pipeline 최적화의 전체 과정을 기록한 engineering log입니다.
1. 전환 전 상태
기존 프로젝트는 Node.js + npm을 중심으로 구성되어 있었습니다.
대표적인 build script는 다음과 같았습니다.
{
"scripts": {
"build": "next build && node scripts/generate-rss.js"
}
}GitHub Actions도 npm을 사용했습니다.
- name: Setup Node
uses: actions/setup-node@v6
with:
node-version: 24.19.0
cache: npm
- name: Install dependencies
run: npm ci --prefer-offline --no-audit --no-fundRSS, Daily Digest, 행사 수집, 프로젝트 metadata 갱신 등의 automation script도 Node.js로 실행됐습니다.
2. 왜 Bun으로 전환했는가
목표는 단순히 package install을 빠르게 만드는 것이 아니었습니다.
repository에 여러 Node.js automation script가 존재하기 때문에 package manager, script runtime, test tooling을 하나의 toolchain으로 통일하는 것이 더 중요했습니다.
목표 구조는 다음과 같았습니다.
여기서 중요한 구분이 있습니다.
Next.js의 내부 compiler/Turbopack을 Bun으로 교체한 것은 아닙니다. Next.js는 그대로 사용하고, 그 위의 dependency/runtime/tooling layer를 Bun으로 통일했습니다.
3. 첫 번째 전환: Pages 배포가 실패했다
첫 단계에서는 automation script부터 Bun으로 바꿨습니다.
- "build": "next build && node scripts/generate-rss.js"
+ "build": "next build && bun scripts/generate-rss.js"또한 프로젝트가 Bun 1.4 이상을 요구하도록 선언했습니다.
"engines": {
"bun": ">=1.4.0"
}CI에는 Bun을 설치했지만, 이때 GitHub Pages deployment workflow에는 Bun 설치가 빠져 있었습니다.
결국 실제 배포에서 다음 흐름으로 실패했습니다.
핵심은 Next.js build 자체의 문제가 아니었습니다. 소스가 요구하는 runtime과 deploy runner의 runtime provisioning이 일치하지 않았던 것입니다.
이 문제는 a3388fa9에서 Pages workflow에 oven-sh/setup-bun@v2와 Bun 1.4 검증 단계를 추가하면서 해결했습니다.
Runtime migration은 source code만 바꾸는 작업이 아니라 실행 환경 전체를 바꾸는 작업이다.
4. 두 번째 단계: Bun-native toolchain으로 전환
Pages runtime 문제가 해결된 후 본격적으로 package manager까지 전환했습니다.
| 변경 항목 | 기존 | Bun 전환 후 |
|---|---|---|
| Dependency | package-lock.json | bun.lock |
| Install | npm ci | bun install --frozen-lockfile |
| Automation | node scripts/... | bun scripts/... |
| Test | 별도 Bun test 없음 | bun test |
| Build tooling | Node/npm 중심 | Bun-native smoke test 추가 |
이제 Bun은 단순히 설치된 runtime이 아니라 package manager + runtime + test runner + build tooling 역할을 담당합니다.
5. GitHub Actions에서 실제 속도를 측정했다
"Bun이 빠르다"는 가정만으로 migration을 평가하지 않고 GitHub Actions 실행 로그에서 실제 시간을 비교했습니다.
비교 환경은 같은 GitHub-hosted ubuntu-24.04 runner입니다.
| 지표 | 기존 | Bun 전환 후 | 변화 |
|---|---|---|---|
| Dependency install | 15.26s | 8.99s | 약 41% 단축 |
| Next.js compile | 10.3s | 9.8s | 거의 동일 |
| 초기 전체 CI | 49.5s | 38.5s | 약 22% 단축 |
이번 migration에서 가장 명확한 성능 개선은 dependency installation에서 확인됐습니다.
6. Next.js build는 거의 빨라지지 않았다
반대로 Next.js 자체 build는 큰 차이가 없었습니다.
이 결과는 자연스럽습니다. 이번 migration은 Next.js의 내부 build engine을 교체한 것이 아니라 package manager와 JavaScript automation runtime을 변경한 것이기 때문입니다.
따라서 "Bun으로 바꾸면 Next.js build가 2배 빨라진다"와 같은 결론은 이 프로젝트의 실측 결과와 맞지 않습니다.
7. 전체 migration 단계의 CI도 빨라졌다
초기 Bun-native migration 단계에서 전체 CI job도 비교했습니다.
Before ≈ 49.5s
After ≈ 38.5s약 11초, 22% 단축입니다.
중요한 점은 Bun migration 후에 bun test와 Bun bundle smoke test까지 추가됐다는 것입니다.
즉 검증 단계는 늘었는데 전체 CI 시간은 더 짧아졌습니다.
8. 그런데 한 번 더 pipeline을 개선할 수 있었다
migration이 끝난 뒤 CI 구조 자체를 다시 살펴봤습니다.
기존에는 같은 production build를 PR과 Deploy에서 각각 수행했습니다.
즉 같은 변경에 대해 production build를 두 번 수행하고 있었습니다.
9. PR CI는 빠르게, Production build는 Deploy에서 한 번
현재는 역할을 명확하게 분리했습니다.
| Pipeline | 책임 |
|---|---|
| PR CI | 머지해도 되는가? |
| Pages Deploy | production build와 배포가 가능한가? |
PR에서는 production build를 제거했고, 실제 production build는 main의 Pages deployment 경계에서 한 번만 수행합니다.
10. 최종 구조
이 구조에서 중요한 것은 Bun을 추가한 것이 아니라 repository 전체의 runtime contract를 명확하게 만든 것입니다.
11. 최종 실증 결과
| 항목 | 기존 | Bun 전환 후 | 결과 |
|---|---|---|---|
| Dependency install | 15.26s | 8.99s | 약 41% 단축 |
| Next.js compile | 약 10.3s | 약 9.8s | 큰 차이 없음 |
| 전체 초기 CI | 약 49.5s | 약 38.5s | 약 22% 단축 |
| Test | 별도 Bun test 없음 | bun test | 검증 추가 |
| Bundle smoke test | 없음 | bun build | 검증 추가 |
| PR production build | 실행 | 제거 | 중복 제거 |
| Main production build | 실행 | 실행 | 최종 배포 경계에서 유지 |
최종 해석
Bun으로 바꿨더니 Next.js가 갑자기 빨라진 것이 아니다. Dependency와 automation layer를 Bun으로 통합하면서 실제 CI가 약 22% 짧아졌고, 이후 PR/Deploy의 중복 production build도 제거해 pipeline 효율을 추가로 높였다.
12. Engineering lesson
Runtime migration은 application code migration과 다르다
package.json 변경
≠
전체 system migration 완료CI, Pages, automation workflow, cache, lockfile까지 함께 봐야 합니다.
실제 workflow에서 측정해야 한다
15.26s → 8.99s처럼 이 저장소의 실제 workflow에서 측정한 수치가 일반적인 벤치마크보다 중요했습니다.
PR과 Deploy는 같은 역할을 할 필요가 없다
PR은 빠른 품질 검증을 담당하고 production build는 실제 배포 경계에서 수행하도록 분리하는 것이 이 프로젝트에서는 더 효율적이었습니다.
실패 기록도 engineering evidence다
첫 번째 bun: not found 실패는 숨길 문제가 아니라 runtime provisioning 누락을 정확히 찾게 해준 관찰 지점이었습니다.
마무리
이번 Bun 전환은 처음부터 깔끔하게 끝난 작업이 아니었습니다.
첫 시도에서는 실제 배포가 실패했고, 그 실패를 통해 runtime provisioning이라는 누락된 조건을 확인했습니다. 이후 npm → bun install, package-lock.json → bun.lock, bun test, bun build까지 확대해 실제 Bun-native toolchain으로 전환했습니다.
마지막으로 PR CI와 Pages Deploy의 production build 중복도 제거했습니다.
Dependency install
npm ci 15.26s
bun install 8.99s
↓
약 41% 개선
전체 초기 migration CI
≈49.5s
↓
≈38.5s
↓
약 22% 개선반면 Next.js build 자체는 거의 동일했습니다.
이번 migration의 핵심은 단순한 속도 향상이 아니라 실패 → 원인 분리 → runtime 보완 → 실측 → pipeline 단순화의 순서였습니다.
12. 연속된 main 변경과 Deploy 취소도 배포 모델의 일부다
Bun 전환 이후 CI/CD를 정리하는 과정에서 main에 짧은 간격으로 여러 변경이 들어오면서 이전 GitHub Pages Deploy run이 cancelled되고 최신 revision의 Deploy가 이어지는 상황도 확인했습니다.
여기서 cancelled는 이전 코드가 삭제되었다는 뜻이 아닙니다. main이 선형적으로 갱신되는 저장소에서는 보통 최신 revision이 이전 변경을 포함하므로, 이전 revision의 배포 작업을 중단하고 최신 revision을 최종 배포 대상으로 선택하는 흐름으로 이해하는 것이 정확합니다.
따라서 Deploy run을 확인할 때는 cancelled만 보고 장애라고 판단하지 않고 다음을 함께 확인해야 합니다.
| 확인 항목 | 판단 기준 |
|---|---|
| 이전 Deploy | cancelled인지 |
| 최신 main commit | 더 새로운 revision이 존재하는지 |
| 최신 Deploy | build와 deploy가 성공했는지 |
이번 사례에서도 Deploy #364가 취소된 뒤 최신 commit 76e1a3e를 대상으로 Deploy #365가 실행됐고 production build는 정상 완료됐습니다.
즉 이 동작은 기존 결과를 무작정 덮어쓰는 것보다 최신 main revision으로 배포 상태를 수렴시키는 것에 가깝습니다. 독립적인 artifact를 배포하는 시스템은 concurrency 정책을 별도로 검토해야 합니다.