Nx에서 Bun 더 잘 사용하기: Nx 18 -> 21 마이그레이션

Nx가 Bun을 공식 지원하기 전에 도입했다면 마주칠 수 있는 문제들과 해결 방법

Nx에서 Bun을 더 잘 사용하기 - 썸네일 이미지

로고 출처: Nx © Nx | Bun © Anthropic

들어가며

이 글의 핵심: 공식 지원 시작이 곧 프로덕션 준비 완료를 의미하지는 않습니다. Nx 문서에는 19.5부터 Bun 공식 지원이라고 적혀 있지만, 실제로 안정적으로 사용하려면 21.4+가 필요했습니다. 이 글에서는 그 과정에서 마주친 문제들과 해결 방법을 공유합니다.

배경

핀테크그룹 프론트엔드에서는 모노레포 관리 도구로 Nx를 사용하고 있습니다. 2024년 3월에 패키지 매니저를 pnpm에서 Bun으로 변경했고, 이후 계속 사용 중입니다. 런타임은 Node.js를 그대로 유지하고 있어 Bun은 순수하게 패키지 매니저로 활용하고 있습니다.

최근 의존성 업그레이드와 빌드 최적화를 위해 다음과 같이 마이그레이션했습니다.

항목 Before After
Nx 18.3.3 21.4+
Bun 1.0.33 1.2.x
Lock file bun.lockb (바이너리) bun.lock (텍스트)

Nx 버전별 Bun 지원 범위

Nx 버전 Bun 지원
18.x 미지원
19.5+ 공식 지원
21.4+ bun.lock 파싱 개선

Nx 18 + Bun 조합은 당시로서는 꽤 도전적인 선택이었습니다. Nx가 Bun을 공식 지원하기 시작한 건 19.5부터였기 때문입니다. 그럼에도 Bun 공식 벤치마크에서 pnpm 대비 17배, npm 대비 29배, Yarn 대비 33배 빠른 의존성 설치 속도를 주장하고 있어 충분히 시도해볼 만한 가치가 있다고 판단했습니다. 벤치마크는 환경에 따라 달라질 수 있으며, 우리 프로젝트에서는 실제로 약 60% 빌드 시간 단축을 확인했습니다.

측정 항목 pnpm Bun 개선
의존성 설치 + 빌드 (nx build web) 1분 46초 42초 약 60% 단축

측정 환경: MacBook Pro (M2 Pro, 32GB RAM), Nx 및 node_modules 캐시 삭제 후 cold start 기준 10회 측정 평균값

도입 전 POC에서 큰 이상이 없었고, 리그레션 QA도 무난히 통과했습니다. 공식 지원이 아니라는 리스크보다 Bun의 빠른 설치 속도라는 이점이 훨씬 컸기에 프로덕션에도 적용했습니다.

다만 공식 미지원이라는 점은 여전히 리스크였습니다. Nx가 Bun을 공식 지원하는 버전이 나오면 마이그레이션해야겠다는 백로그를 늘 가지고 있었고, 이번에 드디어 진행하게 되었습니다.

마주친 문제들

문제 1: Nx가 Bun 대신 Yarn을 사용하려 함

문제 상황

우리 프로젝트는 패키지 매니저로 Bun을 사용하고 있었습니다. 로컬 머신에도 Yarn은 설치되어 있지 않았죠.

그런데 nx migrate를 실행하니 Nx가 Yarn으로 패키지를 설치하려고 시도했습니다. Yarn이 설치되어 있지 않으니 당연히 실패했고, yarn을 찾을 수 없다는 에러가 발생했습니다.

분명 Bun을 쓰고 있는데, 왜 Nx는 Yarn을 사용하려고 했을까요?

원인

Nx는 루트의 lock file 존재 여부로 패키지 매니저를 감지합니다.

