본문으로 건너뛰기 Bun 완전 가이드 | Node.js를 뛰어넘는 초고속 JavaScript 런타임

Bun 완전 가이드 | Node.js를 뛰어넘는 초고속 JavaScript 런타임

Bun 완전 가이드 | Node.js를 뛰어넘는 초고속 JavaScript 런타임

이 글의 핵심

Node.js를 뛰어넘는 초고속 JavaScript 런타임 Bun을 소개합니다. 번들러·테스트·패키지 매니저까지 올인원으로 제공하며, 빠른 실행 속도와 네이티브 TypeScript 지원으로 개발 경험을 크게 개선합니다.

Bun이란 무엇인가

Bun은 JavaScript와 TypeScript를 실행하는 런타임이자, 번들러·테스트 러너·패키지 매니저를 하나로 묶은 올인원 도구입니다. Node.js가 V8 엔진 위에서 동작하는 런타임에 불과했다면, Bun은 처음부터 “자바스크립트 생태계에서 반복적으로 필요한 도구를 전부 하나의 바이너리에 담자”는 목표로 설계되었습니다. 그 결과 webpack, jest, npm 같은 별도 패키지를 설치하지 않아도 프로젝트를 초기화하고, 코드를 실행하고, 테스트하고, 배포용 번들을 만들 수 있습니다.

이 글에서는 처음 Bun을 접하는 개발자를 대상으로 설치부터 서버 구축, 데이터베이스 사용, 테스트, 번들링, 그리고 실제 프로덕션 도입 여부까지 실무 관점에서 정리합니다. “무조건 빠르다”는 마케팅 문구보다는, 언제 Bun을 선택하고 언제 Node.js를 유지해야 하는지에 초점을 맞춥니다.

설치하기

Bun은 운영체제별로 제공되는 설치 스크립트를 실행하면 됩니다. macOS와 Linux에서는 셸 스크립트를, Windows에서는 PowerShell 스크립트를 사용합니다.

curl -fsSL https://bun.sh/install | bash
bun --version
powershell -c "irm bun.sh/install.ps1 | iex"

Docker 환경을 사용한다면 공식 oven/bun 이미지를 그대로 활용하는 것이 가장 간편합니다. CI 파이프라인에서는 매번 스크립트를 내려받는 대신, 이 이미지를 베이스로 삼으면 설치 단계 자체를 생략할 수 있어 빌드 시간을 줄일 수 있습니다. 설치가 끝나면 bun --version 명령으로 정상적으로 설치되었는지 확인하는 것이 좋습니다. 버전이 출력되지 않는다면 셸의 PATH 환경 변수에 Bun의 설치 경로가 추가되었는지 다시 확인해야 합니다.

프로젝트 시작하기: bun init과 bun run

새 프로젝트는 bun init 명령으로 시작합니다. 이 명령은 package.json, tsconfig.json, 진입점 파일을 자동으로 생성해주므로, TypeScript 설정을 처음부터 손으로 작성할 필요가 없습니다.

mkdir my-app && cd my-app
bun init -y
bun add express

가장 눈에 띄는 차이는 bun run index.ts 명령만으로 TypeScript 파일을 별도 컴파일 과정 없이 바로 실행할 수 있다는 점입니다. Node.js 환경에서는 보통 tsc로 컴파일한 뒤 node dist/index.js를 실행하거나, ts-node 같은 별도 패키지를 추가해야 했습니다. Bun은 이 과정을 런타임 내부에서 즉시 처리하기 때문에, package.json의 스크립트가 훨씬 단순해집니다. 처음에는 “컴파일 과정이 생략되어도 괜찮은가”라는 불안이 들 수 있지만, 실제로는 실행 시점에 필요한 부분만 빠르게 트랜스파일하는 방식이므로 개발 단계에서 특히 체감 속도가 큽니다.

패키지 매니저로서의 Bun: bun install 속도

