Prologue: 데이터베이스에 서버가 왜 필요해?
처음 웹 서비스를 만들 때, PostgreSQL이나 MySQL 세팅하면서 이런 생각이 들었다. "DB 서버 띄우고, 포트 열고, 접속 설정하고... 이거 왜 이렇게 복잡하지?" 그냥 파일 하나 만들어서 데이터 저장하면 안 되나? 근데 알고 보니, 그게 바로 SQLite였다.
SQLite는 안드로이드 앱에서만 쓰는 거라고 생각했다. 모바일 로컬 DB 정도? 근데 최근에 Turso와 Litestream을 알게 되면서, 이 생각이 완전히 바뀌었다. SQLite가 프로덕션 웹 서비스에서도 충분히 쓸 수 있는, 아니 어떤 경우엔 PostgreSQL보다 나은 선택이 될 수 있다는 걸 이해했다.
결국 핵심은 이거였다. "서버가 필요 없다는 건, 관리할 게 하나 줄어든다는 뜻이다."
Aha! 순간: SQLite는 라이브러리다, 서버가 아니라
SQLite의 본질: 임베디드 데이터베이스
처음 이해한 건 이거다. PostgreSQL은 별도의 프로세스로 돌아간다. 내 애플리케이션 서버가 TCP/IP로 DB 서버에 연결하고, 쿼리 날리고, 응답 받는다. 네트워크 레이턴시가 생기고, 커넥션 풀 관리해야 하고, DB 서버 리소스 따로 챙겨야 한다.
SQLite는? 파일 하나다. 그리고 내 애플리케이션 코드 안에 라이브러리로 들어가 있다. 쿼리 날리면 그냥 함수 호출이다. 네트워크 없다. 커넥션 풀 필요 없다. DB 서버 모니터링? 그런 거 없다.
// PostgreSQL: 네트워크 연결
import { Pool } from 'pg';
const pool = new Pool({
host: 'db.example.com',
port: 5432,
user: 'admin',
password: 'secret',
database: 'myapp',
});
const result = await pool.query('SELECT * FROM users');
// SQLite: 그냥 파일 읽기
import Database from 'better-sqlite3';
const db = new Database('./myapp.db');
const result = db.prepare('SELECT * FROM users').all();
마치 엑셀 파일 열어서 데이터 읽는 것처럼 간단하다. 근데 SQL 쓸 수 있고, 인덱싱도 되고, 트랜잭션도 된다. 이게 와닿았다.
왜 웹 서비스에서 SQLite를 안 썼을까?
그럼 당연한 질문이 나온다. "이렇게 좋으면 왜 다들 PostgreSQL 쓰지?"
전통적인 SQLite의 한계가 세 가지였다:
- 단일 쓰기 문제: 한 번에 하나의 연결만 쓰기 가능. 동시 사용자가 많으면 병목.
- 복제 없음: DB 파일 하나만 있으니까, 서버 죽으면 끝.
- 분산 불가: 전 세계 사용자에게 낮은 레이턴시로 서비스하려면? 불가능.
솔직히 이거 보고 "아, 역시 장난감이구나"라고 생각했다. 근데 2024년쯤부터 이 모든 게 바뀌기 시작했다.
SQLite 르네상스: 왜 지금인가?
엣지 컴퓨팅이 대세가 되면서, 패러다임이 바뀌었다. 예전엔 "하나의 중앙 서버 + 하나의 중앙 DB"였다. 지금은 "전 세계 수백 개 엣지 서버 + ?"
PostgreSQL을 전 세계에 복제하기? 비싸고 복잡하다. 근데 SQLite 파일을 각 엣지 서버에 복사하면? 그게 바로 Turso가 하는 일이다.
또 하나, 개인 프로젝트나 스타트업 입장에서는 "DB 서버 관리 안 하고 싶다"는 니즈가 컸다. Litestream이 이걸 해결했다. SQLite 파일을 S3에 실시간으로 백업하니까, 서버 날아가도 복구 가능하다.
결국 이해한 건, "복잡한 분산 시스템보다, 간단한 걸 똑똑하게 복제하는 게 낫다"는 철학이었다.
Deep Dive: Turso와 Litestream이 바꾼 게임
Turso (libSQL): 엣지에 흩뿌린 SQLite
Turso는 SQLite를 fork한 libSQL 기반이다. 핵심 아이디어는 "SQLite를 전 세계 엣지 서버에 복제하자"다.
작동 원리는 이렇다:
- Primary DB: 메인 리전에 하나의 primary 데이터베이스.
- Replicas: 전 세계 엣지 로케이션에 읽기 전용 복제본.
- 자동 동기화: Write는 primary로 가고, 즉시 replicas에 전파.
사용자가 한국에서 접속하면? 한국 엣지 서버의 SQLite 복제본에서 읽는다. 네트워크 레이턴시가 거의 없다. Write할 때만 primary로 가니까, 대부분의 read-heavy 애플리케이션에선 엄청 빠르다.
// Turso 연결 (Drizzle ORM 사용)
import { drizzle } from 'drizzle-orm/libsql';
import { createClient } from '@libsql/client';
const client = createClient({
url: process.env.TURSO_DATABASE_URL!, // wss://your-db.turso.io
authToken: process.env.TURSO_AUTH_TOKEN!,
});
const db = drizzle(client);
// 읽기: 가장 가까운 엣지에서
const users = await db.select().from(usersTable);
// 쓰기: primary로 자동 라우팅
await db.insert(usersTable).values({ name: 'Alice', email: 'alice@example.com' });
마치 CDN이 정적 파일을 엣지에 캐싱하는 것처럼, Turso는 DB를 엣지에 캐싱한다. 이게 와닿았다. "데이터를 사용자 가까이 두는 게 제일 빠르다."
Litestream: S3로 실시간 백업
Litestream은 다른 접근이다. SQLite 파일의 변경사항을 실시간으로 S3(또는 Azure Blob, GCS)에 스트리밍한다.
작동 방식:
- SQLite의 WAL(Write-Ahead Log) 모니터링.
- 변경사항이 생기면 즉시 S3에 백업.
- 서버 죽으면? S3에서 복구해서 새 서버 띄움.
이게 개인 프로젝트나 작은 팀에겐 완벽했다. DB 서버 없이, SQLite 파일만 쓰면서도, 안정성은 PostgreSQL만큼 확보할 수 있다.
# litestream.yml
dbs:
- path: /data/myapp.db
replicas:
- type: s3
bucket: my-app-backups
path: db
region: us-east-1
retention: 720h # 30일 보관
# Litestream으로 SQLite 실행
litestream replicate
# 복구할 때
litestream restore -o /data/myapp.db s3://my-app-backups/db
비유하자면, Litestream은 "자동 Time Machine 백업"이다. 파일 시스템에 변경이 생기면 실시간으로 클라우드에 백업되고, 언제든 특정 시점으로 돌아갈 수 있다.
Drizzle ORM과의 조합
Drizzle ORM은 SQLite를 PostgreSQL처럼 타입 안전하게 쓸 수 있게 해준다. Turso도 공식 지원한다.
// schema.ts
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';
export const posts = sqliteTable('posts', {
id: integer('id').primaryKey({ autoIncrement: true }),
title: text('title').notNull(),
content: text('content').notNull(),
publishedAt: integer('published_at', { mode: 'timestamp' }),
});
// db.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 });
// 사용
import { db } from './db';
import { posts } from './schema';
import { eq } from 'drizzle-orm';
const allPosts = await db.select().from(posts);
const post = await db.select().from(posts).where(eq(posts.id, 1));
타입 안전성, 마이그레이션, 관계 쿼리 전부 된다. PostgreSQL에서 SQLite로 마이그레이션할 때 ORM 레이어는 거의 안 바꿔도 된다.
Next.js + Turso 실제 셋업
실제로 Next.js 프로젝트에 Turso 붙이는 과정이 놀라울 정도로 간단했다.
# Turso CLI 설치
brew install tursodatabase/tap/turso
# 로그인
turso auth login
# DB 생성 (자동으로 가장 가까운 리전에)
turso db create my-app
# 토큰 생성
turso db tokens create my-app
# 연결 URL 확인
turso db show my-app --url
// app/actions/posts.ts
'use server';
import { db } from '@/lib/db';
import { posts } from '@/lib/schema';
import { eq } from 'drizzle-orm';
export async function getPosts() {
return await db.select().from(posts);
}
export async function createPost(title: string, content: string) {
const [post] = await db
.insert(posts)
.values({ title, content, publishedAt: new Date() })
.returning();
return post;
}
export async function getPost(id: number) {
const [post] = await db.select().from(posts).where(eq(posts.id, id));
return post;
}
Server Actions에서 바로 쓸 수 있다. 엣지 런타임이랑도 호환된다. Vercel Edge Functions에서 돌려도 문제없다.
SQLite vs PostgreSQL: 언제 뭘 쓸까?
이제 명확하게 이해했다. "SQLite가 PostgreSQL보다 낫다"가 아니라, "상황에 따라 더 맞는 도구가 있다"는 거다.
SQLite (+ Turso/Litestream)를 쓸 때:
- Read-heavy 애플리케이션: 블로그, 문서 사이트, 포트폴리오.
- 개인 프로젝트/MVP: DB 서버 관리 오버헤드 줄이고 빠르게 론칭.
- 엣지 컴퓨팅: 전 세계 사용자에게 낮은 레이턴시.
- 임베디드 분석: 사용자별 로컬 분석 DB, 오프라인 동기화.
- 단순한 데이터 모델: 복잡한 조인이나 집계 적음.
PostgreSQL을 쓸 때:
- Write-heavy 애플리케이션: 소셜 미디어, 실시간 협업 툴.
- 복잡한 쿼리: JSON 쿼리, Full-text search, GIS.
- 강력한 일관성 필요: 금융, 결제 시스템.
- 대규모 동시 쓰기: 수천 명이 동시에 write하는 경우.
- PL/pgSQL 등 고급 기능: Stored procedures, triggers.
내 경험상, 개인 블로그나 작은 SaaS 프로젝트는 Turso로 충분했다. 오히려 더 빨랐다. 근데 실시간 채팅이나 협업 기능이 들어가면 PostgreSQL이 맞다.
Summary: 서버 없는 DB의 시대
SQLite를 다시 보게 된 건, Turso와 Litestream 덕분이었다. 예전엔 "모바일 전용"이었던 게, 이제는 "프로덕션 웹 서비스"에서도 진지하게 고려할 선택지가 됐다.
핵심 깨달음 세 가지:
-
서버리스 DB의 진짜 의미: DB 서버 없이도 안정적이고 빠른 데이터 저장이 가능하다. Litestream은 파일 기반 DB의 약점(백업, 복구)을 완벽히 해결했다.
-
엣지 복제의 위력: Turso는 SQLite를 전 세계에 복제해서, 읽기 성능을 극대화했다. CDN처럼 DB도 사용자 가까이 둘 수 있다.
-
간단함의 가치: PostgreSQL의 모든 기능이 항상 필요한 건 아니다. 대부분의 애플리케이션은 SQLite로도 충분하고, 오히려 관리 오버헤드가 줄어들어 생산성이 올라간다.
다음 개인 프로젝트는 Turso로 시작할 생각이다. DB 서버 띄우고, 마이그레이션 걱정하고, 백업 설정하는 시간에, 그냥 코드 짜고 배포하면 된다. 트래픽 늘어나면? Turso가 알아서 스케일한다.
결국, "필요한 만큼만 복잡하게"가 정답이었다.
SQLite: A Database Without a Server? (Turso and Litestream)
Prologue: Why Do Databases Need Servers Anyway?
When I first built a web service, setting up PostgreSQL or MySQL felt unnecessarily complex. Spinning up a DB server, opening ports, configuring connections—couldn't I just create a file and store data? Turns out, that's exactly what SQLite does.
I used to think SQLite was just for Android apps. A local mobile database, nothing more. But discovering Turso and Litestream completely changed my perspective. SQLite isn't just viable for production web services—in some cases, it's a better choice than PostgreSQL.
The core insight: "No server means one less thing to manage."
The Aha! Moment: SQLite Is a Library, Not a Server
The Essence of SQLite: An Embedded Database
PostgreSQL runs as a separate process. Your application server connects via TCP/IP, sends queries, and waits for responses. There's network latency, connection pooling to manage, and separate DB server resources to monitor.
SQLite? It's just a file. And it lives inside your application code as a library. Running a query is a function call. No network. No connection pool. No DB server monitoring.
// PostgreSQL: Network connection
import { Pool } from 'pg';
const pool = new Pool({
host: 'db.example.com',
port: 5432,
user: 'admin',
password: 'secret',
database: 'myapp',
});
const result = await pool.query('SELECT * FROM users');
// SQLite: Just file access
import Database from 'better-sqlite3';
const db = new Database('./myapp.db');
const result = db.prepare('SELECT * FROM users').all();
It's as simple as opening an Excel file and reading data. But you get SQL, indexing, and transactions. That clicked for me.
Why Wasn't SQLite Used for Web Services?
The obvious question: "If it's so good, why does everyone use PostgreSQL?"
SQLite had three traditional limitations:
- Single writer problem: Only one connection can write at a time. Bottleneck with concurrent users.
- No built-in replication: One DB file. Server dies, you're done.
- No distribution: Low latency for global users? Impossible.
I thought, "Okay, it's a toy database." But around 2024, everything started changing.
The SQLite Renaissance: Why Now?
Edge computing shifted the paradigm. The old model was "one central server + one central DB." The new model is "hundreds of edge servers worldwide + ?"
Replicating PostgreSQL globally? Expensive and complex. But copying SQLite files to each edge server? That's what Turso does.
Also, for indie hackers and startups, the desire to avoid DB server management was strong. Litestream solved this by continuously backing up SQLite files to S3. Server crashes? Restore from S3.
The philosophy I understood: "Smartly replicating simple things beats complex distributed systems."
Deep Dive: How Turso and Litestream Changed the Game
Turso (libSQL): SQLite Scattered Across the Edge
Turso is built on libSQL, a fork of SQLite. The core idea: "Replicate SQLite to edge servers worldwide."
How it works:
- Primary DB: One primary database in a main region.
- Replicas: Read-only replicas at edge locations globally.
- Auto-sync: Writes go to primary, immediately propagate to replicas.
A user in Korea connects? They read from the Korean edge server's SQLite replica. Almost zero network latency. Writes only go to primary, so read-heavy apps are blazing fast.
// Turso connection (using Drizzle ORM)
import { drizzle } from 'drizzle-orm/libsql';
import { createClient } from '@libsql/client';
const client = createClient({
url: process.env.TURSO_DATABASE_URL!, // wss://your-db.turso.io
authToken: process.env.TURSO_AUTH_TOKEN!,
});
const db = drizzle(client);
// Reads: From nearest edge
const users = await db.select().from(usersTable);
// Writes: Auto-routed to primary
await db.insert(usersTable).values({ name: 'Alice', email: 'alice@example.com' });
Just like a CDN caches static files at the edge, Turso caches the database at the edge. "Putting data close to users is the fastest option."
Litestream: Real-Time Backup to S3
Litestream takes a different approach. It streams SQLite file changes to S3 (or Azure Blob, GCS) in real-time.
How it works:
- Monitors SQLite's WAL (Write-Ahead Log).
- Changes stream immediately to S3.
- Server crashes? Restore from S3 and spin up a new server.
Perfect for indie projects and small teams. Use SQLite files without a DB server, but get PostgreSQL-level reliability.
# litestream.yml
dbs:
- path: /data/myapp.db
replicas:
- type: s3
bucket: my-app-backups
path: db
region: us-east-1
retention: 720h # 30-day retention
# Run SQLite with Litestream
litestream replicate
# Restore when needed
litestream restore -o /data/myapp.db s3://my-app-backups/db
Think of Litestream as "automatic Time Machine backups." File changes stream to the cloud in real-time, and you can restore to any point in time.
Pairing with Drizzle ORM
Drizzle ORM makes SQLite type-safe like PostgreSQL. It officially supports Turso too.
// schema.ts
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';
export const posts = sqliteTable('posts', {
id: integer('id').primaryKey({ autoIncrement: true }),
title: text('title').notNull(),
content: text('content').notNull(),
publishedAt: integer('published_at', { mode: 'timestamp' }),
});
// db.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 });
// Usage
import { db } from './db';
import { posts } from './schema';
import { eq } from 'drizzle-orm';
const allPosts = await db.select().from(posts);
const post = await db.select().from(posts).where(eq(posts.id, 1));
Type safety, migrations, relational queries—all supported. Migrating from PostgreSQL to SQLite barely requires ORM layer changes.
Real-World Setup: Next.js + Turso
Setting up Turso in a Next.js project was surprisingly simple.
# Install Turso CLI
brew install tursodatabase/tap/turso
# Login
turso auth login
# Create DB (auto-selects nearest region)
turso db create my-app
# Generate token
turso db tokens create my-app
# Get connection URL
turso db show my-app --url
// app/actions/posts.ts
'use server';
import { db } from '@/lib/db';
import { posts } from '@/lib/schema';
import { eq } from 'drizzle-orm';
export async function getPosts() {
return await db.select().from(posts);
}
export async function createPost(title: string, content: string) {
const [post] = await db
.insert(posts)
.values({ title, content, publishedAt: new Date() })
.returning();
return post;
}
export async function getPost(id: number) {
const [post] = await db.select().from(posts).where(eq(posts.id, id));
return post;
}
Works directly in Server Actions. Compatible with edge runtime. Runs fine on Vercel Edge Functions.
SQLite vs PostgreSQL: When to Use Which?
I now understand clearly: it's not "SQLite is better than PostgreSQL," but "different tools fit different situations."
Use SQLite (+ Turso/Litestream) when:
- Read-heavy apps: Blogs, documentation sites, portfolios.
- Personal projects/MVPs: Reduce DB management overhead, ship fast.
- Edge computing: Low latency for global users.
- Embedded analytics: Per-user local analytics DB, offline sync.
- Simple data models: Minimal complex joins or aggregations.
Use PostgreSQL when:
- Write-heavy apps: Social media, real-time collaboration tools.
- Complex queries: JSON queries, full-text search, GIS.
- Strong consistency required: Finance, payment systems.
- Massive concurrent writes: Thousands writing simultaneously.
- Advanced features: PL/pgSQL, stored procedures, triggers.
In my experience, personal blogs and small SaaS projects worked great with Turso—actually faster. But real-time chat or collaboration features need PostgreSQL.