프로덕션에서의 Bun: 무엇이 되고, 안 되고, 놀라웠는지
런타임, 패키지 매니저, 번들러, 테스트 러너로서의 Bun. 실제 벤치마크, Node.js 호환성 격차, 마이그레이션 패턴, 그리고 현재 프로덕션에서 Bun을 사용하는 곳.
몇 년에 한 번씩, JavaScript 생태계에 새로운 런타임이 등장하고 담론은 예측 가능한 궤도를 따릅니다. 과대광고. 벤치마크. "X는 죽었다." 현실 확인. 그리고 새로운 도구가 진정으로 빛을 발하는 실제 사용 사례에 정착하는 것이죠.
Bun은 지금 그 궤도의 한가운데에 있습니다. 그리고 대부분의 도전자들과 달리, Bun은 살아남고 있습니다. "더 빠르기" 때문이 아닙니다(실제로 자주 더 빠르긴 하지만), JavaScript 도구 체인에 움직이는 부품이 너무 많다는 근본적으로 다른 문제를 해결하고 있기 때문입니다. Bun은 그것들을 하나로 압축합니다.
저는 1년 넘게 다양한 용도로 Bun을 사용해 왔습니다. 일부는 프로덕션에서. 일부는 절대 대체할 수 없을 거라 생각했던 도구들을 대체하면서요. 이 글은 무엇이 되고, 무엇이 안 되고, 어디에 아직 격차가 중요한지에 대한 솔직한 기록입니다.
Bun이 실제로 무엇인가#
먼저 해소해야 할 오해가 있습니다: Bun은 "더 빠른 Node.js"가 아닙니다. 그런 프레이밍은 과소평가입니다.
Bun은 하나의 바이너리에 네 가지 도구를 담고 있습니다:
- JavaScript/TypeScript 런타임 — Node.js나 Deno처럼 코드를 실행합니다
- 패키지 매니저 — npm, yarn 또는 pnpm을 대체합니다
- 번들러 — 특정 사용 사례에서 esbuild, webpack 또는 Rollup을 대체합니다
- 테스트 러너 — 대부분의 테스트 스위트에서 Jest나 Vitest를 대체합니다
Node.js와의 핵심 아키텍처 차이는 엔진입니다. Node.js는 V8(Chrome의 엔진)을 사용합니다. Bun은 JavaScriptCore(Safari의 엔진)를 사용합니다. 둘 다 성숙하고 프로덕션 수준의 엔진이지만, 다른 트레이드오프를 가지고 있습니다. JavaScriptCore는 더 빠른 시작 시간과 더 낮은 메모리 오버헤드를 가지는 경향이 있습니다. V8은 장시간 실행되는 연산에서 더 나은 최대 처리량을 가지는 경향이 있습니다. 실제로 대부분의 워크로드에서 이 차이는 생각보다 작습니다.
또 다른 주요 차별점: Bun은 Zig로 작성되었습니다. Zig는 대략 C와 같은 수준에 있지만 더 나은 메모리 안전성을 보장하는 시스템 프로그래밍 언어입니다. Bun이 성능에 그렇게 공격적일 수 있는 이유가 바로 이것입니다 — Zig는 C가 제공하는 수준의 저수준 제어를 C의 위험성 없이 제공합니다.
# Bun 버전 확인
bun --version
# TypeScript 파일을 직접 실행 — tsconfig도, 컴파일 단계도 필요 없음
bun run server.ts
# 패키지 설치
bun install
# 테스트 실행
bun test
# 프로덕션용 번들
bun build ./src/index.ts --outdir ./dist하나의 바이너리가 node + npm + esbuild + vitest의 역할을 합니다. 좋든 싫든, 이것은 복잡성의 설득력 있는 감소입니다.
속도 주장 — 정직한 벤치마크#
이 부분에 대해 솔직하게 말씀드리겠습니다: Bun의 마케팅 벤치마크는 체리피킹된 것입니다. 사기가 아니라 — 체리피킹입니다. Bun이 가장 잘하는 시나리오를 보여주는데, 이것은 마케팅 자료에서 정확히 예상할 수 있는 것입니다. 문제는 사람들이 그 벤치마크에서 외삽하여 Bun이 모든 것에서 "25배 빠르다"고 주장하는 것인데, 절대 그렇지 않습니다.
Bun이 진정으로, 의미 있게 더 빠른 곳은 다음과 같습니다:
시작 시간#
이것은 Bun의 가장 큰 진정한 장점이며 비교 대상이 없습니다.
# 시작 시간 측정 — 각각 100번 실행
hyperfine --warmup 5 'node -e "console.log(1)"' 'bun -e "console.log(1)"'
# 일반적인 결과:
# node: ~40ms
# bun: ~6ms시작 시간에서 대략 6-7배 차이가 납니다. 스크립트, CLI 도구, 그리고 콜드 스타트가 중요한 서버리스 함수에서는 이것이 의미 있습니다. 한 번 시작하고 몇 주 동안 실행되는 장기 실행 서버 프로세스에서는 무의미합니다.
패키지 설치#
이것은 Bun이 경쟁을 압도하는 또 다른 영역입니다.
# 클린 설치 벤치마크 — 먼저 node_modules와 lockfile 삭제
rm -rf node_modules bun.lockb package-lock.json
# npm 시간 측정
time npm install
# Real: ~18.4s (일반적인 중간 규모 프로젝트)
# bun 시간 측정
time bun install
# Real: ~2.1s8-9배 차이이며 일관적입니다. 주요 이유는 다음과 같습니다:
- 바이너리 lockfile —
bun.lockb는 JSON이 아닌 바이너리 형식입니다. 읽고 쓰기가 더 빠릅니다. - 글로벌 캐시 — Bun은 글로벌 모듈 캐시를 유지하여 프로젝트 간 재설치 시 다운로드된 패키지를 공유합니다.
- Zig의 I/O — 패키지 매니저 자체가 JavaScript가 아닌 Zig로 작성되었습니다. 파일 I/O 작업이 하드웨어에 더 가깝습니다.
- 심링크 전략 — Bun은 파일을 복사하는 대신 글로벌 캐시에서 하드링크를 사용합니다.
HTTP 서버 처리량#
Bun의 내장 HTTP 서버는 빠르지만, 비교에는 맥락이 필요합니다.
# bombardier로 빠른 벤치마크
# 간단한 "Hello World" 응답 테스트
# Bun 서버
bombardier -c 100 -d 10s http://localhost:3000
# 요청/초: ~105,000
# Node.js (네이티브 http 모듈)
bombardier -c 100 -d 10s http://localhost:3001
# 요청/초: ~48,000
# Node.js (Express)
bombardier -c 100 -d 10s http://localhost:3002
# 요청/초: ~15,000Bun vs. 순수 Node.js: 간단한 응답에서 대략 2배. Bun vs. Express: 대략 7배이지만, Express는 미들웨어 오버헤드를 추가하므로 불공정한 비교입니다. 실제 로직 — 데이터베이스 쿼리, 인증, 실제 데이터의 JSON 직렬화 — 을 추가하는 순간 격차는 극적으로 줄어듭니다.
차이가 미미한 곳#
CPU 바운드 연산:
// fibonacci.ts — 이것은 엔진 바운드이지 런타임 바운드가 아닙니다
function fib(n: number): number {
if (n <= 1) return n;
return fib(n - 1) + fib(n - 2);
}
const start = performance.now();
console.log(fib(42));
console.log(`${(performance.now() - start).toFixed(0)}ms`);bun run fibonacci.ts # ~1650ms
node fibonacci.ts # ~1580msNode.js(V8)가 여기서 약간 이깁니다. V8의 JIT 컴파일러는 핫 루프에서 더 공격적입니다. CPU 바운드 작업에서 엔진 차이는 비슷합니다 — 때때로 V8이 이기고, 때때로 JSC가 이기며, 차이는 노이즈 범위 내입니다.
자체 벤치마크를 실행하는 방법#
누구의 벤치마크도 믿지 마세요, 제 것을 포함해서요. 특정 워크로드에서 중요한 것을 측정하는 방법은 다음과 같습니다:
# 적절한 벤치마킹을 위해 hyperfine 설치
brew install hyperfine # macOS
# 또는: cargo install hyperfine
# 실제 앱의 시작 + 실행 벤치마크
hyperfine --warmup 3 \
'node dist/server.js' \
'bun src/server.ts' \
--prepare 'sleep 0.1'
# HTTP 서버의 경우, bombardier 또는 wrk 사용
# 중요: "Hello World"가 아닌 현실적인 페이로드로 테스트하세요
bombardier -c 50 -d 30s -l http://localhost:3000/api/users
# 메모리 비교
/usr/bin/time -v node server.js # Linux
/usr/bin/time -l bun server.ts # macOS경험법칙: 병목이 I/O(파일 시스템, 네트워크, 데이터베이스)라면, Bun의 이점은 적당합니다. 병목이 시작 시간이나 도구 체인 속도라면, Bun이 크게 이깁니다. 병목이 순수 연산이라면, 비슷합니다.
패키지 매니저로서의 Bun#
이것은 제가 완전히 전환한 영역입니다. 프로덕션에서 Node.js를 실행하는 프로젝트에서도 로컬 개발과 CI에서 bun install을 사용합니다. 단순히 더 빠르고, 호환성이 우수합니다.
기본 사용법#
# package.json에서 모든 의존성 설치
bun install
# 의존성 추가
bun add express
# 개발 의존성 추가
bun add -d vitest
# 의존성 제거
bun remove express
# 의존성 업데이트
bun update express
# 특정 버전 설치
bun add express@4.18.2npm이나 yarn을 사용해 본 적이 있다면, 완전히 익숙할 것입니다. 플래그가 약간 다르지만(--save-dev 대신 -d), 멘탈 모델은 동일합니다.
Lockfile 상황#
Bun은 바이너리 lockfile인 bun.lockb를 사용합니다. 이것은 강점이자 가장 큰 마찰 지점입니다.
좋은 점: 읽고 쓰기가 극적으로 빠릅니다. 바이너리 형식이기 때문에 Bun은 lockfile을 마이크로초 단위로 파싱할 수 있는데, npm이 package-lock.json을 파싱하는 데 수백 밀리초를 소비하는 것과 대조적입니다.
나쁜 점: diff에서 검토할 수 없습니다. 팀에서 누군가 의존성을 업데이트하면, PR에서 lockfile diff를 보고 무엇이 변경되었는지 확인할 수 없습니다. 이것은 속도 옹호자들이 인정하고 싶어하는 것보다 더 중요합니다.
# lockfile을 사람이 읽을 수 있는 형식으로 덤프
bun bun.lockb > lockfile-dump.txt
# 또는 내장 텍스트 출력 사용
bun install --yarn
# 이것은 bun.lockb와 함께 yarn.lock을 생성합니다제 접근 방식: bun.lockb를 저장소에 커밋하고 읽을 수 있는 대체 수단으로 yarn.lock이나 package-lock.json도 생성합니다. 이중 안전장치입니다.
워크스페이스 지원#
Bun은 npm/yarn 스타일 워크스페이스를 지원합니다:
{
"name": "my-monorepo",
"workspaces": [
"packages/*",
"apps/*"
]
}# 모든 워크스페이스의 의존성 설치
bun install
# 특정 워크스페이스에서 스크립트 실행
bun run --filter packages/shared build
# 특정 워크스페이스에 의존성 추가
bun add react --filter apps/web워크스페이스 지원은 견고하며 상당히 개선되었습니다. pnpm과 비교했을 때 주요 차이점은 Bun의 워크스페이스 의존성 해결이 덜 엄격하다는 것입니다 — pnpm의 엄격함은 팬텀 의존성을 잡아내기 때문에 모노레포의 장점입니다.
기존 프로젝트와의 호환성#
거의 모든 기존 Node.js 프로젝트에 bun install을 바로 적용할 수 있습니다. package.json을 읽고, 레지스트리 설정을 위한 .npmrc를 존중하며, peerDependencies를 올바르게 처리합니다. 전환은 일반적으로 다음과 같습니다:
# 1단계: 기존 lockfile과 node_modules 삭제
rm -rf node_modules package-lock.json yarn.lock pnpm-lock.yaml
# 2단계: Bun으로 설치
bun install
# 3단계: 앱이 여전히 작동하는지 확인
bun run dev
# 또는: node dist/server.js (Bun 패키지 매니저, Node 런타임)12개 이상의 프로젝트에서 이렇게 했는데 패키지 매니저 자체에서는 문제가 전혀 없었습니다. 유일한 주의점은 CI 파이프라인이 특별히 package-lock.json을 찾는 경우입니다 — bun.lockb를 처리하도록 업데이트해야 합니다.
Node.js 호환성#
이 섹션은 가장 신중해야 하는 부분인데, 상황이 매달 변하기 때문입니다. 2026년 초 기준으로, 여기 정직한 그림이 있습니다.
작동하는 것#
대다수의 npm 패키지가 수정 없이 작동합니다. Bun은 대부분의 Node.js 내장 모듈을 구현합니다:
// 이것들은 모두 Bun에서 예상대로 작동합니다
import fs from "node:fs";
import path from "node:path";
import crypto from "node:crypto";
import { Buffer } from "node:buffer";
import { EventEmitter } from "node:events";
import { Readable, Writable } from "node:stream";
import http from "node:http";
import https from "node:https";
import { URL, URLSearchParams } from "node:url";
import os from "node:os";
import child_process from "node:child_process";CommonJS와 ESM 모두 작동합니다. require()와 import가 공존할 수 있습니다. TypeScript는 컴파일 단계 없이 실행됩니다 — Bun은 파싱 시점에 타입을 제거합니다.
작동하는 프레임워크:
- Express — 미들웨어 생태계를 포함하여 작동
- Fastify — 작동
- Hono — 작동 (Bun과 함께 탁월함)
- Next.js — 주의사항과 함께 작동 (아래에서 자세히)
- Prisma — 작동
- Drizzle ORM — 작동
- Socket.io — 작동
작동하지 않는 것 (또는 문제가 있는 것)#
격차는 몇 가지 카테고리로 분류되는 경향이 있습니다:
네이티브 애드온 (node-gyp): 패키지가 node-gyp으로 컴파일된 C++ 애드온을 사용하는 경우, Bun에서 작동하지 않을 수 있습니다. Bun은 자체 FFI 시스템이 있고 많은 네이티브 모듈을 지원하지만, 커버리지가 100%는 아닙니다. 예를 들어, bcrypt(네이티브 버전)에 문제가 있었습니다 — 대신 bcryptjs를 사용하세요.
# 패키지가 네이티브 애드온을 사용하는지 확인
ls node_modules/your-package/binding.gyp # 이것이 존재하면 네이티브입니다특정 Node.js 내부: 일부 패키지는 process.binding()과 같은 Node.js 내부에 접근하거나 V8 특정 API를 사용합니다. Bun은 JavaScriptCore에서 실행되므로 이것들은 작동하지 않습니다.
// 이것은 Bun에서 작동하지 않습니다 — V8 전용
const v8 = require("v8");
v8.serialize({ data: "test" });
// 이것은 작동합니다 — Bun의 동등한 것 또는 크로스 런타임 접근 사용
const encoded = new TextEncoder().encode(JSON.stringify({ data: "test" }));워커 스레드: Bun은 Web Workers와 node:worker_threads를 지원하지만, 에지 케이스가 있습니다. 일부 고급 사용 패턴 — 특히 SharedArrayBuffer와 Atomics 관련 — 은 다르게 동작할 수 있습니다.
vm 모듈: node:vm은 부분적으로 지원됩니다. 코드나 의존성이 vm.createContext()를 광범위하게 사용하는 경우(일부 템플릿 엔진이 그렇습니다), 철저히 테스트하세요.
호환성 트래커#
Bun은 공식 호환성 트래커를 유지합니다. 프로젝트에 Bun을 채택하기 전에 확인하세요:
# 프로젝트에서 Bun의 내장 호환성 검사 실행
bun --bun node_modules/.bin/your-tool
# --bun 플래그는 node_modules 스크립트에도 Bun의 런타임을 강제합니다제 권장사항: 호환성을 가정하지 마세요. 결정하기 전에 Bun에서 테스트 스위트를 실행하세요. 5분이면 되고 몇 시간의 디버깅을 절약합니다.
# 빠른 호환성 검사 — Bun에서 전체 테스트 스위트 실행
bun test # bun 테스트 러너 사용 시
# 또는
bun run vitest # vitest 사용 시Bun의 내장 API#
여기서 Bun이 흥미로워집니다. Node.js API를 단순히 재구현하는 대신, Bun은 더 간단하고 빠르도록 설계된 자체 API를 제공합니다.
Bun.serve() — 내장 HTTP 서버#
제가 가장 많이 사용하는 API입니다. 깔끔하고, 빠르며, WebSocket 지원이 바로 내장되어 있습니다.
const server = Bun.serve({
port: 3000,
fetch(req) {
const url = new URL(req.url);
if (url.pathname === "/") {
return new Response("Hello from Bun!", {
headers: { "Content-Type": "text/plain" },
});
}
if (url.pathname === "/api/users") {
const users = [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" },
];
return Response.json(users);
}
return new Response("Not Found", { status: 404 });
},
});
console.log(`Server running at http://localhost:${server.port}`);몇 가지 주목할 점:
- 웹 표준 Request/Response — 독점 API가 아닙니다.
fetch핸들러는 표준Request를 받고 표준Response를 반환합니다. Cloudflare Worker를 작성해 본 적이 있다면, 이것은 동일하게 느껴집니다. Response.json()— 내장 JSON 응답 헬퍼.- import 불필요 —
Bun.serve는 전역입니다.require("http")가 필요 없습니다.
다음은 라우팅, JSON body 파싱, 에러 처리가 포함된 더 현실적인 예제입니다:
import { Database } from "bun:sqlite";
const db = new Database("app.db");
db.run(`
CREATE TABLE IF NOT EXISTS todos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
completed INTEGER DEFAULT 0,
created_at TEXT DEFAULT (datetime('now'))
)
`);
const server = Bun.serve({
port: process.env.PORT || 3000,
async fetch(req) {
const url = new URL(req.url);
const method = req.method;
try {
// GET /api/todos
if (url.pathname === "/api/todos" && method === "GET") {
const todos = db.query("SELECT * FROM todos ORDER BY created_at DESC").all();
return Response.json(todos);
}
// POST /api/todos
if (url.pathname === "/api/todos" && method === "POST") {
const body = await req.json();
if (!body.title || typeof body.title !== "string") {
return Response.json({ error: "Title is required" }, { status: 400 });
}
const stmt = db.prepare("INSERT INTO todos (title) VALUES (?) RETURNING *");
const todo = stmt.get(body.title);
return Response.json(todo, { status: 201 });
}
// DELETE /api/todos/:id
const deleteMatch = url.pathname.match(/^\/api\/todos\/(\d+)$/);
if (deleteMatch && method === "DELETE") {
const id = parseInt(deleteMatch[1], 10);
db.run("DELETE FROM todos WHERE id = ?", [id]);
return new Response(null, { status: 204 });
}
return Response.json({ error: "Not found" }, { status: 404 });
} catch (error) {
console.error("Request error:", error);
return Response.json({ error: "Internal server error" }, { status: 500 });
}
},
});
console.log(`Server running on port ${server.port}`);약 50줄에 SQLite가 포함된 완전한 CRUD API입니다. Express도 없고, ORM도 없고, 미들웨어 체인도 없습니다. 소규모 API와 내부 도구에 대해 이것이 지금 제 기본 설정입니다.
Bun.file()과 Bun.write() — 파일 I/O#
Bun의 파일 API는 fs.readFile()에 비해 상쾌할 정도로 간단합니다:
// 파일 읽기
const file = Bun.file("./config.json");
const text = await file.text(); // 문자열로 읽기
const json = await file.json(); // JSON으로 직접 파싱
const bytes = await file.arrayBuffer(); // ArrayBuffer로 읽기
const stream = file.stream(); // ReadableStream으로 읽기
// 파일 메타데이터
console.log(file.size); // 바이트 단위 크기
console.log(file.type); // MIME 타입 (예: "application/json")
// 파일 쓰기
await Bun.write("./output.txt", "Hello, World!");
await Bun.write("./data.json", JSON.stringify({ key: "value" }));
await Bun.write("./copy.png", Bun.file("./original.png"));
// Response body를 파일에 쓰기
const response = await fetch("https://example.com/data.json");
await Bun.write("./downloaded.json", response);Bun.file() API는 지연 평가됩니다 — .text(), .json() 등을 호출할 때까지 파일을 읽지 않습니다. 이는 실제로 데이터가 필요할 때까지 I/O 비용 없이 Bun.file() 참조를 전달할 수 있다는 의미입니다.
내장 WebSocket 지원#
WebSocket은 Bun.serve()에서 일급 시민입니다:
const server = Bun.serve({
port: 3000,
fetch(req, server) {
const url = new URL(req.url);
if (url.pathname === "/ws") {
const upgraded = server.upgrade(req, {
data: {
userId: url.searchParams.get("userId"),
joinedAt: Date.now(),
},
});
if (!upgraded) {
return new Response("WebSocket upgrade failed", { status: 400 });
}
return undefined;
}
return new Response("Use /ws for WebSocket connections");
},
websocket: {
open(ws) {
console.log(`Client connected: ${ws.data.userId}`);
ws.subscribe("chat");
},
message(ws, message) {
// 모든 구독자에게 브로드캐스트
server.publish("chat", `${ws.data.userId}: ${message}`);
},
close(ws) {
console.log(`Client disconnected: ${ws.data.userId}`);
ws.unsubscribe("chat");
},
},
});server.publish()와 ws.subscribe() 패턴은 내장 pub/sub입니다. Redis도 없고, 별도의 WebSocket 라이브러리도 없습니다. 간단한 실시간 기능에 대해 이것은 놀라울 정도로 편리합니다.
bun:sqlite로 내장된 SQLite#
이것이 저를 가장 놀라게 했습니다. Bun은 런타임에 SQLite가 바로 내장되어 있습니다:
import { Database } from "bun:sqlite";
// 데이터베이스 열기 또는 생성
const db = new Database("myapp.db");
// 더 나은 동시 읽기 성능을 위한 WAL 모드
db.exec("PRAGMA journal_mode = WAL");
// 테이블 생성
db.run(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
created_at TEXT DEFAULT (datetime('now'))
)
`);
// 준비된 문 (재사용 가능, 반복 쿼리에 더 빠름)
const insertUser = db.prepare(
"INSERT INTO users (email, name) VALUES ($email, $name) RETURNING *"
);
const findByEmail = db.prepare(
"SELECT * FROM users WHERE email = $email"
);
// 사용법
const user = insertUser.get({
$email: "alice@example.com",
$name: "Alice",
});
console.log(user); // { id: 1, email: "alice@example.com", name: "Alice", ... }
// 트랜잭션
const insertMany = db.transaction((users: { email: string; name: string }[]) => {
for (const user of users) {
insertUser.run({ $email: user.email, $name: user.name });
}
return users.length;
});
const count = insertMany([
{ email: "bob@example.com", name: "Bob" },
{ email: "carol@example.com", name: "Carol" },
]);
console.log(`Inserted ${count} users`);이것은 C 라이브러리의 성능을 가진 동기식 SQLite입니다(실제로 C 라이브러리이기 때문입니다 — Bun은 libsqlite3를 직접 내장합니다). CLI 도구, 로컬 우선 앱, 소규모 서비스에서 내장 SQLite는 데이터 레이어에 대한 외부 의존성이 전혀 필요 없다는 것을 의미합니다.
Bun 테스트 러너#
bun test는 대부분의 경우 Jest의 드롭인 대체품입니다. 동일한 describe/it/expect API를 사용하고 대부분의 Jest matcher를 지원합니다.
기본 사용법#
// math.test.ts
import { describe, it, expect } from "bun:test";
describe("math utilities", () => {
it("adds numbers correctly", () => {
expect(1 + 2).toBe(3);
});
it("handles floating point", () => {
expect(0.1 + 0.2).toBeCloseTo(0.3);
});
});# 모든 테스트 실행
bun test
# 특정 파일 실행
bun test math.test.ts
# 패턴과 일치하는 테스트 실행
bun test --test-name-pattern "adds numbers"
# 워치 모드
bun test --watch
# 커버리지
bun test --coverage모킹#
Bun은 Jest 호환 모킹을 지원합니다:
import { describe, it, expect, mock, spyOn } from "bun:test";
import { fetchUsers } from "./api";
// 모듈 모킹
mock.module("./database", () => ({
query: mock(() => [{ id: 1, name: "Alice" }]),
}));
describe("fetchUsers", () => {
it("returns users from database", async () => {
const users = await fetchUsers();
expect(users).toHaveLength(1);
expect(users[0].name).toBe("Alice");
});
});
// 객체 메서드 스파이
describe("console", () => {
it("tracks console.log calls", () => {
const logSpy = spyOn(console, "log");
console.log("test message");
expect(logSpy).toHaveBeenCalledWith("test message");
logSpy.mockRestore();
});
});Bun Test vs. Vitest — 솔직한 비교#
저는 이 프로젝트(그리고 대부분의 프로젝트)에 Vitest를 사용합니다. 완전히 전환하지 않은 이유는 다음과 같습니다:
bun test가 이기는 곳:
- 시작 속도. Vitest가 설정 파일을 로딩하기도 전에
bun test는 테스트 실행을 시작합니다. - 제로 설정. 기본 설정에는
vitest.config.ts가 필요 없습니다. - 내장 TypeScript. 변환 단계가 없습니다.
Vitest가 여전히 이기는 곳:
- 생태계: Vitest는 더 많은 플러그인, 더 나은 IDE 통합, 그리고 더 큰 커뮤니티를 가지고 있습니다.
- 설정: Vitest의 설정 시스템이 더 유연합니다. 커스텀 리포터, 복잡한 설정 파일, 다중 테스트 환경.
- 브라우저 모드: Vitest는 실제 브라우저에서 테스트를 실행할 수 있습니다. Bun은 할 수 없습니다.
- 호환성: 일부 테스팅 라이브러리(Testing Library, MSW)는 Vitest/Jest에서 더 철저히 테스트되었습니다.
- 스냅샷 테스팅: 둘 다 지원하지만, Vitest의 구현이 더 성숙하고 더 나은 diff 출력을 제공합니다.
간단한 테스팅 요구사항을 가진 새 프로젝트라면 bun test를 사용하겠습니다. Testing Library, MSW, 복잡한 모킹이 있는 기존 프로젝트라면 Vitest를 유지하겠습니다.
Bun 번들러#
bun build는 빠른 JavaScript/TypeScript 번들러입니다. webpack 대체품이 아닙니다 — esbuild 카테고리에 더 가깝습니다: 빠르고, 의견이 있으며, 일반적인 경우에 집중합니다.
기본 번들링#
# 단일 진입점 번들
bun build ./src/index.ts --outdir ./dist
# 다른 대상에 맞게 번들
bun build ./src/index.ts --outdir ./dist --target browser
bun build ./src/index.ts --outdir ./dist --target bun
bun build ./src/index.ts --outdir ./dist --target node
# 압축
bun build ./src/index.ts --outdir ./dist --minify
# 소스맵 생성
bun build ./src/index.ts --outdir ./dist --sourcemap external프로그래매틱 API#
const result = await Bun.build({
entrypoints: ["./src/index.ts", "./src/worker.ts"],
outdir: "./dist",
target: "browser",
minify: {
whitespace: true,
identifiers: true,
syntax: true,
},
splitting: true, // 코드 분할
sourcemap: "external",
external: ["react", "react-dom"], // 이것들은 번들하지 않음
naming: "[dir]/[name]-[hash].[ext]",
define: {
"process.env.NODE_ENV": JSON.stringify("production"),
},
});
if (!result.success) {
console.error("Build failed:");
for (const log of result.logs) {
console.error(log);
}
process.exit(1);
}
for (const output of result.outputs) {
console.log(`${output.path} — ${output.size} bytes`);
}트리 쉐이킹#
Bun은 ESM에 대한 트리 쉐이킹을 지원합니다:
// utils.ts
export function used() {
return "I'll be in the bundle";
}
export function unused() {
return "I'll be tree-shaken away";
}
// index.ts
import { used } from "./utils";
console.log(used());bun build ./src/index.ts --outdir ./dist --minify
# `unused` 함수는 출력에 나타나지 않습니다Bun Build의 한계#
- CSS 번들링 없음 — CSS에는 별도의 도구가 필요합니다(PostCSS, Lightning CSS, Tailwind CLI).
- HTML 생성 없음 — 전체 웹 앱이 아닌 JavaScript/TypeScript를 번들합니다.
- 플러그인 생태계 — esbuild가 훨씬 더 큰 플러그인 생태계를 가지고 있습니다. Bun의 플러그인 API는 호환되지만 커뮤니티가 더 작습니다.
- 고급 코드 분할 — Webpack과 Rollup이 여전히 더 정교한 청크 전략을 제공합니다.
라이브러리나 간단한 웹 앱의 JS 번들을 빌드하는 데는 bun build가 훌륭합니다. CSS 모듈, 이미지 최적화, 커스텀 청크 전략이 필요한 복잡한 앱 빌드에는 여전히 풀 번들러가 필요합니다.
Bun 매크로#
정말 독특한 기능 하나: 매크로를 통한 컴파일 타임 코드 실행.
// build-info.ts — 이 파일은 런타임이 아닌 빌드 타임에 실행됩니다
export function getBuildInfo() {
return {
builtAt: new Date().toISOString(),
gitSha: require("child_process")
.execSync("git rev-parse --short HEAD")
.toString()
.trim(),
nodeVersion: process.version,
};
}// app.ts
import { getBuildInfo } from "./build-info" with { type: "macro" };
// getBuildInfo()는 번들 타임에 실행됩니다
// 결과는 정적 값으로 인라인됩니다
const info = getBuildInfo();
console.log(`Built at ${info.builtAt}, commit ${info.gitSha}`);번들링 후, getBuildInfo()는 리터럴 객체로 대체됩니다 — 런타임에 함수 호출이 없고, child_process의 import도 없습니다. 코드는 빌드 중에 실행되었고 결과가 인라인되었습니다. 이것은 빌드 메타데이터, 기능 플래그, 또는 환경별 설정을 임베딩하는 데 강력합니다.
Next.js와 함께 Bun 사용하기#
이것은 제가 가장 많이 받는 질문이므로, 매우 구체적으로 답하겠습니다.
오늘 작동하는 것#
Next.js의 패키지 매니저로서의 Bun — 완벽하게 작동합니다:
# Bun으로 의존성 설치, 그런 다음 Node.js로 Next.js 실행
bun install
bun run dev # 실제로는 기본적으로 Node.js를 통해 "dev" 스크립트를 실행합니다
bun run build
bun run start이것이 제가 모든 Next.js 프로젝트에서 하는 것입니다. bun run <script> 명령은 package.json의 scripts 섹션을 읽고 실행합니다. 기본적으로 실제 실행에는 시스템의 Node.js를 사용합니다. 런타임을 변경하지 않고 Bun의 빠른 패키지 설치를 얻을 수 있습니다.
Next.js 개발을 위한 Bun 런타임:
# Bun의 런타임에서 Next.js를 강제 실행
bun --bun run dev이것은 대부분의 경우 개발에서 작동합니다. --bun 플래그는 Bun에게 Node.js에 위임하는 대신 자체 런타임을 사용하도록 지시합니다. 핫 모듈 교체가 작동합니다. API 라우트가 작동합니다. 서버 컴포넌트가 작동합니다.
아직 실험적인 것#
Next.js 프로덕션 빌드를 위한 Bun 런타임:
# Bun 런타임으로 빌드
bun --bun run build
# Bun 런타임으로 프로덕션 서버 시작
bun --bun run start많은 프로젝트에서 작동하지만 에지 케이스를 만난 적이 있습니다:
- 일부 미들웨어 동작이 다름 — Node.js 특정 API에 의존하는 Next.js 미들웨어를 사용하면 호환성 문제가 발생할 수 있습니다.
- 이미지 최적화 — Next.js의 이미지 최적화 파이프라인은 네이티브 애드온인 sharp를 사용합니다. Bun에서 작동하지만 가끔 문제를 본 적이 있습니다.
- ISR (증분 정적 재생성) — 작동하지만, Bun 프로덕션에서 스트레스 테스트를 하지는 않았습니다.
Next.js에 대한 권장사항#
Bun을 패키지 매니저로 사용하세요. Node.js를 런타임으로 사용하세요. 이렇게 하면 호환성 위험 없이 bun install의 속도 이점을 얻을 수 있습니다.
{
"scripts": {
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start"
}
}# 일상 워크플로우
bun install # 빠른 패키지 설치
bun run dev # Node.js를 통해 "next dev" 실행
bun run build # Node.js를 통해 "next build" 실행Next.js의 내부 사용에 대한 Bun의 Node.js 호환성이 100%에 도달하면(가깝지만 아직은 아닙니다), 전환하겠습니다. 그때까지는 패키지 매니저만으로도 설치를 정당화할 만큼 충분한 시간을 절약해 줍니다.
Bun과 Docker#
공식 Bun Docker 이미지는 잘 관리되어 있고 프로덕션에 바로 사용할 수 있습니다.
기본 Dockerfile#
FROM oven/bun:1 AS base
WORKDIR /app
# 의존성 설치
FROM base AS deps
COPY package.json bun.lockb ./
RUN bun install --frozen-lockfile --production
# 빌드 (필요한 경우)
FROM base AS build
COPY package.json bun.lockb ./
RUN bun install --frozen-lockfile
COPY . .
RUN bun run build
# 프로덕션
FROM base AS production
WORKDIR /app
# root로 실행하지 않기
RUN addgroup --system --gid 1001 appgroup && \
adduser --system --uid 1001 appuser
USER appuser
COPY --from=deps /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
COPY --from=build /app/package.json ./
EXPOSE 3000
CMD ["bun", "run", "dist/server.js"]최소 이미지를 위한 멀티 스테이지 빌드#
# 빌드 단계: 모든 의존성이 포함된 풀 Bun 이미지
FROM oven/bun:1 AS builder
WORKDIR /app
COPY package.json bun.lockb ./
RUN bun install --frozen-lockfile
COPY . .
RUN bun build ./src/index.ts --target bun --outdir ./dist --minify
# 런타임 단계: 더 작은 베이스 이미지
FROM oven/bun:1-slim AS runtime
WORKDIR /app
RUN addgroup --system --gid 1001 appgroup && \
adduser --system --uid 1001 appuser
USER appuser
COPY --from=builder /app/dist ./dist
EXPOSE 3000
CMD ["bun", "run", "dist/index.js"]단일 바이너리로 컴파일#
이것은 배포를 위한 Bun의 킬러 기능 중 하나입니다:
# 앱을 단일 실행 파일로 컴파일
bun build --compile ./src/server.ts --outfile server
# 출력은 독립 실행형 바이너리입니다 — 실행에 Bun이나 Node.js가 필요 없습니다
./server# 컴파일된 바이너리를 사용하는 초소형 Docker 이미지
FROM oven/bun:1 AS builder
WORKDIR /app
COPY package.json bun.lockb ./
RUN bun install --frozen-lockfile
COPY . .
RUN bun build --compile ./src/server.ts --outfile server
# 최종 이미지 — 바이너리만
FROM debian:bookworm-slim
WORKDIR /app
RUN addgroup --system --gid 1001 appgroup && \
adduser --system --uid 1001 appuser
USER appuser
COPY --from=builder /app/server ./server
EXPOSE 3000
CMD ["./server"]컴파일된 바이너리는 일반적으로 50-90 MB입니다(Bun 런타임을 번들합니다). Go 바이너리보다는 크지만 전체 Node.js 설치 + node_modules보다는 훨씬 작습니다. 컨테이너화된 배포에서 자체 포함된 특성은 상당한 단순화입니다.
크기 비교#
# Node.js 이미지
docker images | grep node
# node:20-slim ~180MB
# Bun 이미지
docker images | grep bun
# oven/bun:1-slim ~130MB
# debian:bookworm-slim 위의 컴파일된 바이너리
# ~80MB 베이스 + ~70MB 바이너리 = ~150MB 합계
# Alpine과 Node.js 비교
# node:20-alpine ~130MB + node_modules바이너리 접근 방식은 최종 이미지에서 node_modules를 완전히 제거합니다. 프로덕션에서 npm install이 없습니다. 수백 개의 패키지에서 오는 공급망 공격 표면이 없습니다. 파일 하나뿐입니다.
마이그레이션 패턴#
Bun으로의 전환을 고려하고 있다면, 제가 추천하는 점진적 경로는 다음과 같습니다:
1단계: 패키지 매니저만 (위험 제로)#
# npm/yarn/pnpm을 bun install로 교체
# CI 파이프라인 변경:
# 변경 전:
npm ci
# 변경 후:
bun install --frozen-lockfile코드 변경 없음. 런타임 변경 없음. 더 빠른 설치만. 문제가 생기면(생기지 않겠지만), bun.lockb를 삭제하고 npm install을 실행하여 되돌립니다.
2단계: 스크립트와 도구#
# 개발 스크립트에 bun 사용
bun run dev
bun run lint
bun run format
# 일회성 스크립트에 bun 사용
bun run scripts/seed-database.ts
bun run scripts/migrate.ts실제 애플리케이션의 런타임으로는 여전히 Node.js를 사용합니다. 하지만 스크립트는 Bun의 더 빠른 시작과 네이티브 TypeScript 지원으로부터 이점을 얻습니다.
3단계: 테스트 러너 (중간 위험)#
# 간단한 테스트 스위트에서 vitest/jest를 bun test로 교체
bun test
# 복잡한 테스트 설정에는 vitest 유지
# (Testing Library, MSW, 커스텀 환경)bun test에서 전체 테스트 스위트를 실행하세요. 모두 통과하면 devDependency 하나를 제거한 겁니다. 호환성 때문에 일부 테스트가 실패하면, 그것들에 대해 Vitest를 유지하고 나머지에 bun test를 사용하세요.
4단계: 새 서비스를 위한 런타임 (계산된 위험)#
// 새 마이크로서비스나 API — 처음부터 Bun으로 시작
Bun.serve({
port: 3000,
fetch(req) {
// 여기에 새 서비스
},
});기존 Node.js 서비스를 Bun 런타임으로 마이그레이션하지 마세요. 대신, 새 서비스를 처음부터 Bun으로 작성하세요. 이렇게 하면 폭발 반경을 제한합니다.
5단계: 런타임 마이그레이션 (고급)#
# 철저한 테스트 후에만:
# 기존 서비스의 node를 bun으로 교체
# 변경 전:
node dist/server.js
# 변경 후:
bun dist/server.js이것은 뛰어난 테스트 커버리지를 가진 서비스에만 추천합니다. 프로덕션을 전환하기 전에 Bun에서 부하 테스트를 실행하세요.
환경 변수와 설정#
Bun은 .env 파일을 자동으로 처리합니다 — dotenv 패키지가 필요 없습니다:
# .env
DATABASE_URL=postgresql://localhost:5432/myapp
API_KEY=sk-test-12345
PORT=3000// 어떤 import 없이도 사용 가능
console.log(process.env.DATABASE_URL);
console.log(process.env.API_KEY);
console.log(Bun.env.PORT); // Bun 전용 대안Bun은 .env, .env.local, .env.production 등을 Next.js와 같은 규칙을 따라 자동으로 로드합니다. package.json에서 의존성 하나를 줄일 수 있습니다.
에러 처리와 디버깅#
Bun의 에러 출력은 상당히 개선되었지만, 일부 경우에는 아직 Node.js만큼 세련되지 않습니다:
# Bun의 디버거 — VS Code에서 작동
bun --inspect run server.ts
# Bun의 inspect-brk — 첫 번째 줄에서 일시 정지
bun --inspect-brk run server.tsVS Code의 경우, .vscode/launch.json에 이것을 추가하세요:
{
"version": "0.2.0",
"configurations": [
{
"type": "bun",
"request": "launch",
"name": "Debug Bun",
"program": "${workspaceFolder}/src/server.ts",
"cwd": "${workspaceFolder}",
"stopOnEntry": false,
"watchMode": false
}
]
}Bun의 스택 트레이스는 일반적으로 정확하며 TypeScript에 대한 소스 맵을 포함합니다. 주요 디버깅 격차는 일부 Node.js 전용 디버깅 도구(ndb나 clinic.js 같은)가 Bun에서 작동하지 않는다는 것입니다.
보안 고려사항#
프로덕션에서 Bun을 평가하고 있다면 고려해야 할 몇 가지:
성숙도: Node.js는 15년 이상 프로덕션에서 사용되어 왔습니다. HTTP 파싱, TLS 처리, 스트림 처리의 모든 에지 케이스가 발견되고 수정되었습니다. Bun은 더 젊습니다. 잘 테스트되었지만, 미발견 버그의 표면적이 더 넓습니다.
보안 패치: Bun 팀은 업데이트를 자주 배포하지만, Node.js 보안 팀은 공식적인 CVE 프로세스, 조정된 공개, 그리고 더 긴 추적 기록을 가지고 있습니다. 보안이 중요한 애플리케이션에서는 이것이 중요합니다.
공급망: Bun의 내장 기능(SQLite, HTTP 서버, WebSockets)은 npm 의존성을 줄여줍니다. 의존성이 적다는 것은 공급망 공격 표면이 더 작다는 의미입니다. 이것은 진정한 보안 이점입니다.
# 의존성 수 비교
# 일반적인 Express + SQLite + WebSocket 프로젝트:
npm ls --all | wc -l
# ~340개 패키지
# Bun 내장 기능으로 같은 기능:
bun pm ls --all | wc -l
# ~12개 패키지 (애플리케이션 코드만)프로덕션 워크로드에 신뢰하는 패키지 수의 의미 있는 감소입니다.
성능 튜닝#
Bun 전용 성능 팁 몇 가지:
// 프로덕션 튜닝을 위한 Bun.serve() 옵션
Bun.serve({
port: 3000,
// 최대 요청 body 크기 증가 (기본값 128MB)
maxRequestBodySize: 1024 * 1024 * 50, // 50MB
// 더 나은 에러 페이지를 위한 개발 모드 활성화
development: process.env.NODE_ENV !== "production",
// 포트 재사용 (무중단 재시작에 유용)
reusePort: true,
fetch(req) {
return new Response("OK");
},
});// 런타임 코드 변환을 위한 Bun.Transpiler 사용
const transpiler = new Bun.Transpiler({
loader: "tsx",
target: "browser",
});
const code = transpiler.transformSync(`
const App: React.FC = () => <div>Hello</div>;
export default App;
`);# Bun의 메모리 사용 플래그
bun --smol run server.ts # 메모리 사용량 감소 (약간 느려짐)
# 최대 힙 크기 설정
BUN_JSC_forceRAMSize=512000000 bun run server.ts # ~512MB 제한흔한 함정#
1년간 Bun을 사용하면서 저를 곤란하게 한 것들:
1. 전역 Fetch 동작 차이#
// Node.js 18+ fetch와 Bun의 fetch는
// 특정 헤더와 리다이렉트 처리 방식이 약간 다릅니다
// Bun은 기본적으로 리다이렉트를 따릅니다 (브라우저처럼)
// Node.js fetch도 리다이렉트를 따르지만, 특정 상태 코드
// (303, 307, 308)에서 동작이 다를 수 있습니다
const response = await fetch("https://api.example.com/data", {
redirect: "manual", // 리다이렉트 처리를 명시적으로 지정
});2. 프로세스 종료 동작#
// Bun은 이벤트 루프가 비면 종료됩니다
// Node.js는 때때로 남아있는 핸들 때문에 계속 실행됩니다
// Bun 스크립트가 예상치 않게 종료되면, 이벤트 루프를
// 유지하는 것이 없다는 의미입니다
// 이것은 Bun에서 즉시 종료됩니다:
setTimeout(() => {}, 0);
// 이것은 계속 실행됩니다:
setTimeout(() => {}, 1000);
// (타임아웃이 실행된 후 Bun이 종료됩니다)3. TypeScript 설정#
// Bun은 자체 tsconfig 기본값을 가지고 있습니다
// Bun과 Node.js 간에 프로젝트를 공유하는 경우,
// tsconfig.json에서 명시적으로 지정하세요:
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"types": ["bun-types"] // Bun 타입 정의 추가
}
}# Bun 타입 설치
bun add -d @types/bun4. 개발 중 핫 리로드#
# Bun에는 내장 워치 모드가 있습니다
bun --watch run server.ts
# 파일 변경 시 프로세스를 재시작합니다
# HMR(핫 모듈 교체)이 아닌 전체 재시작입니다
# 하지만 Bun이 너무 빨리 시작하기 때문에, 즉시처럼 느껴집니다5. bunfig.toml 설정 파일#
# bunfig.toml — Bun의 설정 파일 (선택사항)
[install]
# 프라이빗 레지스트리 사용
registry = "https://npm.mycompany.com"
# 스코프 레지스트리
[install.scopes]
"@mycompany" = "https://npm.mycompany.com"
[test]
# 테스트 설정
coverage = true
coverageReporter = ["text", "lcov"]
[run]
# bun run에 사용할 셸
shell = "bash"최종 판단#
1년간의 프로덕션 사용 후, 제가 정착한 곳은 다음과 같습니다:
오늘 Bun을 사용하는 곳#
모든 프로젝트의 패키지 매니저 — 이 Next.js 블로그를 포함해서. bun install이 더 빠르고, 호환성은 본질적으로 완벽합니다. 더 이상 npm이나 yarn을 사용할 이유를 모르겠습니다. pnpm만이 제가 고려할 유일한 대안입니다(모노레포에서의 엄격한 의존성 해결 때문에).
스크립트와 CLI 도구의 런타임 — 한 번 실행해야 하는 모든 TypeScript 파일을 bun으로 실행합니다. 컴파일 단계 없음. 빠른 시작. 내장 .env 로딩. 제 워크플로우에서 ts-node와 tsx를 완전히 대체했습니다.
소규모 API와 내부 도구의 런타임 — Bun.serve() + bun:sqlite는 내부 도구, 웹훅 핸들러, 소규모 서비스에 놀라울 정도로 생산적인 스택입니다. "하나의 바이너리, 의존성 없는" 배포 모델이 매력적입니다.
간단한 프로젝트의 테스트 러너 — 간단한 테스트 요구사항을 가진 프로젝트에서 bun test는 빠르고 제로 설정이 필요합니다.
Node.js를 고수하는 곳#
프로덕션 Next.js — Bun이 작동하지 않아서가 아니라, 위험 대비 보상이 아직 정당화되지 않기 때문입니다. Next.js는 많은 통합 지점을 가진 복잡한 프레임워크입니다. 그 아래에서 가장 실전 검증된 런타임을 원합니다.
중요한 프로덕션 서비스 — 제 메인 API 서버는 PM2 뒤에서 Node.js를 실행합니다. 모니터링 생태계, 디버깅 도구, 운영 지식 — 모두 Node.js입니다. Bun이 거기에 도달할 것이지만, 아직은 아닙니다.
네이티브 애드온이 있는 모든 것 — 의존성 체인에 C++ 네이티브 애드온이 포함되어 있으면, Bun을 시도하지도 않습니다. 호환성 문제를 디버깅할 가치가 없습니다.
Bun에 익숙하지 않은 팀 — 한 번도 사용해 본 적 없는 팀에 Bun을 런타임으로 도입하면 인지 부하가 추가됩니다. 패키지 매니저로서는 괜찮습니다. 런타임으로서는 팀이 준비될 때까지 기다리세요.
주시하고 있는 것#
Bun의 호환성 트래커 — 제가 관심 있는 Node.js API에 대해 100%에 도달하면 재평가하겠습니다.
프레임워크 지원 — Next.js, Remix, SvelteKit 모두 다양한 수준의 Bun 지원을 가지고 있습니다. 그 중 하나가 공식적으로 Bun을 프로덕션 런타임으로 지원하면, 그것이 신호입니다.
엔터프라이즈 도입 — 실제 SLA를 가진 기업들이 프로덕션에서 Bun을 실행하고 그것에 대해 쓰기 시작하면, 성숙도 질문에 대한 답이 됩니다.
1.2+ 릴리스 라인 — Bun은 빠르게 움직이고 있습니다. 매주 기능이 추가됩니다. 오늘 제가 사용하는 Bun은 1년 전에 시도한 Bun보다 의미 있게 더 좋습니다.
마무리#
Bun은 만능이 아닙니다. 느린 앱을 빠르게 만들지 않고, 잘못 설계된 API를 잘 설계된 것으로 만들지 않습니다. 하지만 JavaScript 생태계의 개발자 경험에 대한 진정한 개선입니다.
Bun에서 제가 가장 감사하는 것은 어떤 단일 기능이 아닙니다. 도구 체인 복잡성의 감소입니다. 패키지를 설치하고, TypeScript를 실행하고, 코드를 번들하고, 테스트를 실행하는 하나의 바이너리. 스크립트를 위한 tsconfig.json이 없습니다. Babel이 없습니다. 별도의 테스트 러너 설정이 없습니다. 그냥 bun run your-file.ts하면 작동합니다.
실용적인 조언: bun install부터 시작하세요. 위험 제로, 즉각적인 이점입니다. 그런 다음 스크립트에 bun run을 시도하세요. 그 다음 특정 요구사항에 따라 나머지를 평가하세요. 올인할 필요가 없습니다. Bun은 부분적 대체품으로도 완벽하게 잘 작동하며, 그것이 아마도 오늘날 대부분의 사람들이 사용해야 하는 방식일 것입니다.
JavaScript 런타임 환경은 Bun이 있어서 더 나아졌습니다. 경쟁은 Node.js도 더 나아지게 만들고 있습니다 — Node.js 22+는 부분적으로 Bun의 압박에 대응하여 상당히 빨라졌습니다. 모두가 이깁니다.