Bun용 타입‑안전 SQL 라이브러리 제작 — ORM·코드젠 없이 순수 SQL (Claude Code 사용)

발행: (2026년 5월 22일 PM 06:26 GMT+9)
7 분 소요
원문: Dev.to

출처: Dev.to

Bun을 사용하면서 계속 마주친 문제가 있습니다. 대부분의 SQL 라이브러리는 Node.js 내부를 요구하거나, 원하지 않는 ORM 추상화에 크게 의존하거나, 스키마 파일로부터 빌드 시점에 타입을 생성합니다.
그래서 저는 squn을 만들었습니다 — Bun의 내장 데이터베이스 클라이언트와 네이티브하게 동작하는 가볍고 타입‑안전한 SQL 쿼리 라이브러리입니다.

모든 쿼리는 sql이라는 태그드 템플릿 리터럴을 통해 실행됩니다. 삽입된 값은 항상 바인드 파라미터가 되며, 문자열에 직접 연결되지 않습니다. 설계상 SQL 인젝션은 불가능합니다.

import { createDb, PostgresAdapter, sql } from "@phonemyatt/squn";

const db = createDb(new PostgresAdapter({
  url: "postgresql://user:password@localhost:5432/mydb",
}));

interface User {
  id: number;
  name: string;
  age: number | null;
}

// 값은 $1, $2 파라미터가 됩니다 — 문자열 연결 절대 없음
const users = await db.query(sql`SELECT * FROM users WHERE age > ${18}`);

스키마 파일도 없고, 코드 생성 단계도 없으며, 빌드 타임 매직도 없습니다. SQL을 작성하고, 타입이 지정된 결과를 받아 쓰면 됩니다.

squn은 Bun에서 사용할 가능성이 높은 네 가지 데이터베이스를 모두 지원합니다:

데이터베이스드라이버
SQLitebun:sqlite (내장)
PostgreSQLBun의 네이티브 Postgres
MySQLBun의 네이티브 MySQL
MSSQLmssql npm 패키지

같은 쿼리 코드를 네 데이터베이스 모두에서 사용할 수 있으며, 어댑터만 교체하면 됩니다.

// 어댑터를 바꿔서 데이터베이스 전환
const db = createDb(new SqliteAdapter({ filename: ":memory:" }));
const db = createDb(new PostgresAdapter({ url: process.env.PG_URL }));
const db = createDb(new MysqlAdapter({ url: process.env.MYSQL_URL }));
const db = createDb(new MssqlAdapter({ host: "localhost", ... }));

// 모든 행
const users = await db.query(sql`SELECT * FROM users`);

// 첫 번째 행 또는 null — 예외 발생 안 함
const user = await db.queryFirst(sql`SELECT * FROM users WHERE id = ${1}`);

// 정확히 한 행 — 0개 혹은 2개 이상이면 예외 발생
const user = await db.querySingle(sql`SELECT * FROM users WHERE id = ${1}`);

// 스칼라값 — 첫 행 첫 열
const count = await db.queryScalar(sql`SELECT COUNT(*) FROM users`);

조각(fragment) 조합

조각을 중첩해서 사용하면 자동으로 인라인 병합되고, 파라미터 번호도 재배열됩니다.

const minAge = 18;
const activeOnly = true;

const conditions = [
  sqlIf(minAge !== undefined, sql`age >= ${minAge}`),
  sqlIf(activeOnly, sql`active = ${true}`),
];

const where = sqlJoin(conditions, " AND ");
const q = sql`SELECT * FROM users WHERE ${where} ORDER BY name`;
// → SELECT * FROM users WHERE age >= $1 AND active = $2 ORDER BY name
// params → [18, true]

문자열 연결이 없고, 인젝션 위험도 없습니다. 완전한 조합성을 제공합니다.

원자적 트랜잭션

atomically는 콜백을 BEGIN/COMMIT으로 감싸며, 오류 발생 시 자동으로 롤백합니다.

await db.atomically(async (q) => {
  await q.execute(sql`UPDATE accounts SET balance = balance - ${100} WHERE id = ${from}`);
  await q.execute(sql`UPDATE accounts SET balance = balance + ${100} WHERE id = ${to}`);
  // 어느 하나라도 예외가 발생하면 두 업데이트 모두 롤백됩니다
});

트랜잭션은 Symbol.asyncDispose를 구현하므로 await using 구문으로 사용하면 자동 정리가 보장됩니다.

await using tx = new Transaction(await adapter.beginTransaction());
await tx.execute(sql`UPDATE users SET active = ${false} WHERE id = ${42}`);
await tx.commit();
// commit이 예외를 던지거나 조기에 반환하면 자동으로 롤백됩니다

배치 삽입

await db.executeBatch(
  sql`INSERT INTO users (name, age) VALUES (@name, @age)`,
  [
    { name: "Alice", age: 30 },
    { name: "Bob",   age: 25 },
    { name: "Carol", age: 35 },
  ],
);

하나의 준비된 문을 사용해 루프 안에서 모든 행을 바인드합니다. 개별 삽입보다 훨씬 빠릅니다.

테이블 스키마 정의와 자동 타입 추론

import { col, defineTable, InferSelect, InferInsert } from "@phonemyatt/squn";

const Users = defineTable({
  id:   col("integer").primaryKey().notNull(),
  name: col("text").notNull(),
  age:  col("integer").nullable(),
});

type UserRow    = InferSelect;  // { id: number; name: string; age: number | null }
type UserInsert = InferInsert;  // { name: string; age?: number | null }
const db = createConnections({
  connections: {
    primary: new PostgresAdapter({ url: process.env.PRIMARY }),
    replica: new PostgresAdapter({ url: process.env.REPLICA }),
  },
  default: "primary",
});

// 읽기 요청을 replica로 라우팅
const users = await db.query(sql`SELECT * FROM users`, { connection: "replica" });

// 스코프 헬퍼 — 매 호출마다 connection 옵션 필요 없음
const replica = db.use("replica");

// 타입이 지정된 동시 쿼리
const [users, roles] = await db.concurrent(
  db.query(sql`SELECT * FROM users`),
  db.query(sql`SELECT * FROM roles`),
);
bun add @phonemyatt/squn

피드백을 환영합니다 — 특히 MySQL이나 MSSQL을 프로덕션 환경에서 사용하고 계신 분들의 의견을 기다립니다.

TypeScript 5.9 strict mode, any 제로, Docker 환경에서 실제 데이터베이스를 대상으로 테스트되었습니다.

0 조회
Back to Blog

관련 글

더 보기 »

내 스킬

프로젝트를 위한 AI 지시문을 만들고, 설치하고, 관리하세요 — 코딩이 필요 없습니다. CREATE 이름을 정하고, 카테고리를 선택하고, 원하는 것을 설명하세요 — 마법사가 자동으로 구성합니다.