문제는 두 가지였습니다:

  1. Nx 18.x는 Bun을 공식 지원하지 않음
    • 감지 우선순위: yarn.lockpnpm-lock.yamlpackage-lock.json (소스 코드)
    • bun.lockb는 감지 대상에 포함되지 않음
  2. 우리 프로젝트에 yarn.lock이 존재
    • Bun 도입 초기, 호환성 이슈에 대비해 yarn.lock도 함께 생성하도록 설정해둔 상태
# bunfig.toml
[install.lockfile]
# whether to save a non-Bun lockfile alongside bun.lockb
# only "yarn" is supported
print = "yarn"  # bun install 시 yarn.lock도 함께 생성 (호환성 이슈 대비)

실제 패키지 매니저는 Bun이었지만, yarn.lock 파일이 존재했기 때문에 Nx는 "이 프로젝트는 Yarn을 사용한다"고 판단한 것입니다.

해결 과정

Bun을 공식 지원하는 Nx 19.5 이상으로 올라가야 권장 방법에 가까운 마이그레이션이 가능합니다. 하지만 그러려면 일단 18 → 19를 넘어가야 했죠.

그래서 마이그레이션 당시에만 yarn.lock을 임시로 제거했습니다. 실제 의존성 트리는 bun.lockb가 관리하고 있었고, yarn.lock은 Bun 호환성 문제 발생 시를 대비한 fallback용으로만 유지하던 파일이었습니다. 따라서 제거해도 의존성 관리에는 영향이 없었습니다.

Nx가 감지할 lock file이 없으니 npm으로 fallback되어 마이그레이션을 정상 진행했고, 19.5+까지 올린 뒤에는 Bun 공식 지원을 받을 수 있게 되었습니다. Nx 19.5+에서는 bun.lock 또는 bun.lockb만 있으면 자동으로 Bun을 인식하기 때문에, 별도의 설정 없이도 정상 동작합니다.

문제 2: webpack 빌드 시 lock file 파싱 실패

문제 상황

Nx 19로 마이그레이션 후 webpack 기반 애플리케이션 빌드에서 다음 오류가 발생했습니다.

HookWebpackError: Cannot read properties of undefined (reading 'data')

재현 조건

  • Nx 19.x
  • Bun 1.0.33 (bun.lockb 사용)
  • @nx/webpack executor + generatePackageJson: true
// project.json
{
  "build": {
    "executor": "@nx/webpack:webpack",
    "options": {
      "generatePackageJson": true // 이 옵션이 문제 유발
    }
  }
}

원인

Nx는 빌드 산출물의 package.json을 생성하기 위해 프로젝트 그래프의 External Node 메타데이터를 읽습니다. External Node란 프로젝트 의존성 그래프에서 외부 npm 패키지를 나타내는 노드로, 버전 정보와 의존 관계를 담고 있습니다. 구 버전 Bun의 bun.lockb(바이너리 형식)에서 이 메타데이터 해석이 누락되는 경우가 있었습니다.

해결 과정

해당 문제를 빠르게 조치하기 위해 몇 가지 우회 방법을 검토했습니다:

  • generatePackageJson: false로 변경 → 배포 환경에서 별도 스크립트로 package.json 생성 필요
  • 커스텀 webpack 플러그인으로 메타데이터 주입 → 유지보수 부담 증가

우회는 가능했지만, 별도 스크립트 유지보수와 Nx 업그레이드 시 호환성 검증 부담을 고려하면 기술 부채 리스크가 더 컸습니다. 마침 팀에서 Storybook 9 마이그레이션도 검토하고 있었습니다. Storybook 9의 Nx 공식 지원 최소 버전이 Nx 21.2였고, Bun과 Nx를 함께 올리면 자연스럽게 해결되는 이슈였습니다. 여러 상황이 맞아떨어진 덕분에, 우회하기보다 마이그레이션하기로 결정했습니다.

결과적으로 해결 방법은 다음과 같습니다:

  1. Bun 업그레이드: 1.2.x로 올려 bun.lockbbun.lock 전환
  2. Nx 업그레이드: 21.4+로 업그레이드 (bun.lock 파싱 개선 포함)

참고: nrwl/nx#32656

bun.lockb → bun.lock 전환 배경

