`pg`, `ioredis`, `@aws-sdk/client-s3`를 지워도 되는 이유 — Bun 네이티브 클라이언트로 스토리지 드라이버 의존성 줄이기
새 프로젝트를 시작할 때마다 npm install pg ioredis @aws-sdk/client-s3를 치고, @types/* 패키지를 맞추고, 버전 충돌을 잡다가 정작 코드는 한 줄도 못 쓰고 오후가 지나가는 경험 — 낯설지 않으실 겁니다. Bun 1.2가 SQL·S3 클라이언트를, Bun 1.3이 Redis 클라이언트를 런타임에 내장하면서 질문 하나가 생겼습니다. 데이터·스토리지 드라이버를 직접 설치할 이유가 아직 있는가?
이 글은 Node.js 백엔드를 Bun으로 옮기는 걸 검토 중인 분들을 위해 씁니다. Bun.sql, Bun.redis, Bun.s3 — 이 세 클라이언트가 실제로 어떻게 작동하는지, 어디서 쓸 만하고 어디서 발목을 잡히는지 솔직하게 다룰 예정입니다.
미리 말씀드리면, 런타임 성능보다는 드라이버 의존성 제거·콜드 스타트 단축·CI 파이프라인 단순화 쪽에서 체감 이득이 큽니다. 이 세 가지가 팀의 페인포인트라면 읽어보실 만합니다.
핵심 개념
세 클라이언트의 등장 배경
pg, ioredis, @aws-sdk/client-s3는 각각 순수 JavaScript로 동작하지만, 설치할 때 수십 개의 전이 의존성을 딸려옵니다. 버전 충돌, @types/* 패키지 정합성, 패키지별 설정 파일 — 서비스 하나 띄우기 전에 복잡도가 이미 쌓입니다. 네이티브 바이너리 빌드 문제가 심각한 건 sharp, bcrypt 류의 N-API 애드온이고, 이 부분은 아래 단점 섹션에서 따로 다룹니다.
Bun은 세 드라이버를 런타임 레벨에서 Zig으로 구현해 내장했습니다. 웹 프레임워크(Hono 등)나 ORM(Drizzle 등)은 여전히 npm으로 설치하지만, 데이터·스토리지 드라이버만큼은 npm 설치 없이 사용할 수 있게 됐습니다.
Bun.sql — 하나의 API로 PostgreSQL·MySQL·SQLite
Bun.sql은 태그드 템플릿 리터럴 문법으로 SQL을 실행하는 클라이언트입니다. Bun 1.2에서 PostgreSQL 지원으로 시작해, v1.2.21에서 MySQL/MariaDB가 추가되면서 Bun.SQL 통합 API로 진화했습니다. bun:sqlite는 그보다 앞서 존재했으니, 세 종류의 SQL DB를 동일한 문법으로 다룰 수 있게 된 셈입니다.
API는 두 가지 방식으로 씁니다.
import { sql } from "bun"—DATABASE_URL환경 변수에서 연결 설정을 읽는 모듈 싱글턴import { SQL } from "bun"— 연결 옵션을 직접 넘겨 인스턴스를 만드는 클래스
대부분의 경우 sql(소문자)을 쓰고, 테스트에서 :memory: SQLite를 프로덕션 DB와 분리해 제어하고 싶을 때처럼 연결을 명시적으로 관리할 때 SQL(대문자 클래스)을 씁니다.
import { sql } from "bun";
// 인터폴레이션은 자동으로 파라미터 바인딩됩니다 — SQL Injection 걱정 없음
const users = await sql`SELECT * FROM users WHERE id = ${userId}`;
// 트랜잭션 — sql.begin()으로 콜백 안에서 원자적 실행
await sql.begin(async (tx) => {
await tx`INSERT INTO orders ${sql(order)}`;
await tx`UPDATE inventory SET stock = stock - 1 WHERE id = ${itemId}`;
});내부적으로 자동 prepared statement, 쿼리 파이프라이닝, 커넥션 풀링, 바이너리 wire 프로토콜을 처리합니다. 바이너리 프로토콜은 텍스트 기반 프로토콜과 달리 직렬화·역직렬화 오버헤드가 작아, 고빈도 쿼리에서 성능 차이가 납니다.
태그드 템플릿이 처음엔 낯설지만, ORM보다 실제 실행 쿼리가 투명하게 보이고 동적 조건 조합 유틸리티도 내장되어 있어 금방 적응됩니다. Drizzle ORM도 Bun.sql 위에서 동작하니, 타입 안전성이 중요하다면 함께 사용하면 됩니다.
DB 종류가 달라도 코드 분기 없이 같은 문법을 씁니다. 단, FOR UPDATE 같은 행 잠금 문법은 PostgreSQL·MySQL에서만 유효하고 SQLite에서는 동작하지 않으므로, 환경 전환 전에 사용 중인 문법을 확인해두세요.
Bun.redis — 자동 파이프라이닝이 기본값
Bun.redis는 Bun 1.3에서 정식 도입된 Redis 7.2+ 및 Valkey 호환 클라이언트입니다. 개발자가 별도 설정 없이 Promise.all로 묶으면 Bun이 알아서 커맨드를 배치로 전송하는 자동 파이프라이닝이 인상적입니다.
import { RedisClient } from "bun";
const redis = new RedisClient("redis://localhost:6379");
// REDIS_URL 또는 VALKEY_URL 환경 변수가 있으면 인자 없이도 됩니다
// TTL(초) 설정
await redis.set("session:abc", JSON.stringify(data), { ex: 3600 });
const cached = await redis.get("session:abc");
// 자동 파이프라이닝 — Promise.all이면 커맨드가 한 번에 전송됩니다
const [a, b, c] = await Promise.all([
redis.get("key1"),
redis.get("key2"),
redis.get("key3"),
]);
// Pub/Sub — v1.2.23부터 실험적 지원 (아래 단점 섹션 참고)
await redis.subscribe("events", (message, channel) => {
console.log(`[${channel}]`, message);
});VALKEY_URL 환경 변수를 설정하면 Valkey에 바로 연결됩니다. Redis가 BSL 라이선스로 전환한 이후 Valkey로 이동한 팀이 많은데, 클라이언트 코드 변경 없이 URL 하나로 전환할 수 있습니다.
Bun.s3 — presigned URL이 동기 계산으로
Bun.s3는 Bun 1.2에서 나온 S3 호환 스토리지 클라이언트입니다. new Bun.S3Client()의 단축 형태로, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION, S3_BUCKET 환경 변수에서 자격증명을 자동으로 읽습니다.
const file = Bun.s3.file("uploads/avatar.png");
await file.write(imageBuffer, { type: "image/png" });
// presigned URL — 로컬 HMAC 계산만으로 동기 반환, 네트워크 요청 없음
const url = file.presign({ expiresIn: 3600 });
// Cloudflare R2 같은 S3 호환 스토리지도 동일한 API
const r2 = new Bun.S3Client({
endpoint: "https://<account>.r2.cloudflarestorage.com",
bucket: "my-bucket",
accessKeyId: process.env.R2_ACCESS_KEY_ID,
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY,
});
const data = await r2.file("report.pdf").arrayBuffer();
// Web 표준 Blob API 호환
const blob = await Bun.s3.file("data.json").blob();presigned URL 생성이 동기적인 이유는 서명 계산을 로컬 HMAC으로만 처리하기 때문입니다. @aws-sdk/client-s3는 시간 동기화를 위한 네트워크 요청이 필요한 경우가 있어, presigned URL을 고빈도로 발급하는 서비스에서 차이가 납니다.
실전 적용
시나리오 1: 세션 캐시 + DB 조회 통합 API
Hono + Bun.redis(세션 캐시) + Bun.sql(유저 데이터) 조합은 현재 Bun 생태계에서 가장 자주 보이는 패턴입니다.
import { Hono } from "hono";
import { sql } from "bun";
import { RedisClient } from "bun";
const app = new Hono();
const redis = new RedisClient(); // REDIS_URL 환경 변수 자동 참조
app.get("/user/:id", async (c) => {
const userId = c.req.param("id");
const cacheKey = `user:${userId}`;
const cached = await redis.get(cacheKey);
if (cached) {
return c.json(JSON.parse(cached));
}
const [user] = await sql`
SELECT id, name, email FROM users WHERE id = ${userId} LIMIT 1
`;
if (!user) return c.json({ error: "Not found" }, 404);
// 5분 캐싱
await redis.set(cacheKey, JSON.stringify(user), { ex: 300 });
return c.json(user);
});
export default app;시나리오 2: 파일 업로드 API (S3 + presigned URL)
Bun.s3의 기본 자격증명은 S3_BUCKET, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION 환경 변수에서 읽습니다. 이 변수들이 없으면 실행 시 에러가 납니다.
import { Hono } from "hono";
const app = new Hono();
// 서버에서 직접 업로드 받아 S3에 저장
app.post("/upload", async (c) => {
const formData = await c.req.formData();
const file = formData.get("file");
// string으로 넘어오는 경우를 방어
if (!(file instanceof File)) {
return c.json({ error: "파일이 없거나 형식이 잘못되었습니다" }, 400);
}
const key = `uploads/${Date.now()}-${file.name}`;
const s3File = Bun.s3.file(key);
await s3File.write(await file.arrayBuffer(), { type: file.type });
// 동기 계산 — 네트워크 요청 없음
const downloadUrl = s3File.presign({ expiresIn: 86400 });
return c.json({ key, downloadUrl });
});
// 클라이언트가 직접 S3에 업로드할 수 있도록 presigned PUT URL 발급
app.get("/upload-url", async (c) => {
const filename = c.req.query("filename") ?? "unknown";
const key = `uploads/${Date.now()}-${filename}`;
const uploadUrl = Bun.s3.file(key).presign({
method: "PUT",
expiresIn: 300,
type: "application/octet-stream",
});
return c.json({ key, uploadUrl });
});
export default app;시나리오 3: 트랜잭션이 필요한 주문 처리
sql.begin()으로 재고 감소와 주문 생성을 원자적으로 처리합니다. FOR UPDATE는 PostgreSQL·MySQL에서 지원하는 행 잠금 문법으로, SQLite에서는 동작하지 않습니다. 이 예시는 PostgreSQL 기준입니다.
import { sql } from "bun";
import { RedisClient } from "bun";
const redis = new RedisClient();
async function placeOrder(userId: number, itemId: number, quantity: number) {
const order = await sql.begin(async (tx) => {
const [item] = await tx`
SELECT id, stock, price FROM items WHERE id = ${itemId} FOR UPDATE
`;
if (!item || item.stock < quantity) {
throw new Error("재고 부족");
}
const [newOrder] = await tx`
INSERT INTO orders (user_id, item_id, quantity, total_price)
VALUES (${userId}, ${itemId}, ${quantity}, ${item.price * quantity})
RETURNING *
`;
await tx`
UPDATE items SET stock = stock - ${quantity} WHERE id = ${itemId}
`;
return newOrder;
});
// DB 트랜잭션 이후 Redis 연산 — 실패해도 주문 데이터에는 영향 없음 (best-effort)
await redis.incr(`order_count:${userId}`);
await redis.expire(`order_count:${userId}`, 3600);
await redis.del(`user_orders:${userId}`);
return order;
}시나리오 4: 환경별 코드 분기 없는 테스트 구성
DATABASE_URL 하나로 연결 대상을 결정할 수 있어, 표준 CRUD 쿼리를 쓰는 한 환경별 코드 분기가 필요 없습니다.
// db.ts
import { SQL } from "bun";
// "postgres://..." → PostgreSQL, "mysql://..." → MySQL, ":memory:" → SQLite
export const db = new SQL(process.env.DATABASE_URL ?? ":memory:");# 로컬 개발
DATABASE_URL=:memory: bun run dev
# 테스트 (인메모리 SQLite)
DATABASE_URL=:memory: bun test
# 프로덕션
DATABASE_URL=postgres://user:pass@host/db bun run startFOR UPDATE처럼 DB 종류에 따라 다르게 동작하는 문법은 환경 전환 시 별도 확인이 필요합니다. "코드 무변경 전환"은 표준 SELECT·INSERT·UPDATE·DELETE 범위 안에서 유효한 이야기입니다.
장단점 분석
실제로 얻는 것들
| 항목 | 내용 |
|---|---|
| 드라이버 의존성 제거 | 데이터·스토리지 드라이버 레이어에서 12개 안팎의 패키지가 제거됩니다 |
| SQL 성능 | pg/postgres.js 대비 약 50% 빠른 쿼리 처리¹ |
| Redis 성능 | ioredis 대비 약 7.9배 빠른 처리량¹ |
| S3 성능 | @aws-sdk/client-s3 대비 약 5배 빠른 처리량¹ |
| 콜드 스타트 | 10ms 미만 (Node.js 약 200ms 대비) |
| 통합 SQL API | PostgreSQL·MySQL·SQLite를 동일한 문법으로 |
| CI 단순화 | 드라이버 패키지 설치 단계가 줄어 Docker 이미지와 CI 파이프라인이 가벼워집니다 |
| Valkey 지원 | Redis BSL 라이선스 이슈로부터 자유롭습니다 |
¹ Bun 공식 벤치마크 기준, I/O 드라이버 레이어 단독 측정입니다. DB 바운드 CRUD 앱에서 end-to-end 요청 처리량 차이는 ~3% 수준에 그치는 경우도 있습니다. 성능보다 의존성 제거와 콜드 스타트 단축 쪽이 실무 체감 이득이 큰 경우가 많습니다.
단점 및 제약 사항
| 항목 | 상세 내용 |
|---|---|
| N-API 네이티브 애드온 | Bun 버전에 따라 지원 범위가 다릅니다. sharp, bcrypt 네이티브 버전 등은 현 시점 Bun 버전에서 호환 여부를 먼저 확인하세요 |
| Redis Pub/Sub 실험 단계 | v1.2.23 이후 실험적 상태 유지 중 — 프로덕션 투입은 안정화 후 권장 |
| Redis Cluster 미지원 | GitHub 이슈로 요청됐으나 아직 구현되지 않았습니다 |
| S3 멀티파트 업로드 | 대용량 파일 업로드는 별도 구현이 필요합니다 |
| APM·컴플라이언스 공백 | 일부 엔터프라이즈 APM 도구가 Bun을 아직 정식 지원하지 않습니다 |
실무에서 자주 빠지는 함정들
1. N-API 애드온 의존 서비스를 사전 확인 없이 이전하는 경우
sharp, bcrypt 같은 네이티브 애드온을 쓰고 있다면 마이그레이션 전에 현재 Bun 버전에서의 호환 여부를 반드시 확인하세요. Bun의 N-API 지원 범위는 버전마다 달라졌고 계속 개선 중이므로, 공식 호환성 문서와 릴리스 노트를 먼저 보는 게 맞습니다. 호환이 안 된다면 대안 패키지를 찾거나 해당 처리를 별도 서비스로 분리하는 방법을 검토해야 합니다.
2. Redis Cluster를 쓰고 있는데 그냥 넘어오려는 경우
Bun.redis는 현재 Redis Cluster를 지원하지 않습니다. Sentinel은 일부 동작하는 것으로 보고되지만, 클러스터 모드로 운영 중이라면 GitHub 이슈 진행 상황을 모니터링하면서 판단하세요.
3. Pub/Sub을 프로덕션에 즉시 투입하는 경우
Redis Pub/Sub이 v1.2.23부터 실험적으로 들어왔습니다. 아직 팀에서 피드백을 수집 중인 상태라, 장애 시 복구가 필요한 실시간 시스템에서는 안정화를 기다리거나 기존 ioredis와 병행 운영하는 편이 안전합니다.
4. 드라이버 벤치마크 수치를 서비스 전체 성능 개선으로 기대하는 경우
Redis 7.9배, SQL 50% 빠르다는 수치는 드라이버 레이어 단독 측정입니다. CRUD 중심 애플리케이션의 병목은 대부분 DB나 외부 API에 있으므로 end-to-end 요청 처리량 차이는 훨씬 좁아집니다. 마이그레이션의 주된 이유를 의존성 제거·콜드 스타트 단축·CI 단순화에 두는 게 현실적입니다.
마이그레이션 의사결정 흐름
Node.js 패키지 대체 매핑
| Node.js 패키지 | Bun 네이티브 대체 |
|---|---|
pg, postgres.js |
Bun.sql (PostgreSQL) |
mysql2 |
Bun.sql (MySQL) |
better-sqlite3 |
bun:sqlite |
ioredis, node-redis |
Bun.redis (RedisClient) |
@aws-sdk/client-s3 |
Bun.s3, Bun.S3Client |
dotenv |
내장 (Bun이 .env 자동 로드) |
ts-node, nodemon |
내장 (Bun이 TS 직접 실행, --watch 기본 제공) |
마치며
Bun.sql, Bun.redis, Bun.s3는 각각 pg/mysql2, ioredis, @aws-sdk/client-s3를 대체하는 런타임 내장 드라이버로, 드라이버 의존성 제거·콜드 스타트 단축·CI 단순화 측면에서 명확한 이득이 있습니다. 다만 N-API 네이티브 애드온 호환성, Redis Cluster 미지원, Pub/Sub 실험 단계, 대용량 멀티파트 업로드는 현 시점에서 확인이 필요한 부분입니다.
지금 바로 시작해볼 수 있는 3단계입니다.
-
N-API 의존성 인벤토리 먼저.
package.json에서sharp,bcrypt,canvas같은 네이티브 애드온을 찾아보고 현 Bun 버전 호환성을 확인합니다. 이게 없거나 호환된다면 마이그레이션 블로커가 없다는 의미입니다. -
신규 프로젝트 하나로 먼저 검증. 기존 서비스를 통째로 이전하기보다, 새로 만드는 API 서버 하나를 Bun + Hono + Bun.sql/Bun.redis로 구성해보세요. 패키지 설치 없이 DB와 캐시가 연결되는 경험에서 감이 잡힙니다.
-
bun run --watch와bun test를 일단 써보기.dotenv도ts-node도nodemon도 필요 없어지는 순간, "배터리 포함 런타임"이 무엇인지 체감됩니다.
참고 자료
- SQL - Bun 공식 문서
- Redis - Bun 공식 문서
- S3 - Bun 공식 문서
- Bun 1.3 공식 블로그
- Bun 1.2 Improves Node Compatibility and Adds Postgres Client — InfoQ
- Bun adds Bun.SQL — a zero-dependency unified SQL client — Progosling
- Bun.sql vs postgres.js vs Drizzle: Postgres in 2026 — PkgPulse Guides
- Bun 1.2 Deep Dive: Built-in SQLite, S3, and Why It Might Actually Replace Node.js — DEV Community
- Bun's Built-in Redis Client: Fast, Simple, Production-Ready — bunjs.run
- Bun v1.2.23 릴리스 노트
- Bun vs Node.js in 2026: Benchmarks & Migration Guide — Strapi
- The Case for Bun in 2026: Where It Works and Where It Doesn't — Medium
- Redis/Valkey Cluster Support Issue — GitHub oven-sh/bun