SQLite 르네상스: 세계에서 가장 많이 쓰이는 DB가 2026년 프로덕션을 점령하고 있는 이유
놀라운 사실 하나 알려드릴게요. 세상에서 가장 많이 배포된 데이터베이스 엔진은 SQLite예요. PostgreSQL도 아니고, MySQL도 아니고, MongoDB도 아니에요. SQLite요. 모든 아이폰, 안드로이드, Mac, Windows 10+ 머신, Firefox와 Chrome 브라우저, 에어버스 A350, 심지어 화성 탐사 로버에도 들어가 있어요. 지금 이 순간 활성 상태인 SQLite 데이터베이스가 문자 그대로 수조 개예요.
그런데도 개발자 커뮤니티는 오랫동안 SQLite를 장난감 취급했어요. "SQLite는 프로덕션용이 아니다." "프로토타입에나 괜찮다." "결국은 진짜 데이터베이스가 필요하게 된다." 이 인식이 너무 뿌리 깊어서, 로컬 개발 이외의 용도로 SQLite를 제안하면 코드 리뷰에서 웃음거리가 될 정도였죠.
그게 바뀌었어요. 2025-2026년에 뭔가 놀라운 일이 벌어졌어요. Turso, Cloudflare D1, LibSQL, Litestream, 그리고 임베디드 데이터베이스 운동 같은 기술들이 한데 모여 SQLite의 실제 한계를 해결하면서도 그 미친 단순함은 고스란히 살려놨거든요. 결과적으로 SQLite가 이전에는 상상도 못 했던 종류의 애플리케이션에서 정식 프로덕션 데이터베이스가 됐어요.
과대광고가 아니에요. 아키텍처 이야기예요. 정확히 뭐가 일어나고 있는지, 왜 중요한지, 그리고 언제 SQLite를 쓰고 안 쓸지 깊이 들어가 볼게요.
SQLite가 "프로덕션 부적격"이었던 이유
흥분하기 전에, 다들 왜 서버 사이드에서 SQLite를 무시했는지 솔직하게 짚어볼게요. 그 판단이 틀렸다기보다는, 다른 시대에 대한 정확한 평가였어요.
전통적인 한계들
1. 싱글 라이터 동시성. SQLite는 파일 레벨 락을 써요. 한 번에 하나의 쓰기만 가능. WAL(Write-Ahead Logging) 모드에서는 동시 읽기 + 싱글 라이터가 되지만, 거기까지예요. 여러 커넥션에서 높은 쓰기 처리량이 필요하면 기존 SQLite는 버텨내질 못해요.
2. 내장 레플리케이션 없음. PostgreSQL에는 스트리밍 레플리케이션, MySQL에는 binlog 레플리케이션이 있어요. SQLite에는... 파일 복사가 있죠. 고가용성이나 장애 조치, 지역 분산이 필요한 앱에서 SQLite는 선택지조차 안 됐어요.
3. 단일 머신 스토리지. 데이터베이스가 디스크의 파일이에요. 클러스터링 없고, 샤딩 없고, 분산 스토리지 없어요. 데이터가 그 머신 하나에 매여 있어요.
4. 네트워크 접근 불가. PostgreSQL이나 MySQL은 네트워크 서비스로 돌면서 어디서든 연결을 받아요. SQLite는 임베디드 라이브러리라서 애플리케이션이 직접 통신해요. 여러 서비스에서 공유 접근이 안 된다는 뜻이죠.
실제 한계예요. 사라진 건 아니에요. 하지만 주변 환경이 너무 극적으로 바뀌어서 예전만큼 중요하지 않게 됐거나, 아예 직접적으로 해결됐어요.
뭐가 달라졌나: 4가지 기둥
기둥 1: 하드웨어가 충분히 빨라졌다
가장 과소평가되는 변화가 하드웨어예요. 최신 NVMe SSD는 ~50만-100만 랜덤 IOPS를 100μs 미만의 지연 시간으로 뽑아내요. SQLite가 NVMe 위에서 돌면 싱글 라이터 모델이라도 초당 수천 건의 쓰기를 처리할 수 있어요. 대부분의 웹 앱에 충분하고도 남아요.
숫자를 보면:
기존 HDD:
랜덤 IOPS: ~100-200
쓰기 지연: ~5-10ms
SQLite 처리량: ~100-200 writes/sec
NVMe SSD (2026):
랜덤 IOPS: ~50만-100만
쓰기 지연: ~10-50μs
SQLite 처리량: ~1만-5만 writes/sec
대부분의 웹 앱은 초당 수백 건 이상의 쓰기가 필요하지 않아요. 분당 100건의 주문을 처리하는 이커머스 사이트? 초당 2건도 안 돼요. 일일 활성 사용자 1만 명인 SaaS 앱? 초당 100건을 넘기기도 힘들어요. NVMe 위의 SQLite는 이 정도는 식은 죽 먹기죠.
기둥 2: WAL 모드와 BEGIN CONCURRENT
2010년부터 있던 SQLite의 WAL 모드가 쓰기 중에도 동시 읽기를 가능하게 해줘요. 대부분의 개발자가 실제로 벤치마크를 안 해보고 "싱글 라이터"라는 말만 듣고 넘어갔거든요.
실제로 WAL 모드의 처리량은 훌륭해요:
-- WAL 모드 활성화 (한 번만 하면 됨) PRAGMA journal_mode=WAL; -- 프로덕션 권장 SQLite 설정 PRAGMA busy_timeout = 5000; -- 락 대기 최대 5초 PRAGMA synchronous = NORMAL; -- 내구성 충분, 훨씬 빠름 PRAGMA cache_size = -64000; -- 64MB 캐시 PRAGMA foreign_keys = ON; -- 참조 무결성 적용 PRAGMA temp_store = MEMORY; -- 임시 테이블 메모리 사용
LibSQL 같은 SQLite 포크에서 쓸 수 있는 BEGIN CONCURRENT 확장은 한 단계 더 나아가요. 같은 페이지를 수정하지 않는 한 여러 쓰기가 동시에 진행 가능해요:
BEGIN CONCURRENT; INSERT INTO orders (user_id, total) VALUES (42, 99.99); COMMIT; -- 같은 데이터 페이지를 건드리지 않으면 -- 여러 커넥션이 동시에 실행 가능
기둥 3: LibSQL — 판을 바꾼 포크
LibSQL은 SQLite의 오픈소스 포크로, 완전한 호환성을 유지하면서 모두가 바라던 기능을 추가했어요. Turso 팀이 만든 LibSQL은 SQLite의 한계를 물리 법칙이 아니라 엔지니어링 문제로 취급해요.
핵심 추가 기능:
- 네이티브 레플리케이션. 프라이머리 서버에서 동기화되는 임베디드 레플리카 지원. 로컬 읽기 성능 + 레플리카 내구성을 동시에 얻을 수 있어요.
- **
BEGIN CONCURRENT**로 다중 쓰기 지원. - 서버 모드. LibSQL이 HTTP와 WebSocket을 통해 네트워크 접근 가능한 서버로 동작해요. "네트워크 접근 불가" 한계가 해결된 거예요.
- ALTER TABLE 개선. 제약 조건이 있는 컬럼 추가 같이 SQLite에서 힘든 작업이 한결 편해졌어요.
- WASM UDF. WebAssembly로 작성하는 사용자 정의 함수. C 확장 없이 SQLite의 기능을 확장할 수 있어요.
- 벡터 검색 내장. AI/임베딩 유스케이스에 바로 쓸 수 있어요.
제일 좋은 점은? LibSQL은 업스트림 SQLite와 merge-forward 전략을 유지하기 때문에, SQLite의 모든 개선 사항을 그대로 받으면서 추가 기능까지 쓸 수 있어요.
기둥 4: 플랫폼 레이어
진짜 혁명은 LibSQL 단독이 아니라 그 위에 구축된 플랫폼이에요.
Turso는 관리형 LibSQL 서비스로:
- 전 세계 30개 이상 위치에 엣지 레플리카
- 앱에 동기화되는 임베디드 레플리카 (서브밀리초 읽기)
- 테넌트별 독립 데이터베이스 (유저/조직당 DB 하나 — 멀티테넌시 핵이 필요 없음)
- 시점 복구와 브랜칭
Cloudflare D1은 Cloudflare의 서버리스 SQLite:
- Cloudflare 엣지 네트워크(300개 이상 도시)에서 실행
- Workers와 네이티브 통합 (네트워크 홉 제로)
- 엣지 노드로 자동 읽기 레플리케이션
- 배치 연산 지원 SQL API
LiteFS (Fly.io)는 역사적 맥락에서 언급할 만해요:
- FUSE 기반으로 Fly.io 머신 간 SQLite 레플리케이션
- 자동 프라이머리 선출과 장애 조치
- 앱에는 투명 (그냥 평소처럼 SQLite 쓰면 됨)
- ⚠️ 참고: LiteFS Cloud(관리형 백업 서비스)는 2024년 10월에 종료됐고, Fly.io가 적극적인 개발을 중단했어요. LiteFS 자체는 안정적이고 프로덕션에서 쓸 수 있지만, pre-1.0 베타 상태로 보장된 로드맵이 없어요. 새 프로젝트에는 Turso나 D1이 더 안전한 선택이에요.
Litestream은:
- SQLite를 S3/GCS/Azure로 지속적 스트리밍 백업
- 오브젝트 스토리지에서 시점 복구
- 가볍고 실전에서 검증됐으며 꾸준히 유지보수 중 — 호스팅 플랫폼에 관계없이 셀프 관리 SQLite 백업에 좋은 선택
아키텍처: 프로덕션 SQLite가 실제로 어떻게 동작하는지
구체적으로 들어가 볼게요. 2026년에 SQLite로 실제 앱을 어떻게 설계하는지 보여드릴게요.
패턴 1: Turso + 임베디드 레플리카
웹 앱에서 가장 인기 있는 패턴이에요. 앱이 Turso 프라이머리에서 동기화되는 로컬 SQLite 레플리카를 내장하는 거예요.
import { createClient } from '@libsql/client'; // 임베디드 레플리카가 있는 클라이언트 생성 const db = createClient({ url: 'file:local.db', // 로컬 레플리카 syncUrl: 'libsql://my-app-db.turso.io', // 원격 프라이머리 authToken: process.env.TURSO_AUTH_TOKEN, syncInterval: 60, // 60초마다 동기화 }); // 읽기는 로컬 레플리카에서 — 서브밀리초 const users = await db.execute('SELECT * FROM users WHERE active = 1'); // 쓰기는 원격 프라이머리로 → 다시 동기화 await db.execute({ sql: 'INSERT INTO users (email, name) VALUES (?, ?)', args: ['[email protected]', 'Alice'], }); // 필요할 때 강제 동기화 await db.sync();
읽기 경로가 놀라운 게, 앱이 로컬 SQLite 파일에서 읽어요. 네트워크 홉 없음. 커넥션 풀 없음. 콜드 스타트 없음. 모든 읽기가 서브밀리초 지연 시간이에요. 쓰기는 네트워크를 통해 프라이머리로 가지만, 대부분의 앱이 읽기 중심이니까 별 문제 아니에요.
패턴 2: Turso로 테넌트별 독립 DB
SQLite 아키텍처가 가장 빛나는 패턴이에요. 모든 테이블에 tenant_id 붙이는 대신, 각 테넌트에 자기만의 데이터베이스를 주는 거예요:
import { createClient } from '@libsql/client'; function getClientForTenant(tenantId: string) { return createClient({ url: `libsql://${tenantId}-my-app.turso.io`, authToken: process.env.TURSO_AUTH_TOKEN, }); } // 테넌트 A의 작업은 테넌트 A의 DB만 건드림 const tenantA = getClientForTenant('tenant-a'); await tenantA.execute('SELECT * FROM invoices'); // 테넌트 B는 완전히 격리됨 const tenantB = getClientForTenant('tenant-b'); await tenantB.execute('SELECT * FROM invoices');
이 패턴의 좋은 점:
- 진짜 격리. 테넌트 간 데이터 유출 위험 없음. 매 쿼리에
WHERE tenant_id = ?안 넣어도 됨. - 독립적 스케일링. 트래픽 많은 테넌트는 자기만의 리소스를 가짐.
- 쿼리가 단순해짐. 모든 쿼리가 기본적으로 테넌트 스코프.
- 컴플라이언스가 쉬움. GDPR 삭제? 데이터베이스 파일 지우면 끝.
- 독립 백업/복구. 한 테넌트 복구해도 다른 테넌트에 영향 없음.
Turso는 무료 티어에서 최대 100개 월간 활성 데이터베이스(5GB 스토리지)를 지원하고, 유료 플랜에서는 수천 개 이상으로 확장돼요 — 테넌트별 DB 패턴에 충분하고도 남아요. SQLite 데이터베이스가 그냥 파일이니까 DB별 오버헤드가 미미하거든요.
패턴 3: Cloudflare D1 + Workers
이미 Cloudflare를 쓰고 있다면, D1이 Workers에서 지연 시간 제로의 DB 접근을 제공해요:
// wrangler.toml // [[d1_databases]] // binding = "DB" // database_name = "my-app-db" // database_id = "xxxx-xxxx-xxxx" export default { async fetch(request: Request, env: Env): Promise<Response> { // 네트워크 홉 제로 — D1이 Worker와 같은 머신에서 실행 const { results } = await env.DB.prepare( 'SELECT * FROM products WHERE category = ?' ).bind('electronics').all(); // 여러 연산 일괄 처리 const batchResults = await env.DB.batch([ env.DB.prepare('UPDATE inventory SET count = count - 1 WHERE id = ?').bind(productId), env.DB.prepare('INSERT INTO orders (product_id, user_id) VALUES (?, ?)').bind(productId, userId), ]); return Response.json(results); } };
패턴 4: 로컬 퍼스트 + 동기화
가장 흥미로운 패턴인 로컬 퍼스트 앱. 데이터베이스가 클라이언트 디바이스에 살면서 서버와 동기화돼요:
import { createClient } from '@libsql/client/web'; // 브라우저나 모바일 앱에서 const localDb = createClient({ url: 'file:local-user.db', syncUrl: 'libsql://user-data.turso.io', authToken: userToken, }); // 오프라인에서도 동작 — 로컬 DB에서 읽기/쓰기 await localDb.execute( 'INSERT INTO notes (title, body) VALUES (?, ?)', ['회의록', 'Q3 로드맵 논의...'] ); // 온라인이면 서버와 동기화 await localDb.sync();
이 패턴이 가능하게 하는 것들:
- 오프라인 퍼스트 — 인터넷 없이도 동작하는 앱
- 즉시 UI 업데이트 (읽기에 로딩 스피너 없음)
- LibSQL 레플리케이션 프로토콜이 처리하는 충돌 해결
- 서버 프라이머리를 통한 디바이스 간 동기화
벤치마크: 실전 숫자로 보기
구체적인 숫자를 볼게요. 일반적인 웹 워크로드에서 SQLite 기반 솔루션과 전통적인 접근 방식을 비교한 벤치마크예요.
읽기 성능
기본 키 기준 단순 SELECT (p50 지연 시간):
SQLite (로컬 파일): ~0.01ms
SQLite (Turso 임베디드 레플리카): ~0.02ms
Cloudflare D1 (Worker에서): ~0.5ms
PostgreSQL (같은 리전): ~1-3ms
PostgreSQL (관리형, Neon 등): ~3-10ms
PlanetScale MySQL (리전): ~3-8ms
읽기 중심 워크로드에서 로컬 SQLite는 네트워크 DB보다 100-1000배 빨라요. 인프로세스 호출과 네트워크 왕복을 비교하는 거니까 정확한 비교는 아니지만, 바로 그게 핵심이에요.
쓰기 성능
단일 INSERT (p50 지연 시간):
SQLite WAL 모드 (NVMe): ~0.05ms
SQLite (Turso, 프라이머리 쓰기): ~15-50ms (네트워크)
Cloudflare D1 (쓰기): ~5-30ms
PostgreSQL (같은 리전): ~1-5ms
쓰기에서는 네트워크 기반 SQLite 솔루션이 지연 시간이 더 높아요. 쓰기가 프라이머리에 도달해야 하니까요. 이게 트레이드오프예요: 읽기가 미쳤을 정도로 빠른 대신 쓰기는 네트워크를 타요. 읽기 중심 앱(대부분의 웹 앱)에서는 이게 훌륭한 트레이드오프예요.
동시 쓰기 처리량
지속 writes/sec (단일 데이터베이스):
SQLite WAL (로컬, NVMe): ~1만-5만
SQLite + BEGIN CONCURRENT: ~5천-2만 (다중 쓰기)
Turso (단일 프라이머리): ~1천-5천 (네트워크 바운드)
Cloudflare D1: ~500-2천
PostgreSQL: ~1만-5만
MySQL: ~1만-5만
쓰기가 많은 워크로드에서는 전통적인 DB가 여전히 유리해요. 여러 동시 프로세스에서 초당 수천 건의 쓰기가 필요하다면 — 실시간 입찰 시스템이나 고빈도 로그 파이프라인 같은 거 — PostgreSQL이나 MySQL이 여전히 더 나은 선택이에요.
언제 프로덕션에서 SQLite를 쓰나 (안 쓰나)
SQLite를 쓰면 좋은 경우:
1. 읽기 중심 앱. 대부분의 웹 앱이 90%+ 읽기예요. 블로그, 콘텐츠 사이트, 이커머스 카탈로그, 대시보드, 문서 사이트 — 전부 읽기 중심이에요. 임베디드 레플리카 + SQLite가 딱이에요.
2. 복잡함 없이 지역 분산이 필요할 때. Turso 엣지 레플리카나 Cloudflare D1이면 데이터가 물리적으로 유저에게 가까워져요. 레플리케이션 토폴로지 관리할 필요 없이 그냥 돌아요.
3. 테넌트 격리가 필요할 때. 데이터 격리 요구사항이 엄격한 SaaS 앱이 테넌트별 DB 패턴의 혜택을 엄청나게 받아요. 모든 테이블에 tenant_id, 매 쿼리에 tenant_id 넣을 필요 없어요.
4. 로컬 퍼스트 또는 오프라인 앱을 만들 때. 오프라인 작업 후 나중에 동기화해야 한다면 SQLite가 자연스러운 기반이에요.
5. 운영 부담을 최소화하고 싶을 때. 관리할 DB 서버 없음. 튜닝할 커넥션 풀 없음. SQLite는 많은 유스케이스에서 제로 운영이에요.
6. 사이드 프로젝트나 중소 규모 앱. 이게 딱 스위트 스팟이에요. 대다수의 웹 앱은 단일 SQLite DB가 감당할 수 있는 범위를 벗어나지 않아요. 일일 사용자 1천 명인 앱에 관리형 PostgreSQL 인스턴스에 돈 쓸 이유가 있을까요?
SQLite를 쓰면 안 되는 경우:
1. 여러 소스에서 높은 지속 쓰기 처리량이 필요할 때. 다른 프로세스에서 초당 수천 건의 동시 쓰기 트랜잭션이 진짜로 필요하면 PostgreSQL이나 MySQL을 쓰세요. 금융 트레이딩, 실시간 분석, IoT 센서 파이프라인 같은 거요.
2. 분산 시스템에서 복잡한 다중 테이블 트랜잭션이 필요할 때. SQLite에는 PostgreSQL이나 CockroachDB 같은 분산 트랜잭션 기능이 없어요.
3. RLS(Row-Level Security)나 고급 접근 제어가 필요할 때. PostgreSQL의 RLS는 하나의 DB 안에서 세밀한 접근 제어가 필요한 멀티테넌트에 검증된 솔루션이에요. SQLite에는 이게 없어요.
4. 팀이 이미 PostgreSQL/MySQL에 강하고 인프라도 갖춰져 있을 때. 유행이라서 SQLite로 바꾸는 건 비추예요. 기존 스택이 잘 돌아가면 마이그레이션 비용이 아깝죠.
5. LISTEN/NOTIFY, 논리 레플리케이션, 스토어드 프로시저 같은 기능이 필요할 때. PostgreSQL의 확장 생태계와 고급 기능은 SQLite에 동등한 것이 없어요.
스택 통합: 실전 구성
2026년에 SQLite로 프로덕션 스택을 구성한다면 이렇게 돼요:
미니멀 스택
앱: Next.js / Remix / SvelteKit / Astro
DB: Turso (LibSQL) + 임베디드 레플리카
ORM: Drizzle ORM (LibSQL/Turso 퍼스트클래스 지원)
호스팅: Vercel / Cloudflare / Fly.io
Drizzle ORM은 LibSQL/Turso 통합이 훌륭해요:
// drizzle.config.ts import { defineConfig } from 'drizzle-kit'; export default defineConfig({ schema: './src/db/schema.ts', dialect: 'turso', dbCredentials: { url: process.env.TURSO_DATABASE_URL!, authToken: process.env.TURSO_AUTH_TOKEN!, }, });
// src/db/schema.ts import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'; export const users = sqliteTable('users', { id: integer('id').primaryKey({ autoIncrement: true }), email: text('email').notNull().unique(), name: text('name').notNull(), createdAt: text('created_at').notNull().default(sql`(datetime('now'))`), }); export const posts = sqliteTable('posts', { id: integer('id').primaryKey({ autoIncrement: true }), title: text('title').notNull(), content: text('content'), authorId: integer('author_id').references(() => users.id).notNull(), published: integer('published', { mode: 'boolean' }).default(false), });
// src/db/index.ts import { drizzle } from 'drizzle-orm/libsql'; import { createClient } from '@libsql/client'; import * as schema from './schema'; const client = createClient({ url: process.env.TURSO_DATABASE_URL!, authToken: process.env.TURSO_AUTH_TOKEN!, }); export const db = drizzle(client, { schema }); // 다른 Drizzle 설정과 동일하게 쿼리 const publishedPosts = await db .select() .from(schema.posts) .where(eq(schema.posts.published, true)) .leftJoin(schema.users, eq(schema.posts.authorId, schema.users.id));
Cloudflare 스택
앱: Cloudflare Workers / Pages
DB: Cloudflare D1
ORM: Drizzle ORM
호스팅: Cloudflare
// D1 + Drizzle import { drizzle } from 'drizzle-orm/d1'; export default { async fetch(request: Request, env: Env) { const db = drizzle(env.DB); const users = await db .select() .from(usersTable) .where(eq(usersTable.active, true)); return Response.json(users); } };
프로덕션 SQLite 운영 팁
프로덕션에서 SQLite를 꺼낸다면 이 습관들이 흔한 함정에서 구해줄 거예요:
1. WAL 모드는 무조건 켜세요
PRAGMA journal_mode=WAL;
협상의 여지 없어요. WAL 모드가 있어야 쓰기 중에도 동시 읽기가 돼요.
2. 프로덕션 PRAGMA 설정
PRAGMA journal_mode = WAL; PRAGMA busy_timeout = 5000; PRAGMA synchronous = NORMAL; PRAGMA cache_size = -64000; PRAGMA foreign_keys = ON; PRAGMA temp_store = MEMORY; PRAGMA mmap_size = 268435456; -- 256MB 메모리 맵 I/O PRAGMA page_size = 4096; -- 파일시스템 블록 사이즈에 맞추기
3. SQLITE_BUSY 깔끔하게 처리하기
async function executeWithRetry(db, sql, args, maxRetries = 3) { for (let i = 0; i < maxRetries; i++) { try { return await db.execute({ sql, args }); } catch (err) { if (err.code === 'SQLITE_BUSY' && i < maxRetries - 1) { await new Promise(r => setTimeout(r, Math.pow(2, i) * 100)); continue; } throw err; } } }
4. 백업 전략
# Litestream — S3로 지속 백업 litestream replicate /data/app.db s3://my-bucket/app.db # 시점 복구 litestream restore -o /data/restored.db s3://my-bucket/app.db # Turso — 내장 시점 복구 turso db create my-app --from-db my-app-backup --timestamp "2026-02-26T10:00:00Z"
PostgreSQL에서 SQLite로 마이그레이션 경로
기존 앱을 SQLite로 마이그레이션 고려 중이라면, 현실적인 로드맵은 이래요:
1단계: 스키마 감사
SQLite는 PostgreSQL과 스키마 차이가 있어요:
-- PostgreSQL -- SQLite 동등 표현 SERIAL / BIGSERIAL INTEGER PRIMARY KEY AUTOINCREMENT UUID DEFAULT gen_random_uuid() TEXT (앱 코드에서 UUID 생성) TIMESTAMP WITH TIME ZONE TEXT (ISO8601 문자열) JSONB TEXT (JSON을 텍스트로 저장) ENUM types TEXT + CHECK 제약조건 ARRAY types 동등 표현 없음 (JSON이나 연결 테이블)
2단계: 호환 불가 패턴 식별
PostgreSQL에만 있고 SQLite에 직접 동등한 것이 없는 기능들:
- Row-Level Security — 앱 레이어에서 구현
- LISTEN/NOTIFY — 폴링이나 외부 pub/sub 사용
- 전문 검색 — SQLite에 FTS5 있음 (문법은 다르지만 성능은 동급)
- Stored Procedure — 앱 코드로 이동
- Materialized View — 일반 뷰나 트리거 포함 테이블로 대체
큰 그림: 왜 이게 중요한지
SQLite 르네상스는 단순히 SQLite 이야기가 아니에요. 데이터베이스를 바라보는 시각의 근본적인 전환을 대표해요:
1. 지역성이 핵심이다. 가장 빠른 DB 쿼리는 네트워크를 넘지 않는 쿼리예요. 임베디드 레플리카와 엣지 DB가 이 원칙을 구현하고 있어요.
2. 단순함은 복리로 이자가 붙는다. DB 커넥션 풀 설정, ORM 연결 문자열, 관리형 DB 인스턴스 — 전부 운영 비용이에요. SQLite는 대부분을 없애요. 시간이 갈수록 이 단순함이 복리로 쌓여요.
3. "적합한 도구"의 정의가 바뀌고 있다. 오랫동안 진지한 프로젝트의 "적합한 도구"는 PostgreSQL이었어요. 많은 앱에서 여전히 맞는 말이에요. 하지만 콘텐츠 사이트, 개인 프로젝트, 소규모 SaaS, 로컬 퍼스트 앱, 엣지 함수 같은 점점 넓어지는 유스케이스에서는 모던 툴링 + SQLite가 이제 진짜로 적합한 도구예요.
4. 싱글 테넌시가 다시 돌아오고 있다. 멀티테넌트 PostgreSQL 패턴(하나의 DB, 모든 곳에 tenant_id)은 쿼리, 마이그레이션, 백업, 컴플라이언스에서 복잡성을 만들어요. 테넌트별 SQLite DB가 많은 SaaS 앱에 더 깔끔한 아키텍처를 제공하는 거예요.
결론
SQLite 르네상스는 진짜이고, 사그라들 기미가 없어요. 더 빨라진 하드웨어, LibSQL의 혁신, Turso와 Cloudflare D1 같은 플랫폼이 합쳐져서 SQLite의 역사적 한계를 충분히 해결했어요.
근데 이것이 뭐고 뭐가 아닌지 명확히 해야 해요:
- 읽기 중심 웹 앱, 테넌트 격리가 필요한 SaaS, 엣지 배포, 로컬 퍼스트 앱, 중소 규모 프로젝트에 쓸 수 있는 프로덕션 DB예요.
- 모든 시나리오에서 PostgreSQL을 대체하는 건 아니에요. 높은 쓰기 처리량, 복잡한 분산 트랜잭션, RLS, 고급 확장 생태계는 여전히 PostgreSQL의 영역이에요.
제일 좋은 조언요? 다음 사이드 프로젝트를 SQLite로 시작해 보세요. "진짜 DB" 나오기 전 임시로가 아니라, 메인 DB로요. Turso나 D1으로 호스팅하고, Drizzle 쓰고, 뭔가 만들어 보세요. 생각보다 훨씬 멀리까지 갈 수 있을 거예요.
그리고 나중에 못 감당하게 되면 — 통계적으로 대부분의 프로젝트는 그런 일이 없지만 — PostgreSQL로 마이그레이션하는 건 잘 알려진 길이에요. 근데 어쩌면 SQLite가 처음부터 끝까지 필요한 전부였다는 걸 알게 될 수도 있어요.
세계에서 가장 많이 배포된 데이터베이스가 드디어 서버 사이드에서 제대로 된 대접을 받고 있어요. 솔직히? 이제야 그러는 거예요.
관련 도구 둘러보기
Pockit의 무료 개발자 도구를 사용해 보세요