Bun 1.2.x로 업그레이드하면 lock file이 bun.lockb(바이너리)에서 bun.lock(텍스트, JSONC 형식)으로 바뀝니다. 기존 바이너리 형식은 속도를 위한 선택이었지만 여러 문제가 있었습니다:

문제 설명
PR 리뷰 불가 바이너리라 diff를 볼 수 없음
Merge 충돌 수동 해결이 거의 불가능
도구 호환성 Nx, Dependabot 등이 파싱 불가

텍스트 형식으로 전환되면서 외부 도구들이 lock file을 정상적으로 파싱할 수 있게 되었고, Nx의 External Node 메타데이터 해석 문제도 해결됩니다.

흥미롭게도 텍스트 형식 전환 이후에도 내부 최적화(Structure of Arrays, Streaming Parsing 등) 덕분에 약 30% 성능을 개선했다고 합니다. 자세한 내용은 Bun 블로그를 참고하세요.

결과

마이그레이션 완료 후 측정한 수치입니다.

항목 Before After 개선
CI 파이프라인 13분 30초 11분 40초 -14%
Artifact 크기 330MB 280MB -15%

측정 환경: GitLab Runner, 5회 실행 평균. 수치는 10 단위에서 반올림.

정량적 개선 외에도 다음과 같은 변화가 있었습니다:

  • 기존 bun.lockb + yarn.lock 이중 관리에서 bun.lock 단일 파일로 통합
  • bun.lock이 텍스트 형식으로 바뀌면서 Dependabot PR의 diff 확인 가능
  • Bun 공식 지원으로 향후 Nx 업그레이드 시 호환성 걱정 감소
  • Storybook 9 마이그레이션을 위한 Nx 버전 요구사항 충족

배운 점

막히는 지점에서는 오픈소스 코드를 직접 확인하는 게 답일 때가 있었습니다. Nx의 패키지 매니저 감지 로직을 직접 보고 나서야 왜 bun.lockb가 있는데도 Yarn으로 fallback하는가를 이해할 수 있었습니다. 예전이라면 코드를 찾아보거나 검색하는 데 시간이 오래 걸리고 진입 장벽도 있었겠지만 AI를 활용해서 실제 소스 코드와 issue tracker 등을 더욱 빠르게 탐색할 수 있었습니다.

메이저 업그레이드는 미루지 않는 게 낫습니다. 안정화 시점이 확인되면 백로그에 넣어 주기적으로 진행하면, 여러 이슈가 한꺼번에 터지는 것을 막을 수 있습니다. 이번 마이그레이션 과정은 쉽지 않았지만, 빌드 시간 60% 단축, CI 파이프라인 14% 개선 같은 정량적 성과와 함께 PR 리뷰가 가능해진 점, Dependabot이 정상 동작하게 된 점 같은 부수적 개선도 얻을 수 있었습니다.

공식 미지원 도구 조합을 쓸 때 확인할 것

공식 지원되지 않는 도구 조합을 사용하면서 예상치 못한 엣지 케이스를 많이 만났습니다. 다음에 비슷한 선택을 할 때는 이런 것들을 미리 확인하려고 합니다:

  • 공식 지원 로드맵이 있는가? (GitHub Discussions, 블로그 등)
  • 관련 GitHub 이슈가 얼마나 있는가? 해결되고 있는가?
  • lock file 호환성이 보장되는가?
  • 롤백 플랜이 있는가?

다음에 또 비슷한 도전을 하게 된다면, 이번 경험을 토대로 더 신중하고 체계적으로 접근할 수 있을 것 같습니다.

마치며

이 글이 Nx와 Bun을 함께 사용하시는 분들께 도움이 되었으면 합니다. 리스크가 있는 작업임에도 마이그레이션을 허락하고 지원해주신 핀테크그룹의 김현홍 님, 안재민 님께 감사드립니다. 또한, 매번 글 작성 시 기술 내용 정리에 늘 도움 주시는 최진호 님께도 감사의 말씀을 전하며 글을 마칩니다.