
SQLite: 서버 없이 DB를 쓴다고? (Turso, Litestream)
SQLite는 모바일에서만 쓰는 줄 알았는데, Turso와 Litestream 덕분에 프로덕션 웹 서비스에서도 진지하게 쓸 수 있게 됐다.

SQLite는 모바일에서만 쓰는 줄 알았는데, Turso와 Litestream 덕분에 프로덕션 웹 서비스에서도 진지하게 쓸 수 있게 됐다.
DB 설계의 기초. 데이터를 쪼개고 쪼개서 이상 현상(Anomaly)을 방지하는 과정. 제1, 2, 3 정규형을 쉽게 설명합니다.

이진 트리는 메모리용입니다. 디스크(SSD/HDD)는 느리니까 트리 키를 낮추고 옆으로 뚱뚱하게 만들어서 디스크 I/O 횟수를 최소화했습니다. B-Tree vs B+Tree 차이와 MySQL 인덱스의 비밀.

서비스를 MSA로 쪼갰더니 트랜잭션 관리가 지옥이 되었습니다. 주문은 성공했는데 결제는 실패하고, 재고는 이미 차감되었다면? 모놀리식의 ACID가 그리워지는 순간, 분산 환경에서 데이터 일관성을 지키는 Two-Phase Commit(2PC), Saga 패턴(Choreography, Orchestration)을 구체적인 예제와 함께 다뤄봤습니다.

서버가 미국에 있어서 한국 사용자 응답이 느렸는데, Cloudflare Workers로 전 세계 엣지에서 코드를 실행하니 응답 시간이 극적으로 줄었다.

처음 웹 서비스를 만들 때, PostgreSQL이나 MySQL 세팅하면서 이런 생각이 들었다. "DB 서버 띄우고, 포트 열고, 접속 설정하고... 이거 왜 이렇게 복잡하지?" 그냥 파일 하나 만들어서 데이터 저장하면 안 되나? 근데 알고 보니, 그게 바로 SQLite였다.
SQLite는 안드로이드 앱에서만 쓰는 거라고 생각했다. 모바일 로컬 DB 정도? 근데 최근에 Turso와 Litestream을 알게 되면서, 이 생각이 완전히 바뀌었다. SQLite가 프로덕션 웹 서비스에서도 충분히 쓸 수 있는, 아니 어떤 경우엔 PostgreSQL보다 나은 선택이 될 수 있다는 걸 이해했다.
결국 핵심은 이거였다. "서버가 필요 없다는 건, 관리할 게 하나 줄어든다는 뜻이다."
처음 이해한 건 이거다. 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 쓸 수 있고, 인덱싱도 되고, 트랜잭션도 된다. 이게 와닿았다.
그럼 당연한 질문이 나온다. "이렇게 좋으면 왜 다들 PostgreSQL 쓰지?"
전통적인 SQLite의 한계가 세 가지였다:
솔직히 이거 보고 "아, 역시 장난감이구나"라고 생각했다. 근데 2024년쯤부터 이 모든 게 바뀌기 시작했다.
엣지 컴퓨팅이 대세가 되면서, 패러다임이 바뀌었다. 예전엔 "하나의 중앙 서버 + 하나의 중앙 DB"였다. 지금은 "전 세계 수백 개 엣지 서버 + ?"
PostgreSQL을 전 세계에 복제하기? 비싸고 복잡하다. 근데 SQLite 파일을 각 엣지 서버에 복사하면? 그게 바로 Turso가 하는 일이다.
또 하나, 개인 프로젝트나 스타트업 입장에서는 "DB 서버 관리 안 하고 싶다"는 니즈가 컸다. Litestream이 이걸 해결했다. SQLite 파일을 S3에 실시간으로 백업하니까, 서버 날아가도 복구 가능하다.
결국 이해한 건, "복잡한 분산 시스템보다, 간단한 걸 똑똑하게 복제하는 게 낫다"는 철학이었다.
Turso는 SQLite를 fork한 libSQL 기반이다. 핵심 아이디어는 "SQLite를 전 세계 엣지 서버에 복제하자"다.
작동 원리는 이렇다:
사용자가 한국에서 접속하면? 한국 엣지 서버의 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은 다른 접근이다. SQLite 파일의 변경사항을 실시간으로 S3(또는 Azure Blob, GCS)에 스트리밍한다.
작동 방식:
이게 개인 프로젝트나 작은 팀에겐 완벽했다. 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은 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 붙이는 과정이 놀라울 정도로 간단했다.
# 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가 PostgreSQL보다 낫다"가 아니라, "상황에 따라 더 맞는 도구가 있다"는 거다.
내 경험상, 개인 블로그나 작은 SaaS 프로젝트는 Turso로 충분했다. 오히려 더 빨랐다. 근데 실시간 채팅이나 협업 기능이 들어가면 PostgreSQL이 맞다.
SQLite를 다시 보게 된 건, Turso와 Litestream 덕분이었다. 예전엔 "모바일 전용"이었던 게, 이제는 "프로덕션 웹 서비스"에서도 진지하게 고려할 선택지가 됐다.
핵심 깨달음 세 가지:
서버리스 DB의 진짜 의미: DB 서버 없이도 안정적이고 빠른 데이터 저장이 가능하다. Litestream은 파일 기반 DB의 약점(백업, 복구)을 완벽히 해결했다.
엣지 복제의 위력: Turso는 SQLite를 전 세계에 복제해서, 읽기 성능을 극대화했다. CDN처럼 DB도 사용자 가까이 둘 수 있다.
간단함의 가치: PostgreSQL의 모든 기능이 항상 필요한 건 아니다. 대부분의 애플리케이션은 SQLite로도 충분하고, 오히려 관리 오버헤드가 줄어들어 생산성이 올라간다.
다음 개인 프로젝트는 Turso로 시작할 생각이다. DB 서버 띄우고, 마이그레이션 걱정하고, 백업 설정하는 시간에, 그냥 코드 짜고 배포하면 된다. 트래픽 늘어나면? Turso가 알아서 스케일한다.
결국, "필요한 만큼만 복잡하게"가 정답이었다.
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."
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.
The obvious question: "If it's so good, why does everyone use PostgreSQL?"
SQLite had three traditional limitations:
I thought, "Okay, it's a toy database." But around 2024, everything started changing.
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."
Turso is built on libSQL, a fork of SQLite. The core idea: "Replicate SQLite to edge servers worldwide."
How it works:
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 takes a different approach. It streams SQLite file changes to S3 (or Azure Blob, GCS) in real-time.
How it works:
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.
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.
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.
I now understand clearly: it's not "SQLite is better than PostgreSQL," but "different tools fit different situations."
In my experience, personal blogs and small SaaS projects worked great with Turso—actually faster. But real-time chat or collaboration features need PostgreSQL.