Bun 1.4 출시: Zig에서 Rust로 다시 쓴 자바스크립트 런타임

Bun 1.4 출시: Zig에서 Rust로 다시 쓴 자바스크립트 런타임

2026년 8월 20일에 Bun 1.4가 출시되었습니다. 저는 릴리스 노트를 열어보고 잠깐 스크롤을 멈췄는데요. 보통 이런 글은 새 API 목록으로 시작하는데, 이번에는 첫 문단부터 “Bun을 Zig에서 Rust로 다시 썼다”는 문장이 나오더라고요 🤯

런타임의 구현 언어를 통째로 바꾼다는 건 자동차가 달리는 중에 엔진을 갈아 끼우는 일에 가깝습니다. 그런데 이번 릴리스는 그 소식 말고도 볼 게 많습니다. Node.js 호환성 테스트가 1,517개 늘었고, 이미지 처리와 크론, 마크다운 파서가 표준 라이브러리로 들어왔으며, bun test는 드디어 병렬로 돌아갑니다. 이번 글에서는 Bun 1.4에서 바뀐 것들을 하나씩 짚어보겠습니다.

Zig에서 Rust로

Bun은 처음부터 Zig로 만들어진 런타임이었습니다. Bun을 처음 소개했던 글에서도 “Zig라는 아직 많은 분들에게 생소할 수도 있는 프로그래밍 언어로 개발이 되었습니다”라고 적었을 정도로, Zig는 Bun의 정체성 같은 부분이었는데요. 그 코드베이스가 1.4에서 Rust로 넘어갔습니다.

이유는 성능이 아니라 메모리 안전성이었습니다. Bun 팀이 버그 목록을 정리해 보니 상당수가 해제 후 사용(use-after-free)과 이중 해제(double free), 오류 처리 경로에서 메모리를 해제하지 않고 빠져나가는 문제였다고 합니다. Zig에서는 defer로 일일이 챙겨야 하는 것들인데, 안전한 Rust에서는 소유권과 대여 검사기(borrow checker)가 이런 실수를 컴파일 오류로 잡아줍니다. 런타임에서 터지던 버그가 빌드 단계에서 걸러지는 셈이죠.

규모는 만만치 않았습니다. Zig 파일 1,448개, 주석을 뺀 535,496줄을 옮겼고 커밋은 6,502개가 쌓였습니다. 사람이 손으로 하기엔 무리인 분량이라 Bun 팀은 Claude Code를 대규모로 돌렸는데요. 5월 3일부터 14일까지 11일 동안, 많을 때는 64개의 에이전트가 4개의 워크트리에 나뉘어 동시에 작업했다고 합니다. 작업 단위마다 구현 담당 하나, 코드가 틀렸다고 가정한 채 변경분(diff)만 들여다보는 적대적 리뷰어 둘, 수정 담당 하나를 붙이는 구조였습니다.

100만 줄이 넘는 변경분을 에이전트가 만들었다는 점 때문에 커뮤니티에서는 신뢰할 수 있느냐는 논쟁도 뜨거웠습니다. Bun 팀이 내놓은 답은 숫자였는데요. 1.4.0은 1.3.14에서 재현되던 버그 128개를 고쳤고, 같은 프로세스에서 빌드를 반복할 때 3MB씩 새어 나가던 메모리도 이제는 일정 수준에서 더 늘지 않습니다. 남은 unsafe 블록은 Rust 코드의 약 4% 수준이고, JavaScriptCore와 BoringSSL, SQLite 같은 C 라이브러리 연동 부분은 그대로 남아 있습니다.

무엇보다 이 재작성본은 이미 몇 달째 실전에서 돌아가고 있었습니다. Claude Code와 Prisma의 Prisma Compute가 정식 릴리스 전부터 Rust 포팅본 위에서 서비스되고 있었거든요. 1.4는 그걸 공개 릴리스로 확정한 버전입니다.

Node.js 호환성

