기본 패턴
// node:sqlite — Node.js 22.5+, --experimental-sqlite 플래그 필요
import { DatabaseSync } from "node:sqlite";
// 파일 DB 열기 (없으면 자동 생성)
const db = new DatabaseSync("./data.db");
// 테이블 생성
db.exec(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL
)
`);
// 구문 준비 → 바인딩 → 실행
const insert = db.prepare("INSERT INTO users (name, email) VALUES (?, ?)");
insert.run("김철수", "kim@example.com");
// 조회
const selectAll = db.prepare("SELECT * FROM users WHERE name = ?");
const rows = selectAll.all("김철수");
console.log(rows); // [{ id: 1, name: '김철수', email: 'kim@example.com' }]
db.close();설명
node:sqlite는 외부 의존성 없이 경량 데이터베이스를 제공한다
기존에 Node.js에서 SQLite를 사용하려면 better-sqlite3이나 node-sqlite3 같은 네이티브 애드온 패키지를 설치해야 했다. 이는 빌드 툴체인(node-gyp, Python) 의존성과 플랫폼별 바이너리 배포 문제를 수반했다. Node.js 22.5에서 도입된 node:sqlite는 런타임에 SQLite 라이브러리를 내장해 npm install 없이 바로 사용할 수 있다. CLI 도구, 설정 저장, 오프라인 캐시 등 외부 DB 서버가 불필요한 시나리오에 특히 적합하다.
# Node.js 22.5+ 필요, 현재 실험적 플래그 필요
node --experimental-sqlite app.js
# 또는 NODE_OPTIONS로 전역 적용
NODE_OPTIONS="--experimental-sqlite" node app.js// package.json scripts에서 플래그를 항상 포함하는 방법
// {
// "scripts": {
// "start": "node --experimental-sqlite src/index.js"
// }
// }
import { DatabaseSync } from "node:sqlite";
const db = new DatabaseSync(":memory:"); // 메모리 DB
console.log("SQLite 버전:", db.prepare("SELECT sqlite_version()").get());동기 API로 설계된 이유 — SQLite 자체가 파일 I/O 기반이라 비동기 이점이 제한적이다
node:sqlite의 모든 API(exec, prepare, run, all, get)는 동기(synchronous)다. SQLite는 단일 파일에 순차적으로 쓰는 구조이므로 여러 쿼리를 동시에 실행하는 이점이 거의 없다. 비동기 래퍼를 추가하면 콜백 오버헤드만 생기고 처리량은 늘지 않는다. 서버 요청 핸들러처럼 이벤트 루프를 막으면 안 되는 컨텍스트에서는 worker_threads 안에서 실행하는 것이 권장 패턴이다.
import { Worker, isMainThread, parentPort, workerData } from "node:worker_threads";
import { DatabaseSync } from "node:sqlite";
// 메인 스레드: 쿼리를 worker에 위임
if (isMainThread) {
const worker = new Worker(new URL(import.meta.url), {
workerData: { sql: "SELECT COUNT(*) AS cnt FROM users" },
});
worker.on("message", (result) => console.log("결과:", result));
} else {
// Worker 스레드: 동기 SQLite 쿼리 (이벤트 루프 블로킹 없음)
const db = new DatabaseSync("./data.db");
const row = db.prepare(workerData.sql).get();
parentPort.postMessage(row);
db.close();
}:memory: DB로 테스트·임시 저장소, 파일 DB로 영속 저장
SQLite는 파일 경로 대신 ":memory:"를 지정하면 프로세스 메모리에만 존재하는 임시 DB를 생성한다. 프로세스 종료 시 모든 데이터가 사라지므로 단위 테스트, 빌드 타임 데이터 변환, 요청 단위 임시 집계에 유용하다. 영속이 필요하면 파일 경로를 지정하고 주기적으로 WAL(Write-Ahead Logging) 모드를 활성화해 동시 읽기 성능을 높인다.
import { DatabaseSync } from "node:sqlite";
// 단위 테스트용 인메모리 DB — 매 테스트마다 새로 생성
function createTestDb() {
const db = new DatabaseSync(":memory:");
db.exec(`
CREATE TABLE products (id INTEGER PRIMARY KEY, name TEXT, price REAL);
INSERT INTO products VALUES (1, '사과', 1500);
INSERT INTO products VALUES (2, '배', 2500);
`);
return db;
}
// 파일 DB — WAL 모드로 읽기 성능 향상
const prodDb = new DatabaseSync("./store.db");
prodDb.exec("PRAGMA journal_mode = WAL"); // 동시 읽기 허용
prodDb.exec("PRAGMA synchronous = NORMAL"); // 안전성·속도 균형
// 테스트
const testDb = createTestDb();
const rows = testDb.prepare("SELECT * FROM products WHERE price > ?").all(2000);
console.log(rows); // [{ id: 2, name: '배', price: 2500 }]
testDb.close();Statement 준비(prepare)와 바인딩으로 SQL 인젝션 방지 및 반복 쿼리 최적화
문자열 템플릿으로 SQL을 조합하면 SQL 인젝션에 취약하다. db.prepare()는 SQL 구문을 미리 파싱·컴파일해 바인딩 파라미터(? 또는 :name)를 분리한다. 같은 구문을 반복 실행할 때도 파싱 비용이 첫 번째 한 번만 발생하므로 루프 내 쿼리 성능이 향상된다.
import { DatabaseSync } from "node:sqlite";
const db = new DatabaseSync(":memory:");
db.exec("CREATE TABLE logs (id INTEGER PRIMARY KEY, msg TEXT, ts INTEGER)");
// ❌ 위험: 문자열 조합 — SQL 인젝션 가능
// db.exec(`INSERT INTO logs VALUES (NULL, '${userInput}', ${Date.now()})`);
// ✅ 안전: prepare + 바인딩 파라미터
const insertLog = db.prepare("INSERT INTO logs (msg, ts) VALUES (?, ?)");
// 루프에서 prepare 재사용 — 파싱 비용은 처음 한 번만
const messages = ["시작", "진행 중", "완료"];
for (const msg of messages) {
insertLog.run(msg, Date.now());
}
// 이름 있는 파라미터 (가독성↑)
const findLog = db.prepare("SELECT * FROM logs WHERE msg = :msg");
const row = findLog.get({ msg: "완료" });
console.log(row); // { id: 3, msg: '완료', ts: ... }
db.close();빠른 정리
| 상황 | 적합한 선택 |
|---|---|
| 외부 DB 서버 없이 로컬 저장 | node:sqlite (Node 22.5+) |
| 단위 테스트용 임시 DB | new DatabaseSync(":memory:") |
| 영속 파일 DB + 읽기 성능 향상 | 파일 경로 + PRAGMA journal_mode = WAL |
| SQL 인젝션 방지 | db.prepare() + 바인딩 파라미터 |
| 이벤트 루프 블로킹 방지 | worker_threads 안에서 SQLite 실행 |
| 프로덕션 critical path 사용 | Node 버전 안정성 확인 후 적용 |
주의할 점
node:sqlite는 Node.js 22 기준 실험적(Experimental) API다. --experimental-sqlite 플래그 없이 임포트하면 ERR_UNKNOWN_BUILTIN_MODULE 오류가 발생하며, 프로덕션 critical path에 도입하기 전에 해당 Node.js 버전의 안정화 상태를 반드시 확인해야 한다.
// ❌ 오류: 플래그 없이 실행 시
// node app.js
// → Error [ERR_UNKNOWN_BUILTIN_MODULE]: No such built-in module: node:sqlite
// ✅ 올바른 실행
// node --experimental-sqlite app.js
// ✅ 런타임에 지원 여부 확인 후 조건부 사용
let DatabaseSync;
try {
({ DatabaseSync } = await import("node:sqlite"));
} catch {
console.warn("node:sqlite 미지원 환경 — 대체 라이브러리 사용");
// 폴백: better-sqlite3 등
}