Bun은 런타임뿐 아니라 npm을 대체하는 패키지 매니저로도 동작합니다. bun install 명령은 npm이나 yarn보다 훨씬 빠른 설치 속도를 보이는데, 이는 병렬 다운로드와 전역 캐시, 그리고 네이티브 코드로 작성된 설치 로직 덕분입니다. 실제로 의존성이 많은 프로젝트에서는 npm install이 수 분씩 걸리던 작업이 bun install로는 수 초에서 수십 초 수준으로 단축되는 경우가 흔합니다.

bun install          # package.json 기준으로 의존성 설치
bun add zod           # 의존성 추가
bun add -d typescript  # devDependencies로 추가
bun remove lodash      # 의존성 제거

Bun은 자체 락파일 형식인 bun.lockb(바이너리 형식)를 사용합니다. 이 방식은 파싱 속도를 높이는 대신 사람이 직접 읽거나 git diff로 변경 내역을 확인하기 어렵다는 단점이 있습니다. 최근 버전에서는 텍스트 기반의 bun.lock 형식도 지원하므로, 코드 리뷰 과정에서 락파일 변경을 눈으로 확인해야 하는 팀이라면 이 옵션을 활용하는 것이 좋습니다. 중요한 점은 한 프로젝트 안에서 npmbun을 섞어 쓰지 않는 것입니다. package-lock.jsonbun.lockb가 동시에 존재하면 팀원마다 서로 다른 버전의 의존성을 설치하게 되어 “내 컴퓨터에서는 되는데” 문제가 발생하기 쉽습니다. 팀 차원에서 어떤 패키지 매니저를 표준으로 삼을지 명확히 정하고, CI 스크립트와 README에 명시해 두는 것을 권장합니다.

Bun.serve로 서버 만들기

Bun은 Bun.serve라는 내장 API로 별도 프레임워크 없이 HTTP 서버를 띄울 수 있습니다. Express 같은 기존 프레임워크도 대부분 그대로 동작하므로, 기존 코드를 크게 수정하지 않고 런타임만 Bun으로 바꾸는 것도 현실적인 선택지입니다.

const server = Bun.serve({
  port: 3000,
  fetch(req) {
    const url = new URL(req.url);
    if (url.pathname === "/health") {
      return new Response("ok");
    }
    return new Response("Hello from Bun");
  },
});

console.log(`Listening on http://localhost:${server.port}`);

Bun.serve는 표준 웹 Request/Response 객체를 그대로 사용한다는 점이 특징입니다. Node.js의 http 모듈이 자체적인 요청/응답 인터페이스를 정의했던 것과 달리, Bun은 브라우저와 Cloudflare Workers 등 다른 런타임에서도 통용되는 Fetch API 표준을 따릅니다. 이는 코드를 여러 런타임 사이에서 재사용하기 쉽게 만들어주는 장점이 있습니다. 다만 미들웨어 생태계나 세션 관리 라이브러리는 아직 Express나 Fastify만큼 풍부하지 않으므로, 복잡한 인증·권한 로직이 필요한 서비스라면 기존 프레임워크를 그대로 Bun 위에서 실행하는 편이 개발 속도 면에서 유리할 수 있습니다.

bun:sqlite로 데이터베이스 다루기

Bun에는 SQLite를 다루는 bun:sqlite 모듈이 내장되어 있습니다. 별도 패키지 설치 없이 네이티브 바인딩으로 동작하기 때문에 속도가 빠르고, API도 매우 직관적입니다.

import { Database } from "bun:sqlite";

const db = new Database("app.db");
db.run(`
  CREATE TABLE IF NOT EXISTS users (
    id INTEGER PRIMARY KEY,
    name TEXT NOT NULL,
    email TEXT UNIQUE
  )
`);

// 쿼리 준비 및 실행
const insert = db.prepare("INSERT INTO users (name, email) VALUES (?, ?)");
insert.run("Alice", "alice@example.com");

const select = db.prepare("SELECT * FROM users WHERE email = ?");
const user = select.get("alice@example.com");
console.log(user);

이 API가 짧고 간결한 이유는 그만큼 추상화 계층이 얇기 때문입니다. 이는 장점이자 단점입니다. 간단한 CRUD 작업에는 별도 ORM 없이도 충분히 실용적이지만, 트랜잭션 관리, 커넥션 풀링, 마이그레이션 이력 관리처럼 규모가 커지면 필요한 기능은 직접 구현하거나 Drizzle ORM 같은 상위 라이브러리를 함께 사용하는 것이 안전합니다. 특히 동시성이 높은 서비스에서는 스트림 처리, 에러 핸들링, 백프레셔(backpressure) 제어를 별도로 설계해야 한다는 점을 염두에 두어야 합니다.