이번 릴리스에서 Node.js 테스트 스위트를 1,517개 더 통과했습니다. Bun 1.0 이후 가장 큰 폭의 상승이고, node:httpnode:fs, node:cluster는 Node.js 자체 테스트의 97%를 통과합니다.

숫자보다 와닿는 건 이제 실제로 돌아가는 도구입니다. Playwright가 Bun 위에서 실행되고, Next.js 16을 Turbopack으로 빌드할 수 있으며, Vitest--coverage까지 붙여서 돌릴 수 있게 되었습니다. Bun이 오랫동안 “Node.js를 그대로 대체한다”고 마케팅해 온 것에 비해 실제로는 라이브러리 하나 물릴 때마다 조마조마했는데, 이런 무거운 도구들이 통과했다는 건 의미가 작지 않습니다.

이미지 처리와 헤드리스 브라우저

Bun 1.4에서 가장 눈에 띄는 추가는 표준 라이브러리 쪽입니다. 먼저 Bun.Image가 JPEG, PNG, WebP, GIF, BMP를 디코딩하고 크기 조정과 회전, 인코딩까지 처리합니다.

await Bun.file("photo.jpg")
  .image()
  .resize(1024, 1024, { fit: "inside" })
  .rotate(90)
  .webp({ quality: 85 })
  .write("thumb.webp");

Bun.file()에서 바로 .image()를 이어 붙이는 체이닝이 꽤 편한데요. 서버에서 업로드받은 이미지를 그 자리에서 변환해 응답으로 돌려주는 것도 한 줄입니다.

return new Response(new Bun.Image(upload).resize(200).jpeg());

그동안 자바스크립트에서 이미지를 다루려면 sharp 같은 네이티브 모듈을 설치해야 했고, 그 네이티브 바이너리가 배포 환경마다 말썽을 부리는 일도 잦았죠. Bun은 디코딩부터 리사이즈, 인코딩까지 이어지는 작업이 sharp보다 1.38배 빠르다고 밝히고 있습니다.

Bun.WebViewPuppeteer나 Playwright 없이 헤드리스 브라우저(headless browser)를 제어합니다.

await using view = new Bun.WebView({ width: 800, height: 600 });
await view.navigate("https://bun.sh");
await view.click("a[href='/docs']");
const title = await view.evaluate("document.title");
await Bun.write("page.png", await view.screenshot());

await using 구문을 써서 블록을 벗어날 때 뷰가 알아서 정리되는 점도 눈여겨볼 만합니다. 스크린샷 한 장 찍자고 수백 메가바이트짜리 브라우저를 내려받던 일이 줄어들겠네요.

마크다운과 크론, 터미널

Bun.markdown은 마크다운 파서입니다. HTML로 뽑을 수도 있고, React 엘리먼트로 바로 렌더링할 수도 있습니다.

const html = Bun.markdown.html("# Hello **world**");

터미널 출력용으로 쓸 때는 노드 종류별로 변환 함수를 넘겨서 ANSI 이스케이프 시퀀스를 직접 구성할 수 있습니다.

const ansi = Bun.markdown.render("# Hello\n\n**bold**", {
  heading: (children) => `\x1b[1;4m${children}\x1b[0m\n`,
  paragraph: (children) => children + "\n",
  strong: (children) => `\x1b[1m${children}\x1b[22m`,
});

Bun.cron()은 조금 독특합니다. 프로세스 안에서 도는 스케줄러가 아니라 운영체제에 작업을 등록하는 방식이거든요. 리눅스에서는 crontab, macOS에서는 launchd, 윈도우에서는 작업 스케줄러를 씁니다.

await Bun.cron("./worker.ts", "30 2 * * MON", "weekly-report");

프로세스 수명 안에서만 도는 스케줄러가 필요하다면 콜백을 넘기면 됩니다.

using job = Bun.cron("*/5 * * * *", async () => {
  await cleanupTempFiles();
});

