달리는 차의 엔진을 바꾸다 — Nx 모노레포에서 Bun 도입까지의 여정
고속도로를 달리는 중에 엔진을 교체해야 한다면? 차를 세울 수 없고, 승객(사용자)은 속도 변화를 눈치채면 안 된다. 차체, 시트, 핸들은 그대로인데 엔진만 바뀐다. Nx 모노레포에서 패키지 매니저를 Bun으로 교체하는 작업이 정확히 이런 성격이다. 프로젝트 구조와 코드는 건드리지 않으면서, 의존성 설치와 스크립트 실행을 담당하는 엔진만 교체하는 것이다.
컬리(Kurly) 기술팀은 Nx 18에서 21까지 메이저 버전 3단계를 올리면서 동시에 Bun을 패키지 매니저로 도입했다. 그 과정에서 마주친 실전 이슈와 해결 전략을 정리한다.
Nx — 모노레포의 관리인
[💡 잠깐! 이 용어는?] Nx: Nrwl(현 Nx)이 만든 모노레포 빌드 시스템이다. 여러 프로젝트(앱, 라이브러리)를 하나의 저장소에서 관리하면서, 캐싱, 병렬 실행, 의존성 그래프 기반 태스크 오케스트레이션을 제공한다.
모노레포를 대형 마트에 비유하면 이해가 빠르다. 식품, 전자기기, 의류 매장이 한 건물 안에 있다. 각 매장(프로젝트)은 독립적으로 운영되지만, 주차장(빌드 도구), 물류센터(공통 라이브러리), 에스컬레이터(의존성 관리)는 공유한다. Nx는 이 대형 마트의 총괄 매니저다. 어떤 매장에 변경이 생겼는지 파악하고, 영향받는 매장만 골라서 업데이트하고, 이전 작업 결과를 캐싱해서 불필요한 반복을 건너뛴다.
| 기능 | 설명 | 비유 |
|---|---|---|
| 캐싱 | 이전 빌드/테스트 결과 저장 후 재사용 | 시험 답안지를 복사해두고, 같은 문제엔 다시 풀지 않는 것 |
| 태스크 오케스트레이션 | 의존 관계에 따라 빌드 순서 자동 결정 | 요리 순서 — 밥부터 안치면 반찬이 먼저 식는다 |
| 영향 분석 | 변경된 파일이 어떤 프로젝트에 영향을 주는지 계산 | 배관이 터졌을 때 어떤 방에 물이 새는지 파악하는 것 |
왜 Bun인가
Bun은 Jarred Sumner가 만든 올인원 JavaScript 런타임이다. 패키지 매니저, 번들러, 테스트 러너까지 내장하고 있다. 컬리가 Bun을 택한 이유는 명확하다.
설치 속도의 혁명
Bun의 패키지 설치 속도는 npm 대비 최대 25배, yarn 대비 최대 5배 빠르다. 모노레포에서 수백 개의 패키지를 관리하면 npm install 한 번에 3~5분이 걸리는 게 일상이다. Bun은 이것을 30초 이내로 줄인다. CI/CD 파이프라인에서 매일 수십 번 돌아가는 설치 시간이 이렇게 단축되면, 연간 수백 시간의 빌드 시간을 절약할 수 있다.
CI 파이프라인 전체 가속
설치 속도 개선은 CI 전체에 파급된다. 컬리의 경우 모노레포의 PR당 CI 실행 시간이 평균 12분에서 7~8분으로 줄었다. 하루에 수십 개의 PR이 올라오는 팀에서 PR당 4분 절약은 체감이 크다.
네이티브 번들러
Bun은 자체 번들러를 내장하고 있다. 별도의 webpack이나 esbuild 없이 빌드가 가능하다. 다만 Nx 환경에서는 기존 빌드 파이프라인과의 호환성 이슈가 있어, 번들러 전환은 별도 단계로 진행해야 한다.
엔진 마운트가 안 맞는 문제
컬리가 마이그레이션을 시작한 시점(Nx 18)에서 Bun은 Nx의 공식 패키지 매니저로 지원되지 않았다.
Nx는 패키지 매니저를 자동 감지해서 lock 파일 생성, 의존성 해석, 워크스페이스 구조를 처리한다. npm은 package-lock.json, yarn은 yarn.lock, pnpm은 pnpm-lock.yaml을 사용하는데, Bun의 bun.lockb(바이너리 형식)를 Nx 18은 인식하지 못했다.
새 엔진은 가져왔는데 차체의 엔진 마운트 규격이 안 맞는 상황이다. 엔진 자체는 훌륭하지만, 차에 고정할 수 없다. 해결책은 하나, Nx 버전을 올려서 Bun 지원을 확보하는 것이었다.
[💡 잠깐! 이 용어는?]
Lock 파일: 프로젝트의 모든 의존성이 정확히 어떤 버전으로 설치되었는지 기록하는 파일이다. 같은 package.json이라도 lock 파일이 없으면 설치 시점에 따라 다른 버전이 깔릴 수 있다.
한 계단씩 — Nx 18 → 19 → 20 → 21
한 번에 18에서 21로 뛰어넘는 건 위험하다. 메이저 버전마다 breaking change가 있고, 한꺼번에 적용하면 어떤 버전이 문제를 일으켰는지 추적할 수 없다. 컬리는 한 버전씩 순차적으로 올리는 전략을 택했다.
Nx 18 → 19: 추론 기반 플러그인 시스템
Nx 19의 가장 큰 변화는 **자동 플러그인 추론(inferred plugins)**이 기본값으로 바뀐 것이다. 기존에는 project.json에 빌드/린트 타겟을 명시적으로 선언했지만, Nx 19부터는 프로젝트 구조를 보고 자동으로 추론한다.
{
"targets": {
"build": {
"executor": "@nx/webpack:webpack",
"options": {
"outputPath": "dist/apps/web"
}
},
"test": {
"executor": "@nx/jest:jest",
"options": {
"jestConfig": "apps/web/jest.config.ts"
}
}
}
}{
"targets": {}
}project.json이 간결해지지만, 커스텀 executor를 쓰던 프로젝트는 추론 로직과 충돌할 수 있다. 컬리는 이 단계에서 약 40개의 project.json을 수정해야 했다.
Nx 19 → 20: Executor에서 Plugin으로
일부 executor가 deprecated되고 플러그인 기반으로 전환되었다. 특히 @nx/webpack:webpack executor가 @nx/webpack 플러그인으로 바뀌면서, 빌드 설정을 webpack.config.ts로 이관하는 작업이 필요했다.
Nx 20 → 21: Bun 공식 지원
드디어 Bun이 공식 패키지 매니저로 지원된다. nx.json에 패키지 매니저를 명시하고, Bun의 workspace 프로토콜을 사용할 수 있게 되었다.
{
"packageManager": "bun",
"workspaceLayout": {
"appsDir": "apps",
"libsDir": "libs"
}
}실전 이슈와 대응
Lock 파일 전환 시 버전 불일치
npm의 package-lock.json에서 Bun의 bun.lockb로 전환하면 의존성 해석 결과가 미묘하게 달라질 수 있다. 같은 package.json이라도 lock 파일이 다르면 설치 버전이 달라지기 때문이다. 컬리는 전환 시점에 **모든 의존성 버전을 정확히 고정(pinning)**하는 방법으로 해결했다.
{
"dependencies": {
"react": "19.0.0",
"next": "15.1.0",
"typescript": "5.7.2"
}
}^19.0.0(호환 범위)이 아니라 19.0.0(정확한 버전)으로 고정한 것이다. "아무 19번 버스나 타라"가 아니라 "오후 3시 15분에 출발하는 그 버스를 타라"고 지정한 것과 같다.
플러그인 Breaking Changes
Nx 공식 플러그인(@nx/react, @nx/next, @nx/jest 등)이 메이저 버전마다 API를 바꾸었다. 특히 jest 설정 파일의 경로 해석 방식 변경으로 기존 테스트가 깨지는 경우가 발생했다. nx migrate 커맨드로 대부분 자동 수정되지만, 커스텀 설정이 많은 프로젝트에서는 수동 개입이 필요했다.
# Nx 마이그레이션 실행
npx nx migrate @nx/workspace@20
# 마이그레이션 스크립트 확인
cat migrations.json
# 마이그레이션 적용
npx nx migrate --run-migrationsExecutor 이동
@nx/node:node executor가 @nx/js:node로 이동하면서, Node.js 애플리케이션의 실행 설정을 일괄 변경해야 했다. 모노레포에 Node.js 서비스가 12개 있었기 때문에, 하나씩 수동으로 바꾸는 대신 코드모드(codemod) 스크립트를 작성해 일괄 처리했다.
[💡 잠깐! 이 용어는?] 코드모드(Codemod): 소스 코드를 자동으로 변환하는 스크립트다. AST(추상 구문 트리)를 파싱해서 특정 패턴을 찾고, 새로운 패턴으로 대체한다. 대규모 리팩토링이나 마이그레이션에서 수동 작업을 대폭 줄여준다.
마이그레이션 체크리스트
사전_준비:
- 현재 Nx 버전과 목표 버전 사이의 changelog를 모두 읽는다
- 각 메이저 버전의 breaking changes 목록을 별도 문서에 정리한다
- 의존성 버전을 모두 고정(pin)한다
- CI 파이프라인에서 현재 빌드 시간을 기록한다 (비교 기준)
단계별_실행:
- 한 번에 하나의 메이저 버전만 올린다
- 각 단계마다 nx migrate를 실행하고, 자동 마이그레이션 결과를 검토한다
- 각 단계마다 전체 빌드 + 테스트를 돌려 regression을 확인한다
- 문제가 발생하면 해당 단계에서 해결하고 넘어간다
bun_전환:
- Nx가 Bun을 공식 지원하는 버전까지 올린 후에 전환한다
- package-lock.json을 삭제하고 bun install을 실행한다
- bun.lockb를 git에 커밋한다 (바이너리 파일)
- CI의 setup-node를 setup-bun으로 교체한다
- postinstall 스크립트가 Bun에서도 동작하는지 확인한다
검증:
- 전체 빌드가 통과하는지 확인한다
- 전체 테스트가 통과하는지 확인한다
- CI 빌드 시간이 개선되었는지 측정한다
- 개발자 로컬 환경에서 설치 시간이 개선되었는지 확인한다숫자로 보는 결과
| 지표 | 마이그레이션 전 (Nx 18 + npm) | 마이그레이션 후 (Nx 21 + Bun) |
|---|---|---|
install 시간 (CI) | 약 3분 20초 | 약 28초 |
| PR CI 전체 시간 | 약 12분 | 약 7분 30초 |
로컬 install 시간 | 약 2분 | 약 15초 |
| 캐시 적중 시 빌드 | 45초 | 38초 |
가장 체감이 큰 부분은 로컬 설치 시간이다. 브랜치 전환이나 새 클론 시마다 2분 기다리던 것이 15초로 줄어든 건 개발자 경험(DX) 측면에서 의미가 크다.
마무리
Nx 모노레포에서 Bun으로의 전환은 "달리는 차의 엔진 교체"다. 차체(프로젝트 구조)는 건드리지 않으면서 엔진(패키지 매니저)만 바꾸는 작업이다. 단, 엔진 마운트 규격(Nx 버전)이 맞아야 하므로 Nx 버전 업그레이드가 선행 조건이다. 한 번에 뛰어넘지 말고 한 메이저 버전씩 올리면서 각 단계의 breaking change를 소화하는 것이 안전한 경로다. 엔진 교체가 끝나면, 설치 속도와 CI 시간에서 즉각적인 개선을 체감할 수 있다.
관심 있을 만한 포스트
Babel 7.29.0 — 10년 역사의 마지막 마이너, 그리고 8 RC1
2026년 1월 31일, Babel 7의 마지막 마이너 릴리스가 공개됐다. 이 버전이 갖는 역사적 의미와 Babel 8 RC1의 핵심 변화를 정리한다.
Bun이 빠른 건 맞다 — 그런데 당신의 이벤트 루프가 문제다
Bun으로 바꿔도 p99가 개선되지 않는 이유. 런타임 선택보다 먼저 봐야 할 진짜 병목 지점들.
jQuery 4.0 — 10년 만의 메이저 릴리스, 무엇이 바뀌었나
jQuery가 20주년을 맞아 10년 만에 메이저 버전을 출시했다. IE 지원 축소, ES 모듈 전환, Trusted Types 등 핵심 변경 사항을 정리한다.
Babel 8 Beta — CJS를 버리고 ESM 전용으로 간다
2년간의 알파를 거쳐 베타에 진입한 Babel 8의 핵심 변경사항과 마이그레이션 전략을 정리한다.
TypeScript 6.0 Beta — TS 7 가기 전에 tsconfig부터 정리하자
TypeScript 6.0 Beta의 주요 변경 사항과 깨지는 기본값들을 정리하고, TS 7(Go 네이티브) 전환을 대비하는 마이그레이션 전략을 다룬다.
Electrobun v1 — Bun으로 14MB짜리 데스크톱 앱을 만든다
Electron의 번들 크기 문제를 Bun 런타임과 네이티브 웹뷰로 해결하려는 새 프레임워크 Electrobun v1이 출시됐다.
Bun vs Node.js vs Deno — 뭐가 다른지, 그래서 뭘 쓰면 좋은지 (2026 기준)
런타임 3대장 비교: 호환성(Node), 속도/번들(Bun), 올인원/보안(Deno). 팀/프로덕트 상황별 선택 기준과 체크리스트까지 정리.
OOM이 터지고 나서야 깨달은 것들 — Webpack4에서 Vite로 갈아탄 5년 묵은 CMS
CI 빌드가 OOM으로 터진 뒤, 5년 동안 방치된 Webpack4 기반 CMS를 Vite로 전환하며 빌드 시간 48%, 번들 크기 81%를 줄인 과정.