bun:test로 테스트 작성하기

Bun은 bun:test라는 내장 테스트 러너를 제공하며, API가 Jest와 매우 유사해 기존 테스트 코드를 옮기는 부담이 적습니다.

import { describe, it, expect, beforeEach } from "bun:test";

describe("add 함수", () => {
  it("두 숫자를 더한다", () => {
    expect(1 + 1).toBe(2);
  });

  it("음수도 처리한다", () => {
    expect(-1 + 1).toBe(0);
  });
});
bun test               # 전체 테스트 실행
bun test --watch       # 파일 변경 감지 후 자동 재실행
bun test src/utils.test.ts  # 특정 파일만 실행

“엄청 빠르다”는 문구를 그대로 믿기보다는, 테스트 스위트의 규모에 따라 체감이 달라진다는 점을 알아두는 것이 좋습니다. 테스트 파일이 몇 개 되지 않는 작은 프로젝트에서는 Jest와 큰 차이를 느끼기 어렵지만, 수백 개 이상의 테스트 파일을 가진 대규모 프로젝트에서는 Bun의 네이티브 실행 속도가 확실한 이점으로 작용합니다. 또한 Jest의 설정 파일(jest.config.js)이나 babel 트랜스파일 과정 없이 TypeScript 테스트 파일을 즉시 실행할 수 있다는 점도 초기 설정 시간을 줄여줍니다.

Bun.build로 번들링하기

Bun은 Bun.build API 또는 bun build CLI 명령으로 프로덕션용 번들을 생성할 수 있습니다.

bun build ./src/index.ts --outdir ./dist --target node --minify
// build.ts
await Bun.build({
  entrypoints: ["./src/index.ts"],
  outdir: "./dist",
  target: "browser",
  minify: true,
  sourcemap: "external",
});

Webpack이나 esbuild를 오래 사용해온 팀이라면 플러그인 생태계와 세부 설정 옵션의 차이를 먼저 확인해야 합니다. Bun의 번들러는 속도는 빠르지만, Webpack만큼 다양한 로더와 플러그인을 지원하지는 않습니다. 따라서 기존 대규모 프로젝트를 한 번에 전환하기보다는, 새로 만드는 마이크로서비스나 내부 도구 하나에 먼저 적용해보고 문제가 없는지 확인한 뒤 점진적으로 범위를 넓히는 접근이 안전합니다.

Node.js와의 호환성

Bun은 Node.js의 핵심 API를 상당 부분 구현하고 있습니다. fs, path, os, crypto, stream, events 같은 코어 모듈은 물론, requireimport를 모두 지원하기 때문에 대부분의 npm 패키지를 별도 수정 없이 그대로 사용할 수 있습니다.

import fs from "node:fs";
import path from "node:path";

const filePath = path.join(import.meta.dir, "data.json");
const data = fs.readFileSync(filePath, "utf-8");

다만 완벽한 호환을 전제로 하는 것은 위험합니다. 실무에서 자주 부딪히는 지점은 네이티브 애드온(C++로 작성되어 node-gyp로 빌드되는 패키지)입니다. 이런 패키지는 Node.js의 V8 바인딩에 의존하는 경우가 많아 Bun에서 동일하게 동작하지 않을 수 있습니다. 이런 상황을 만나면 무리하게 우회하기보다는, 해당 모듈을 사용하는 부분만 Node.js로 실행하거나 컨테이너 이미지를 분리하는 방식으로 대응하는 것이 현실적입니다. 마이그레이션 전에는 프로젝트의 의존성 목록을 훑어보고, 네이티브 바인딩을 사용하는 패키지가 있는지 미리 확인하는 작업을 권장합니다.

마이그레이션 시 주의할 점