다음 실행 시각만 계산하고 싶을 때는 Bun.cron.parse()를 쓰면 되고요. 크론 표현식 자체가 낯설다면 crontab 명령어 사용법에서 정리한 다섯 필드 문법이 그대로 적용됩니다.

마지막으로 Bun.Terminal은 의사 터미널(pseudo-terminal, PTY)을 내장해서 bash나 vim, htop 같은 프로그램을 자바스크립트에서 직접 조종할 수 있게 해줍니다. Bun.spawn()terminal 옵션을 넘기는 방식입니다.

const proc = Bun.spawn(["bash"], {
  terminal: {
    cols: 80,
    rows: 24,
    data(term, data) {
      process.stdout.write(data);
    },
  },
});

proc.terminal.write("echo Hello from PTY!\n");

이 밖에도 Bun.JSON5, Bun.JSONC, Bun.JSONL, Bun.TOML, Bun.XML, Bun.Archive 같은 파싱과 압축 유틸리티가 한꺼번에 들어왔고, URLPattern 웹 표준 API와 네이티브 CompressionStream도 지원됩니다.

bun install

패키지 매니저 쪽에도 그동안 아쉬웠던 명령어들이 채워졌습니다.

  • bun dedupe: 잠금 파일(lockfile)에서 중복된 버전을 정리합니다.
  • bun prune: 쓰지 않는 패키지를 지웁니다. --production을 붙이면 개발 의존성까지 정리합니다.
  • bun audit fix: 취약점이 있는 패키지를 안전한 버전으로 올려줍니다.
  • bun pm licenses: 의존성을 라이선스별로 나열합니다.
  • bun pm diff: 패키지 버전 간 차이를 압축 해제된 형태로 비교합니다.
bun dedupe
bun prune --production
bun audit fix

npm audit fixpnpm dedupe를 쓰다가 Bun으로 넘어오면 매번 아쉬웠던 것들인데, 이제 npm 계열 명령어와 기능 차이가 거의 없어졌습니다. 격리 설치 모드에서는 심볼릭 링크로 캐시를 공유하는 전역 가상 저장소가 도입되어 캐시가 살아 있을 때 설치가 7배 빨라졌고요.

의존성의 의존성을 덮어쓰는 중첩 오버라이드도 가능해졌습니다. package.json에 이렇게 적으면 express가 끌고 오는 qs만 특정 버전으로 고정할 수 있습니다.

package.json
{
  "overrides": {
    "express": {
      "qs": "6.13.0"
    }
  }
}

bun test

bun test는 그동안 파일을 순차적으로 실행했는데, 1.4부터 워커 프로세스로 나눠서 병렬로 돌릴 수 있습니다.

bun test --parallel
bun test --parallel=4 --isolate
bun test --shard=1/3 --timings=timings.json

--isolate는 파일마다 새 전역 객체와 깨끗한 모듈 레지스트리를 주기 때문에, 테스트끼리 전역 상태를 오염시켜 생기던 순서 의존 버그를 잡을 때 유용합니다. --shard=M/N은 CI 머신 여러 대에 테스트를 쪼개 나누고, --timings로 기록한 파일별 소요 시간을 함께 넘기면 샤드마다 걸리는 시간이 고르게 맞춰집니다.

바뀐 파일만 골라 돌리는 --changed와 불안정한 테스트를 다시 시도하는 --retry <N>도 추가되었습니다. Jest의 가짜 타이머인 jest.useFakeTimers()jest.advanceTimersByTime(), jest.setSystemTime()도 이제 동작합니다.

스크립트 실행에도 병렬 옵션이 생겼습니다. 여러 스크립트를 동시에 돌리면서 출력에 접두사를 붙여주고, 글롭 패턴과 워크스페이스 필터도 지원합니다.

bun run --parallel build test
bun run --parallel "build:*"
bun run --parallel --filter '*' build

bun build

