- Published on
SQLite 르네상스 2026 — 24살 단일 파일 DB가 어떻게 가장 핫한 인프라가 되었나 (libSQL·Turso·LiteFS·D1·mvSQLite 심층 분석)
- Authors

- Name
- Youngju Kim
- @fjvbn20031
프롤로그 — 24살 데이터베이스가 왜 다시 핫해졌나
2026년의 인프라 스택을 보면 묘한 장면이 펼쳐진다. 한쪽에서는 Postgres 18이 멀티마스터 논리 복제를 다듬고, 다른 한쪽에서는 분산 NewSQL이 글로벌 일관성을 약속한다. 그런데 가장 시끄러운 트렌드는 그 어느 쪽도 아니다 — 2000년 D. Richard Hipp이 한 파일짜리 임베디드 라이브러리로 시작한 SQLite가 다시 무대 한복판에 섰다.
SQLite는 데이터베이스가 아니다. 라이브러리다. 그게 이 모든 부활의 비밀이다.
이상하게 들리지만 다시 생각해 보자. 2026년 인프라의 두 가지 거시적 압력은 (a) 엣지로 가는 컴퓨트와 (b) 로컬 우선 UX다. 둘 다 답이 같다 — "네트워크 라운드트립을 없애라". 그리고 네트워크를 없애려면 데이터가 코드 옆에 있어야 한다. 코드 옆에 있는 데이터베이스는 무엇인가? 로컬 파일. SQLite는 처음부터 로컬 파일이었다.
이 글은 24살 먹은 단일 파일 DB가 어떻게 2026년의 가장 흥미로운 인프라 카테고리로 부활했는지 해부한다. 정확히 무엇이 새로운지 — libSQL 포크와 Turso의 임베디드 레플리카, Cloudflare D1, 이제는 일몰된 LiteFS, FoundationDB 기반 mvSQLite, Litestream 백업 — 그리고 SQLite를 "엣지 데이터베이스"로 만든 토폴로지가 무엇인지. 그리고 정직하게 말한다 — SQLite가 옳은 선택일 때와 절대 틀린 선택일 때.
1장 · SQLite-at-the-edge 테제 — 왜 지금인가
먼저 명확히 하자. SQLite는 새롭지 않다. 1.0이 2000년에 나왔다. 모든 안드로이드, iOS, macOS, 모든 브라우저, 거의 모든 IoT 기기에 들어 있다. 세계에서 가장 많이 배포된 데이터베이스다. 그런데 왜 2026년에 갑자기 "트렌드"가 되었나?
세 가지가 동시에 일어났다.
첫째, 엣지 컴퓨트가 메인스트림이 됐다. Cloudflare Workers, Deno Deploy, Vercel Edge Functions, Fly.io — 코드는 사용자 가까이로 갔다. 그런데 데이터베이스는? 여전히 us-east-1에 있다. 서울 사용자가 도쿄 엣지에서 실행되는 함수를 호출했는데, 그 함수가 us-east-1의 Postgres로 200ms 라운드트립을 하면, 엣지의 의미가 사라진다.
둘째, 로컬 우선(local-first) 운동이 형태를 갖췄다. Ink & Switch의 2019년 에세이로 시작된 흐름이 2026년에는 실제 제품들 — Linear, Figma, Notion의 일부 — 로 구체화됐다. "오프라인에서도 동작하고, 즉각적이고, 내 데이터는 내 기기에" 라는 약속. 이를 위해서는 데이터가 클라이언트 안에 있어야 한다. 클라이언트에 들어가는 임베디드 SQL DB는? SQLite다.
셋째, "스케일 아웃이 답"이라는 신화가 흔들렸다. 2020년대 초반에는 모든 회사가 "분산 분산 분산"을 외쳤지만, 2026년에 모두가 깨달은 것은 — 대부분의 앱은 분산 일관성이 필요 없다. 하나의 노드에 다 들어간다. 그리고 단일 노드 데이터베이스는 운영이 거짓말처럼 단순하다.
이 세 가지의 교집합이 바로 SQLite다. 로컬 파일이고, 임베디드이고, 작고, 신뢰할 수 있다. 거기에 2020년대 중반의 새 도구들이 SQLite의 약점 — 분산, 복제, 백업 — 을 메우기 시작한 것이 이 르네상스의 본질이다.
2장 · libSQL과 Turso — "임베디드 레플리카"라는 발명
이 부활의 가장 영향력 있는 한 가지를 골라야 한다면 Turso의 임베디드 레플리카(embedded replica) 다.
libSQL — SQLite의 오픈 포크
SQLite 자체는 오픈소스지만 외부 컨트리뷰션을 받지 않는다. SQLite 팀은 "퍼블릭 도메인이지만 우리 코드는 우리가 쓴다"는 입장이다. 이게 갈증을 만들었다 — 사람들은 SQLite에 새 기능 (스토리지 백엔드 교체, 네이티브 복제, 더 나은 동시성) 을 붙이고 싶었지만 길이 없었다.
그래서 2022~2023년경 Turso(당시 ChiselStrike)가 libSQL을 만들었다. SQLite의 진짜 포크다. MIT 라이선스, 외부 컨트리뷰션 받음, 그리고 가장 중요하게 — 네이티브 네트워크 프로토콜(HTTP, WebSocket via "Hrana"), 사용자 정의 함수, 그리고 임베디드 레플리카를 추가했다.
임베디드 레플리카 — 모델 자체가 새롭다
전통적인 모델: 앱 ↔ 네트워크 ↔ DB. 모든 쿼리가 라운드트립.
Turso의 임베디드 레플리카: 앱 안에 SQLite 파일이 있다. 읽기는 그 로컬 파일에서 일어난다 — 마이크로초 단위. 쓰기는 원격 마스터로 보내지고, 마스터가 변경분(WAL 프레임)을 백그라운드로 푸시해 로컬 파일을 동기화한다.
┌────────────────────────┐ ┌──────────────────┐
│ 앱 (예: 엣지) │ │ Turso 마스터 │
│ ┌──────────────────┐ │ 읽기: 로컬 디스크 │ (글로벌 1차) │
│ │ libSQL 클라이언트 │──┘ < 1ms │ │
│ │ + 로컬 .db 파일 │ │ │
│ └────────┬─────────┘ │ │
│ │ 쓰기 (HTTP/Hrana) ─────────────▶│ │
│ │ ◀── WAL 프레임 (백그라운드 sync) │ │
└────────────────────────┘ └──────────────────┘
이게 뭐가 그렇게 새로운가? 읽기와 쓰기의 비대칭성을 처음으로 정직하게 받아들였다. 대부분의 웹 앱은 99% 읽기다. 읽기를 1ms 이하로 만들 수 있다면, 쓰기는 라운드트립을 감수해도 된다. 그런데 기존의 "리드 레플리카" 모델은 항상 네트워크 너머에 있었다. Turso의 임베디드 레플리카는 그 레플리카를 앱 프로세스 안의 디스크 파일로 가져왔다.
코드: Turso 임베디드 레플리카 셋업
// pnpm add @libsql/client
import { createClient } from '@libsql/client'
const db = createClient({
// 로컬 SQLite 파일 (디스크에 진짜로 존재함)
url: 'file:local.db',
// 원격 1차 — 쓰기는 여기로
syncUrl: 'libsql://my-db-myteam.turso.io',
authToken: process.env.TURSO_AUTH_TOKEN,
// 백그라운드 동기화 주기
syncInterval: 60, // 초
})
// 첫 sync — 원격 상태를 로컬에 복제
await db.sync()
// 읽기 — 로컬 파일에서 곧장. 네트워크 없음.
const rows = await db.execute('SELECT * FROM posts WHERE published = 1')
// 쓰기 — 원격으로 가고, 다음 sync에 로컬도 따라잡음
await db.execute({
sql: 'INSERT INTO posts (title, body) VALUES (?, ?)',
args: ['Hello', 'World'],
})
// 수동 동기화도 가능
await db.sync()
이 모델의 의미는 큰데, 가장 큰 것 — 읽기는 로컬 파일 IO만큼 빠르다. SSD에서 SQLite 인덱스 룩업은 마이크로초 단위다. 어떤 분산 DB의 어떤 캐시도 이걸 못 이긴다. 트레이드오프는 일관성 이다. 로컬 레플리카는 마지막 sync 시점까지의 데이터만 본다. 쓰기 직후의 자기 데이터를 즉시 봐야 한다면 read-your-writes 모드를 켜야 하고 (그 경우 그 한 쿼리만 원격으로 가서 라운드트립 발생), 강한 일관성이 필요한 워크로드는 그냥 원격 모드를 써야 한다.
Turso 플랫폼 자체
Turso는 그 위에 매니지드 서비스를 얹었다. 무료 티어가 후하고 (수억 행 무료), 데이터베이스 브랜치 가 있고 (Git 브랜치처럼 DB를 분기), 멀티 리전 1차도 가능하다. 2024년 이후로 Turso의 큰 베팅은 per-tenant 데이터베이스 다 — 사용자 한 명당 DB 한 개. SQLite는 가볍기 때문에 수십만 개의 작은 DB가 가능하다. 멀티테넌시의 새 모델이다.
3장 · LiteFS — Fly의 분산 SQLite 실험과 그 일몰
Fly.io는 2022년에 다른 답을 시도했다 — LiteFS, FUSE 파일시스템 레이어다.
LiteFS의 아이디어
평범한 SQLite 파일을 FUSE 마운트 위에 두면, LiteFS가 그 파일에 일어나는 모든 트랜잭션(WAL 프레임)을 가로채서 다른 노드에 복제한다. 앱은 자기가 분산 환경에 있는지조차 모른다 — 그냥 SQLite 파일을 연다. 1개 노드가 "1차"이고 모든 쓰기는 1차로 라우팅된다(LiteFS Proxy가 자동으로 처리). 다른 노드들은 거의 실시간으로 복제본을 받는다.
가장 큰 매력은 앱 코드 변경이 없다는 점 이었다. Rails, Django, Phoenix — 어떤 프레임워크든 SQLite를 열 수만 있으면 LiteFS 위에서 분산 모드로 동작했다.
일몰
그런데 2024년 중반, Fly는 LiteFS 개발이 사실상 일시 정지 됐다고 공지했고, 2025년에는 새 사용자에게 더 이상 권장하지 않는다는 신호가 강해졌다(공식 "유지보수 모드(maintenance mode)" 라는 표현이 자주 인용된다). 기술적으로는 FUSE 자체의 복잡성, 멀티 1차의 어려움, 그리고 운영 부담 이 원인으로 거론됐다. Fly의 매니지드 SQLite 비전은 다른 방향(예: Fly Postgres 강화, Tigris 같은 외부 파트너십)으로 이동했다.
그래도 배울 점
LiteFS의 일몰이 아이디어의 실패는 아니다. LiteFS는 "SQLite를 어떻게 분산시킬 것인가" 에 대한 가장 깔끔한 답 중 하나였다. 앱 투명성, 단방향 1차, WAL-레벨 복제. 비록 매니지드 형태로는 살아남지 못했지만, libSQL과 D1이 그 디자인 어휘 — 1차 + 비동기 복제, WAL 프레임 단위 동기화 — 의 일부를 이어받았다.
오늘 분산 SQLite를 새로 시작한다면 LiteFS를 권하지 않는다. 그러나 그 디자인 문서들은 여전히 읽을 가치가 있다.
4장 · Cloudflare D1 — 엣지 SQLite의 매니지드 답
Cloudflare가 던진 답은 다르다. D1은 SQLite 자체를 Workers 런타임 근처에 가져다 놓는다.
D1의 모델
- 저장 단위: SQLite 데이터베이스.
- 호스팅: Cloudflare의 글로벌 네트워크 위, 일종의 1차 리전 + 자동 리드 레플리카로 동작.
- 접근: Workers 바인딩 (
env.DB.prepare(...))을 통해서. HTTP API도 있지만 주된 모델은 Workers 내부에서의 호출. - 상태: 2026년 기준으로 GA 상태이며, "Global Read Replication" 옵션을 통해 읽기 레플리카가 여러 대륙에 자동 배치된다. 데이터베이스당 크기 한도가 한 자릿수 GB(현재 10GB대) 수준이라 거대 단일 DB보다는 per-application 혹은 per-tenant 모델에 맞는다.
코드 예시: Workers + D1
// wrangler.toml에 D1 바인딩이 있다고 가정
// [[d1_databases]]
// binding = "DB"
// database_name = "blog"
// database_id = "..."
export interface Env {
DB: D1Database
}
export default {
async fetch(req: Request, env: Env): Promise<Response> {
const url = new URL(req.url)
const slug = url.pathname.slice(1)
// prepare + bind — SQL injection 안전
const stmt = env.DB.prepare(
'SELECT title, body, published_at FROM posts WHERE slug = ?1 LIMIT 1'
).bind(slug)
const row = await stmt.first<{ title: string; body: string; published_at: string }>()
if (!row) return new Response('Not found', { status: 404 })
return Response.json(row)
},
}
핵심은 env.DB가 네트워크 핸들이 아니라 런타임이 주입한 바인딩 이라는 점이다. Workers는 D1과 같은 인프라 위에서 돌고, Cloudflare가 라우팅·풀링·복제를 다 숨긴다. 개발자 입장에서는 그냥 "엣지 함수 안에 SQL이 있다".
트레이드오프
- 쓰기 일관성: 단일 1차에 직렬화. 다중 리전 쓰기는 없다. 글로벌 일관성을 약속하지 않는다.
- 트랜잭션: 인터랙티브 트랜잭션은 제한적. 배치 API 가 권장된다 — 여러 statement를 한 번에 묶어 보낸다.
- 크기: DB 1개당 한 자릿수 GB. 더 큰 데이터는 R2 + D1 메타데이터 패턴으로 분할.
- lock-in: D1은 Workers 생태계 내부에서 가장 빛난다. 다른 곳에서는 매력이 줄어든다.
D1의 진짜 가치는 "Workers를 쓴다면 거의 마찰 없이 영속성을 얻는다" 는 데 있다. SQLite-at-the-edge의 가장 통합적인 구현이다.
5장 · 분산 SQLite의 다른 길 — mvSQLite와 rqlite
위의 셋(libSQL/Turso, LiteFS, D1)이 메인스트림이지만, 더 흥미로운 실험들이 있다.
mvSQLite — FoundationDB로 백킹
mvSQLite는 SQLite의 스토리지 엔진(VFS 레이어)을 통째로 갈아끼웠다. 데이터는 로컬 디스크가 아니라 FoundationDB 에 저장된다. FoundationDB는 Apple이 쓰는 분산 트랜잭셔널 키밸류 스토어로, 직렬화 가능 트랜잭션과 수평 확장을 제공한다.
결과: SQL 인터페이스는 SQLite 그대로, 내부 스토리지는 분산 트랜잭셔널 KV. 단일 DB 크기 한계가 사라지고, 멀티 노드가 같은 DB를 동시에 읽고 쓴다. 트레이드오프는 운영 — FoundationDB 자체가 운영이 만만하지 않다.
mvSQLite는 프로덕션 채택이 활발하다고 보기는 어렵지만, "SQLite의 API를 유지하면서 스토리지만 분산화한다" 는 디자인 어휘는 강력하다.
rqlite — Raft 위의 SQLite
rqlite는 SQLite 인스턴스 여러 개 앞에 Raft 합의 알고리즘 을 두고 강한 일관성을 만든 시스템이다. 모든 쓰기는 Raft 리더로 가고, 로그가 복제된 뒤에 각 노드의 SQLite에 적용된다.
특징:
- CP 시스템 (CAP에서 일관성 우선).
- 클러스터 크기는 보통 3~7대.
- 매니지드 서비스가 아닌 자가 운영 이 기본.
- HTTP API로 노출.
rqlite는 큰 회사보다는 엣지 디바이스(IoT, 산업용 게이트웨이)·소규모 클러스터·임베디드 분산 시나리오 에서 빛난다. "복잡한 분산 DB를 운영하기엔 작지만, SQLite의 단일 노드는 위험한" 케이스의 정답이다.
비교가 중요하다 — 다 같지 않다
| 시스템 | 1차 디자인 축 | 쓰기 토폴로지 | 일관성 |
|---|---|---|---|
| libSQL/Turso | 임베디드 레플리카 + 매니지드 | 단일 1차, 비동기 복제 | 결과적 (read-your-writes 옵션) |
| LiteFS | FUSE 투명 복제 (일몰) | 단일 1차, 비동기 복제 | 결과적 |
| Cloudflare D1 | 엣지 매니지드 + 읽기 레플리카 | 단일 1차, 자동 리드 레플리카 | 결과적 (읽기), 직렬화 (쓰기) |
| mvSQLite | FDB 백킹 분산 SQL | 멀티 라이터, FDB 합의 | 직렬화 가능 (FDB 보장) |
| rqlite | Raft 위의 SQLite | 리더 쓰기 | 선형화 (Raft 보장) |
| 평범한 SQLite + Litestream | 단일 노드 + S3 백업 | 단일 라이터 | (백업/복구만) |
이 표가 핵심이다 — "SQLite 기반"이라는 한 단어 안에 매우 다른 디자인이 들어 있다. 무엇을 풀고 싶은지 가 선택을 결정한다.
6장 · Litestream — 모두의 기반이 된 지속 백업
분산화를 원하지 않는다면? 그래도 백업은 필요하다. Litestream이 그 자리를 차지했다.
무엇을 하는가
Litestream은 SQLite의 WAL(Write-Ahead Log)을 거의 실시간으로 S3(또는 호환 스토리지)에 스트리밍 한다. 사이드카 프로세스로 돈다. 앱은 자기가 백업되고 있는지 모른다.
# 한 줄 셋업
litestream replicate /var/lib/app/data.db s3://my-bucket/backups/data.db
# 복구
litestream restore -o /var/lib/app/data.db s3://my-bucket/backups/data.db
복구는 PITR(Point-in-Time Recovery)이다 — 임의의 과거 시점으로 되돌릴 수 있다. RPO(복구 시점 목표)는 보통 초 단위.
왜 중요한가
Litestream은 "단일 노드 SQLite는 위험하다"는 마지막 정당화 를 깼다. 디스크가 죽어도, 인스턴스가 사라져도, 몇 분 안에 S3에서 복원된다. 운영 모델은 충격적일 만큼 단순하다 — 앱 1개, SQLite 파일 1개, Litestream 사이드카 1개. 끝.
LiteFS 일몰 이후 Fly와 다른 PaaS들이 Litestream을 더 추천하는 흐름이 강해졌다. "복잡한 분산 SQLite보다, 단일 노드 + Litestream이 90% 케이스에 더 낫다"는 입장이다.
저자(Ben Johnson, Fly.io)는 이후 LiteFS도 만든 사람인데, 본인이 직접 "대부분의 앱에는 Litestream이면 충분하다"고 자주 말한다. 통찰이다.
7장 · Bun SQLite + 네이티브 런타임 SQLite — 또 다른 부활의 축
런타임 레벨에서도 변화가 있다.
Bun의 bun:sqlite
Bun은 처음부터 bun:sqlite를 내장 모듈로 제공한다. 동기 API, 매우 빠르고, 의존성 0개.
import { Database } from 'bun:sqlite'
const db = new Database('app.db', { create: true })
db.exec(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY,
email TEXT UNIQUE,
created_at INTEGER DEFAULT (strftime('%s', 'now'))
)
`)
// Prepared statement — N번 호출에도 한 번만 컴파일
const insert = db.prepare('INSERT INTO users (email) VALUES (?)')
const tx = db.transaction((emails: string[]) => {
for (const e of emails) insert.run(e)
})
tx(['a@x.com', 'b@x.com', 'c@x.com'])
// Query
const all = db.query('SELECT id, email FROM users').all()
console.log(all)
이게 왜 새로운가? Node.js 진영에서는 SQLite를 쓰려면 외부 패키지(better-sqlite3, node-sqlite3)가 필요했다. Node.js 22+ 이후로는 실험적 node:sqlite가 들어왔다. 런타임이 SQLite를 1급 시민으로 받아들이기 시작한 것이다.
의미
이 흐름이 의미하는 것 — "앱 안에 SQL DB가 있다"는 모델이 더 이상 이상하지 않다. 별도 의존성 없이, 한 줄 임포트로. 이는 위에서 본 임베디드 레플리카 모델과 자연스럽게 결합한다. 런타임 자체가 SQLite를 알고, 라이브러리는 그 위에서 동기화를 한다.
Deno도, 그리고 모든 곳에서
Deno도 SQLite 표준 모듈을 제공한다. WASM 빌드의 SQLite(sql.js, wa-sqlite)는 브라우저에서 동작한다. 모든 런타임이 SQLite를 안다. 이 보편성이 SQLite를 "교환 표준" 으로 만든다 — 같은 파일을 서버, 클라이언트, 엣지가 모두 읽고 쓸 수 있다.
8장 · 로컬 우선 앱 — SQLite가 가능하게 한 패러다임
여기까지가 "서버 쪽 SQLite의 부활" 이라면, 더 큰 파도는 클라이언트다. 로컬 우선(local-first) 앱들.
정의
Ink & Switch의 정의를 요약하면 로컬 우선 앱은 다음을 만족한다:
- 즉각적 (네트워크 라운드트립 없음).
- 오프라인에서 동작.
- 다중 기기 동기화.
- 협업 가능.
- 데이터 소유권이 사용자에게.
- 보안과 프라이버시가 기본.
- 장기 수명 (서비스가 죽어도 데이터가 산다).
이를 위해서는 데이터가 클라이언트 안에 영속적으로 있어야 한다. 그 안의 SQL 엔진? SQLite다.
두 축은 다르다 — "로컬 우선" 과 "CRDT"
흔한 오해 하나를 짚자. "로컬 우선 = CRDT" 가 아니다. 둘은 직교한다.
- 로컬 우선 은 데이터 토폴로지에 관한 약속이다 — 데이터가 클라이언트에 있다, 네트워크 없이도 동작한다.
- CRDT(Conflict-free Replicated Data Type) 는 여러 클라이언트가 같은 데이터를 동시에 수정할 때 충돌을 해결하는 알고리즘이다.
협업이 없거나 단일 사용자가 다중 기기를 쓰는 경우(예: 개인 노트), 단순한 last-write-wins나 벡터 클럭으로 충분하다. CRDT는 다중 사용자 동시 편집에서 필요해진다(Figma, Linear 등).
SQLite는 로컬 우선의 영속성 기반 이다. CRDT 엔진(Automerge, Yjs)이나 SQL-네이티브 동기 엔진(ElectricSQL, Replicache, PowerSync)이 그 위에서 동기화를 다룬다.
SQL-네이티브 동기 엔진들
- ElectricSQL — Postgres와 SQLite 사이의 양방향 동기. SQLite 안에 변경분이 모이고, 백그라운드로 Postgres와 머지. 충돌은 Rich-CRDT(reg. tree of operations)로 해결.
- Replicache (Rocicorp) — 자체 데이터 모델, 클라이언트는 IndexedDB(또는 SQLite-WASM)를 쓰고, 서버는 push/pull 엔드포인트만 구현하면 됨.
- PowerSync — Postgres 기반, 클라이언트는 SQLite. ElectricSQL과 경쟁/유사.
이들의 공통점: 클라이언트에 SQLite, 서버에 Postgres, 그 사이의 동기 레이어. SQLite의 부활은 단순히 서버 사이드만이 아니라, 이런 클라이언트-서버 토폴로지의 한쪽 끝을 책임지는 위치에서도 일어나고 있다.
9장 · SQLite가 옳을 때, 그리고 틀릴 때
여기까지 보면 SQLite 만능론처럼 들릴 수 있다. 그래서는 안 된다. 정직하게 보자.
SQLite가 옳은 케이스
- 읽기 헤비, 트래픽이 한 노드에 들어가는 앱. 대부분의 웹사이트, 대부분의 SaaS 백오피스, 거의 모든 블로그·문서 사이트. "QPS가 분당 1만 미만이고 단일 인스턴스로 충분" 이라면 SQLite가 종종 더 빠르다(네트워크 라운드트립이 없으므로).
- 엣지 네이티브 앱. Cloudflare Workers + D1, Fly.io + Litestream 모델. 데이터를 코드 옆에 두는 게 자연스럽다.
- 임베디드/IoT. 산업용 게이트웨이, 디바이스 로컬 로깅. SQLite의 본업.
- 로컬 우선 클라이언트. 데스크톱·모바일·PWA 앱의 영속 저장소.
- per-tenant 격리. 테넌트당 DB 1개. SQLite는 너무 가벼워서 수십만 개도 가능. Notion·Linear·Turso의 일부 디자인 패턴.
- 분석 쿼리의 임시 작업 공간. DuckDB가 더 어울리지만, 단순 케이스는 SQLite로도 충분.
- 테스트 환경의 격리된 DB. Postgres 대신 인메모리 SQLite. 빠르고 깨끗하다.
SQLite가 틀린 케이스
- 글로벌 단일 1차 + 다중 지역 동시 쓰기. 한 DB에 여러 지역에서 동시에 마이크로초 단위로 쓰기가 필요하면, SQLite 기반 시스템은 거의 다 단일 1차다. 다중 지역 쓰기는 Spanner, CockroachDB, YugabyteDB, FoundationDB 같은 진짜 분산 시스템의 영역이다.
- 쓰기 처리량이 코어 1개를 초과. SQLite는 쓰기에 단일 라이터 락이 있다(WAL 모드여도 트랜잭션 단위로 직렬화). 초당 수만 쓰기를 지속해야 한다면 다른 엔진을 봐라.
- 트랜잭션이 매우 길게 잡혀야 하는 OLTP. 긴 트랜잭션은 다른 쓰기를 막는다. Postgres MVCC가 훨씬 우아하다.
- 거대한 단일 DB. D1은 한 자릿수 GB가 한계, libSQL/Turso도 수십~수백 GB는 가능하지만 TB급 단일 DB는 어색하다. mvSQLite가 답일 수도 있지만, 그 시점에는 분산 OLTP를 다시 검토하라.
- 복잡한 분석 쿼리 (OLAP). SQLite 옵티마이저는 OLTP 워크로드에 맞춰져 있다. 대량 분석은 DuckDB·ClickHouse·Snowflake가 자기 자리다.
- 다중 라이터 어플리케이션이 데이터 무결성을 강하게 요구. 동시 쓰기 격리 수준이 필요하면 Postgres가 정답이다.
- 풀텍스트 검색 헤비. SQLite의 FTS5는 훌륭하지만 Elasticsearch·Meilisearch·Typesense 수준은 아니다.
결정 트리
시작:
단일 노드에 들어가는가? (예: < 1만 QPS, < 100GB)
└─ 예 → 다중 지역에 분산이 필요한가?
├─ 아니오 → SQLite + Litestream (가장 단순함)
└─ 예 → 읽기/쓰기 비율은?
├─ 읽기 헤비 → libSQL/Turso (임베디드 레플리카)
└─ 균형 → D1 (Workers라면) / Postgres + 리드 레플리카
└─ 아니오 → 워크로드는?
├─ OLTP 분산 → CockroachDB / Spanner / YugabyteDB
├─ OLAP/분석 → ClickHouse / Snowflake / BigQuery
└─ 멀티테넌트 → 테넌트당 SQLite (per-tenant) 진지하게 고려
이 결정 트리에서 가장 자주 답이 되는 것은 첫 번째 분기 다 — 대부분의 앱은 단일 노드에 들어간다. 그 사실을 인정하고 나면 운영의 단순함이 폭발적으로 좋아진다.
10장 · 운영의 실제 — 무엇을 진짜 신경 써야 하나
SQLite 기반 시스템을 운영할 때 자주 마주치는 함정들.
WAL 모드는 기본값으로
PRAGMA journal_mode=WAL;. 동시 읽기-쓰기 처리량이 극적으로 좋아진다. 거의 모든 라이브러리가 기본값으로 켜지만, 직접 만든 셋업이면 확인하라.
PRAGMA synchronous 의 트레이드오프
FULL(기본): 가장 안전, 가장 느림.NORMAL: WAL 모드에서 권장. 매우 안전하고 훨씬 빠름.OFF: 위험. 권장하지 않음.
백업은 무조건 Litestream 또는 동급
단일 노드 SQLite의 최대 위험은 디스크/인스턴스 손실 이다. Litestream으로 분 단위 RPO를 확보하라. EBS 스냅샷도 보완책으로 좋다.
분리 마이그레이션 + 앱 배포
SQLite 마이그레이션은 보통 빠르지만, 큰 테이블의 ALTER TABLE은 락이 걸린다. 큰 변경은 새 테이블 + 백필 + 스왑 패턴을 써라.
임베디드 레플리카의 동기 주기
Turso 임베디드 레플리카에서 syncInterval을 너무 짧게 잡으면(예: 1초) 네트워크와 CPU를 낭비한다. 보통 30~60초가 좋은 출발점이고, 쓰기 직후 즉시성이 필요한 곳에서는 명시적 db.sync()나 read-your-writes 모드를 써라.
per-tenant DB의 메타데이터 관리
테넌트당 DB 1개 패턴을 쓰면, "어느 테넌트 DB가 어느 인스턴스/리전에 있는지" 라우팅 메타데이터가 필요하다. 이 메타데이터 자체는 한 개의 중앙 DB(보통 Postgres 또는 더 큰 SQLite)에 둔다.
에필로그 — 부활은 우연이 아니다
24년 된 라이브러리가 가장 핫한 인프라가 된 것은 우연이 아니다. 세 가지가 동시에 일어났다 — 엣지 컴퓨트의 보편화, 로컬 우선 운동의 성숙, 그리고 "분산이 답이 아닐 수도 있다" 는 자각.
이 글의 한 줄 요약: SQLite는 새 도구가 아니라, 새 토폴로지를 가능하게 한 오래된 도구다. libSQL/Turso의 임베디드 레플리카, Cloudflare D1의 엣지 통합, Litestream의 백업 기반, Bun과 Node의 네이티브 SQLite — 이 모든 것이 같은 방향을 가리킨다. 데이터를 코드 옆에 두라.
체크리스트 — SQLite 기반 시스템 시작하기 전
- 단일 노드에 들어가는가? (QPS, 데이터 크기 추산)
- 읽기/쓰기 비율을 측정했는가? (90/10 이상이면 매우 유리)
- 백업 전략이 있는가? (Litestream 또는 동급)
- WAL 모드,
synchronous=NORMAL켰는가? - 마이그레이션 패턴을 정했는가?
- 멀티 지역이 필요하다면, libSQL/Turso 또는 D1을 골랐는가?
- 운영 단순성의 이점이 다른 DB의 분산 기능 손실을 상쇄하는가?
안티 패턴
- "SQLite는 장난감 DB" 라는 편견으로 시작도 안 함. Notion, Linear, Cloudflare 등이 프로덕션에서 쓴다.
- 거꾸로, SQLite로 다 풀려는 만능론. 9장의 "틀린 케이스" 를 인정하라.
- 백업 없는 단일 노드 SQLite. 디스크는 죽는다.
- 임베디드 레플리카에 강한 일관성을 기대. 모델 자체가 결과적 일관성이다.
- LiteFS를 새 프로젝트에 도입. 일몰 흐름이다. libSQL이나 D1을 봐라.
- 너무 잦은
db.sync()호출. 60초 + 명시적 호출 조합이 보통 좋다. - 모든 분산 SQLite 시스템이 같은 보장을 한다고 가정. 표를 다시 보라 — rqlite는 선형화, Turso는 결과적, D1은 쓰기 직렬화 + 결과적 읽기다.
다음 글 예고
다음 두 편에서는 SQLite 르네상스의 자매 주제 두 가지를 다룬다:
- 로컬 우선 앱 구축 실습 — ElectricSQL과 PowerSync로 Postgres ↔ SQLite 양방향 동기 구현하기. 충돌 해결, 오프라인 큐, 모바일 클라이언트.
- 2026년의 데이터베이스 지도 — SQLite 르네상스, Postgres 18의 새 기능, DuckDB의 분석 침공, NewSQL의 현실, 그리고 벡터 DB가 결국 Postgres로 흡수되는 흐름.
24살 된 단일 파일 라이브러리가 가장 흥미로운 곳에 있다는 사실은 우리 업계의 진리 하나를 다시 가르친다 — 단순함은 다시 트렌드가 된다. 충분히 오래 기다리면.
참고 / References
- SQLite Official Site — D. Richard Hipp의 24년 된 단일 파일 DB.
- libSQL on GitHub — SQLite의 오픈 포크, MIT 라이선스.
- Turso Documentation — 매니지드 libSQL, 임베디드 레플리카.
- Turso Embedded Replicas Guide — 모델 설명과 코드 예시.
- The Future of LiteFS — Fly.io Community — 일몰 공지 배경.
- LiteFS on GitHub — 아카이브 가까운 상태이지만 디자인 문서로서 가치.
- Cloudflare D1 Documentation — 엣지 SQLite 매니지드 서비스.
- Making D1 Ready for Production — GA 발표 글.
- Cloudflare D1 Global Read Replication — 글로벌 리드 레플리카.
- mvSQLite on GitHub — FoundationDB 기반 분산 SQLite.
- rqlite Documentation — Raft 위의 SQLite.
- Litestream — SQLite의 S3 지속 백업.
- Ben Johnson — Why I Built Litestream — 동기 설명.
- Bun SQLite — Bun의 네이티브 SQLite 모듈.
- Node.js
node:sqlite— Node 22+ 실험적 SQLite 모듈. - Local-first software — Ink & Switch — 로컬 우선의 정의 에세이.
- ElectricSQL — Postgres ↔ SQLite 동기 엔진.
- Replicache — 클라이언트 사이드 동기 프레임워크.
- PowerSync — Postgres ↔ SQLite 동기, ElectricSQL의 경쟁/유사 제품.
- SQLite WAL Mode — Write-Ahead Logging 공식 문서.
- FoundationDB — mvSQLite가 백킹으로 쓰는 분산 KV.