기존 Node.js 프로젝트를 Bun으로 옮길 때는 다음 사항을 점검하는 것이 좋습니다.

  • 락파일 정리: package-lock.json 또는 yarn.lock을 제거하고 bun install로 새로 생성합니다. 두 종류의 락파일이 공존하면 설치 결과가 팀원마다 달라질 수 있습니다.
  • CI/CD 스크립트 수정: 빌드 서버의 Node.js 설치 단계를 Bun 설치 단계로 교체하고, npm ci 대신 bun install --frozen-lockfile을 사용합니다.
  • 환경 변수 처리 방식 확인: Bun은 .env 파일을 기본으로 읽어들이는 반면, Node.js는 dotenv 패키지를 명시적으로 불러와야 합니다. 이 차이로 인해 환경 변수 로딩 순서가 달라질 수 있습니다.
  • 네이티브 모듈 목록 점검: 위에서 설명한 대로, C++ 애드온에 의존하는 패키지가 있는지 사전에 확인합니다.
  • 점진적 도입: 핵심 서비스를 한 번에 전환하기보다, 내부 도구나 배치 스크립트처럼 리스크가 낮은 영역부터 시작해 검증 기간을 충분히 확보합니다.

프로덕션에서 Bun을 써도 될까

Bun 1.0 발표 이후 프로덕션 도입 사례는 꾸준히 늘고 있으며, 실제로 여러 기업이 일부 서비스에 Bun을 적용하고 있습니다. 그럼에도 결제, 인증, 감사 로그처럼 안정성이 최우선인 영역에서는 신중한 접근이 필요합니다. 이유는 단순합니다. 장애가 발생했을 때 참고할 수 있는 레퍼런스와 커뮤니티 자료, 스택 트레이스 분석 노하우, 그리고 채용 시장에서 구할 수 있는 엔지니어의 숫자까지 종합적으로 고려하면, 아직은 Node.js 쪽이 더 두텁습니다.

반대로 Edge 환경에서의 실험적 서비스, 내부 대시보드, 자동화 봇, CLI 도구, 프리뷰 배포 환경처럼 실패의 영향 범위가 제한적인 곳이라면 Bun을 적극적으로 시도해볼 만합니다. 도입할 때는 런타임만 갑작스럽게 교체하기보다, 로컬 환경과 스테이징 환경에서 충분한 기간 동안 운용해보고, 락파일과 설치 스크립트를 팀 전체가 동일하게 사용하도록 규칙을 정해두는 것이 안전한 접근입니다.

언제 Bun을 쓰고 언제 Node.js를 쓸까

두 런타임 중 하나를 선택할 때는 다음 기준을 참고할 수 있습니다.

상황권장 런타임
신규 사이드 프로젝트, CLI 도구Bun
CI 파이프라인의 설치·테스트 속도 개선Bun
네이티브 애드온에 크게 의존하는 서비스Node.js
결제·인증 등 안정성이 최우선인 코어 서비스Node.js
Edge 환경, 내부 대시보드, 프리뷰 배포Bun
대규모 팀, 폭넓은 커뮤니티 지원이 필요한 경우Node.js

결론

Bun이 해결해주는 문제는 단순히 “자바스크립트 실행 속도가 느리다”는 것만이 아닙니다. 더 크게 다가오는 가치는 프로젝트를 구성하는 도구의 개수를 줄여준다는 점입니다. 번들러, 테스트 러너, 패키지 매니저, 런타임을 하나의 바이너리로 통합함으로써, 팀마다 서로 다른 도구 조합을 익혀야 하는 인지 부담을 크게 줄여줍니다.

처음 시작하는 방법은 간단합니다. 설치 스크립트를 실행한 뒤 bun init으로 프로젝트를 만들고, bun add로 의존성을 추가하고, bun test로 테스트를 돌려보는 흐름을 한번 경험해보는 것을 권장합니다. 이 과정이 팀의 작업 방식과 잘 맞는다면 점진적으로 적용 범위를 넓혀가면 되고, 맞지 않는다면 언제든 Node.js로 돌아갈 수 있습니다. 중요한 것은 어느 한쪽을 맹신하지 않고, 프로젝트의 요구사항과 팀의 상황에 맞춰 실용적으로 선택하는 것입니다.

같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.