번들러 쪽에서는 React 컴파일러가 내장된 것이 가장 큽니다. Babel 플러그인으로 돌리던 것에 비해 큰 코드베이스에서 20배 빠르다고 하네요. 배럴 파일에서 쓰지 않는 익스포트를 건너뛰는 최적화도 들어갔고, 2만 개 모듈 규모에서 코드 분할은 14배 빨라졌습니다.

빌드 타임에 결정되는 기능 플래그도 재미있는데요. bun:bundle에서 feature()를 불러와 쓰면 번들 시점에 true나 false로 치환되어 죽은 코드가 제거됩니다. 컴파일 대상으로 --compile --target=browser를 주면 모든 것이 인라인된 HTML 파일 하나가 떨어지고, --asset으로 파일이나 디렉터리를 실행 파일에 그대로 심을 수도 있습니다.

얼마나 빨라졌나

Rust 재작성 자체는 2~5% 정도의 개선이지만, 이번 릴리스에 함께 들어간 최적화까지 더하면 체감할 만한 수치가 나옵니다.

작은 애플리케이션의 유휴 CPU 사용량은 5분의 1로 줄었습니다. 트래픽이 없는 시간대에도 CPU를 계속 갉아먹던 문제가 개선된 것이라, 인스턴스를 여러 개 띄워두는 환경이라면 비용에 바로 반영될 부분입니다.

HTTP 서버의 메모리 사용량은 프레임워크에 따라 13%에서 48%까지 줄었습니다. Fastify는 233MB에서 120MB로, Express는 169MB에서 92MB로, node:http는 135MB에서 81MB로 내려갔습니다.

시작 시간도 짧아졌습니다. 리눅스에서 10.9ms가 5.1ms로, 윈도우에서는 39.0ms가 15.5ms로 줄었습니다. CLI 도구처럼 프로세스를 자주 띄웠다 내리는 용도라면 이 차이가 누적되죠. 바이너리 용량도 리눅스 x64 기준 88.5MB에서 77.0MB로 가벼워졌습니다.

업그레이드

이미 Bun을 쓰고 계신다면 명령어 한 줄이면 됩니다.

bun upgrade

처음 설치하신다면 아래 스크립트를 쓰시면 되고요.

curl -fsSL https://bun.sh/install | bash

마치며

지금까지 Bun 1.4의 주요 변경점을 살펴봤습니다.

이번 릴리스는 성격이 조금 다릅니다. Bun 1.1 릴리스가 네이티브 윈도우 지원이라는 분명한 한 가지를 들고 나왔다면, 1.4는 런타임의 구현 언어를 바꾸는 위험한 수술을 하면서 동시에 표준 라이브러리와 도구 체인을 한꺼번에 넓혔습니다. 개인적으로는 Bun.ImageBun.cron처럼 그동안 패키지를 설치해서 해결하던 일이 런타임 안으로 들어온 흐름이 반갑습니다. 헬로 Bun을 쓰면서 “Bun으로 도대체 못 하는 게 뭐지?” 싶었는데, 1.4를 보고 또 한 번 같은 생각이 들었습니다 😄

물론 에이전트로 생성한 100만 줄짜리 재작성을 얼마나 믿을 수 있느냐는 질문은 아직 열려 있습니다. 지금 당장 프로덕션 런타임을 올리기보다는 개인 프로젝트나 CI에서 며칠 굴려보면서 지켜보는 편이 마음 편할 것 같아요. 다만 Bun 위에 웹 프레임워크를 얹어 쓰고 계시다면, Elysia가 새 API와 어떻게 맞물리는지 미리 확인해 두면 좋겠습니다.

더 자세한 변경 내역은 Bun 1.4 릴리스 노트를 참고하세요.

This work is licensed under CC BY 4.0CCBY

개발자를 위한 뉴스레터

달레가 정리한 AI 개발 트렌드와 직접 만든 콘텐츠를 전해드립니다.

Discord