Turso 완전 가이드 | Edge에서 실행되는 SQLite 서버리스 데이터베이스
이 글의 핵심
SQLite 기반 Edge Database Turso. 전 세계 35개 리전에 자동 복제되어 사용자 근처에서 0ms 읽기 지연으로 데이터를 제공합니다. Drizzle ORM과 완벽히 통합되며, 무료 플랜으로 500개 DB까지 지원합니다.
이 글의 핵심
Turso는 SQLite 기반 Edge Database입니다. 전 세계 35개 리전에 자동 복제되어 사용자 근처에서 0ms 읽기 지연으로 데이터를 제공하며, Drizzle ORM과 완벽히 통합됩니다. LibSQL로 SQLite를 확장해 동시 쓰기와 원격 접속을 지원합니다.
목차
- Turso란? · 핵심 아키텍처: LibSQL과 Edge 복제
- SQLite의 한계와 Turso의 보완 · Edge 데이터베이스란?
- Turso 시작하기
- Node.js/TypeScript 통합 · 실전 CRUD (TypeScript)
- Drizzle ORM 통합
- Cloudflare Workers에서 사용
- Vercel·Netlify와의 통합
- 복제 설정과 다중 리전 전략 · Embedded Replicas
- 가격·무료 티어 활용 · 프로덕션에서 느낀 점
- 핵심 정리
Turso란?
Turso는 2022년 Glauber Costa가 설립한 SQLite 기반 서버리스 데이터베이스입니다. 팀이 LibSQL(오픈소스 포크)을 중심으로 원격·복제·프로토콜을 정리해 두었기 때문에, 기존에 SQLite에 익숙한 개발자는 학습 곡선이 짧고, 엣지 런타임(Cloudflare Workers, Vercel Edge, Deno Deploy 등)과도 궁합이 좋습니다.
🚀 핵심 특징
1. Edge에 자동 복제
기존 PostgreSQL:
- 단일 리전 (Seoul)
- 미국 사용자: 200ms 지연
Turso:
- 35개 리전에 자동 복제
- 미국 사용자: 0ms 지연 (로컬 읽기)
2. SQLite 기반
-- SQLite와 동일한 문법
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT UNIQUE
);
INSERT INTO users (name, email) VALUES ('Alice', '[email protected]');
SELECT * FROM users;
3. LibSQL 확장
- 동시 쓰기 지원
- 원격 접속 (HTTP API)
- WebAssembly 내장
- 벡터 검색 (AI 임베딩)
4. 무료 플랜
- 500개 데이터베이스
- 9GB 스토리지
- 10억 행 읽기/월
핵심 아키텍처: LibSQL과 Edge 복제
LibSQL은 Turso 팀이 만든 SQLite의 오픈소스 갈래이며, 여기에 HTTP·WebSocket 기반 프로토콜, 원격 연결, 임베디드(embedded) 복제, 때로는 서버 측 확장이 얹어져 있습니다. 데이터베이스 파일이 한 대의 디스크에만 묶여 있던 기존 SQLite와 달리, Turso는 중앙(또는 기본) 데이터베이스를 두고, 그 변경을 Edge 리전의 복제본(replica)으로 전파하는 모델을 취합니다.
Edge 복제(edge replication)의 감각은 이렇게 잡으면 이해가 빠릅니다. 쓰기는 보통 Primary에 모이고, 읽기는 가까운 복제본에서 처리해 RTT(왕복 지연)를 줄입니다. 앱이 서울이든 오리건이든, DB 클라이언트가 지리적으로 가깝다고 판단한 엔드포인트에 붙게 설계할 수 있고, JAMstack·서버리스·엣지 함수처럼 한 리전에 고정되지 않는 워크로드에 특히 잘 맞습니다. 물론 “항상 0ms”는 마케팅적 수치이고, 실제로는 캐시·연결 풀·쿼리 복잡도에 따라 달라집니다. 다만 읽기 위주나 쓰기 빈도가 낮은 제품(블로그, 대시보드, B2B 툴 프로토타입)에서는 체감이 꽤 큽니다.
정리하면, SQLite 엔진 + LibSQL의 네트워킹/복제가 Turso의 뼈대이고, Turso Cloud는 호스팅·인증·리전·토큰을 한 번에 제공하는 상위 계층이라고 보면 됩니다.
SQLite의 한계와 Turso의 보완
로컬 파일 하나에 붙는 전통 SQLite의 대표적 제약은 다음과 비슷합니다.
- 동시 쓰기: 단일 writer에 가깝다는 인상(대규모 동시 쓰기엔 RDB·분산 DB가 유리).
- 원격 접속: 기본엔 OS 파일 핸들이 전제라, “클라우드 DB URL 하나”로 쓰기엔 애초에 설계가 다름.
- 운영: 백업·리전·장애 복구를 직접 다져야 함.
Turso(및 libSQL 클라이언트)는 여기에 HTTP/RPC 스타일 접속과 호스티드 복제로 답을 붙입니다. 즉, “파일 락 하나”로 끝나는 로컬 앱이 아니라, 서비스로 제공되는 SQLite에 가깝게 쓰는 패턴이 됩니다. 다만 트랜잭션이 복수 리전에 동시에 강하게 일관되어야 한다거나, OLTP에 초당 엄청난 쓰기가 몰리는 시스템이면, 여전히 PostgreSQL·Cockroach·Spanner 쪽을 먼저 비교하는 편이 안전합니다. Turso는 읽기 확장 + 엣지 친화 + SQLite 친숙도에 강점이 있는 선택지입니다.
Edge 데이터베이스란?
Edge 데이터베이스는 계산(Edge Functions)이 사용자 가까이 가는 것처럼, 데이터 읽기 경로도 지연과 홉을 줄이는 방향으로 설계한 데이터베이스/배치 모델을 넓게 부르는 말에 가깝습니다. Turso는 그중에서도 지역 복제본을 앞에 두고, SQLite 호환으로 앱 코드 변경을 최소화하려는 쪽에 속합니다.
장점(실무 관점):
- API 레이어가 엣지에 퍼져 있을 때 DB도 멀리 두지 않으려는 니즈를 충족.
- 읽기 비중이 높은 서비스(목록, 설정, 캐시·세션 부근)에 유리.
- 인프라 단순화: 작은 팀이 Postgres 리전·연결 풀·빗장까지 한꺼번에 들이는 부담을 줄일 수 있음.
주의할 점:
- 쓰기 일관성·지연은 아키텍처를 봐야 합니다(Primary-Replica 구조의 일반적 트레이드오프).
- Postgres 전용 확장이나 복잡한 락·동시성 시나리오가 핵심이면, SQLite 계열이 맞는지 먼저 검증이 필요합니다.
Turso 시작하기
1️⃣ CLI 설치
# Turso CLI 설치 (macOS/Linux)
curl -sSfL https://get.tur.so/install.sh | bash
# Windows
iwr https://get.tur.so/install.ps1 -useb | iex
# 로그인
turso auth login
# 버전 확인
turso --version
2️⃣ 데이터베이스 생성
# DB 생성
turso db create my-database
# 생성된 DB 목록
turso db list
# DB URL 확인
turso db show my-database
# 토큰 생성
turso db tokens create my-database
3️⃣ SQL 실행
# 인터랙티브 SQL 쉘
turso db shell my-database
# 쉘에서 SQL 실행
sqlite> CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT);
sqlite> INSERT INTO users (name) VALUES ('Alice');
sqlite> SELECT * FROM users;
Node.js/TypeScript 통합
@libsql/client 사용
npm install @libsql/client
// src/db/client.ts
import { createClient } from '@libsql/client';
export const turso = createClient({
url: process.env.TURSO_DATABASE_URL!,
authToken: process.env.TURSO_AUTH_TOKEN!,
});
// 쿼리 실행
const result = await turso.execute('SELECT * FROM users');
console.log(result.rows);
// 파라미터 바인딩
const user = await turso.execute({
sql: 'SELECT * FROM users WHERE id = ?',
args: [1],
});
// 트랜잭션
await turso.batch([
{ sql: 'INSERT INTO users (name) VALUES (?)', args: ['Alice'] },
{ sql: 'INSERT INTO posts (title, user_id) VALUES (?, ?)', args: ['Hello', 1] },
]);
실전 CRUD (TypeScript)
스키마가 items 테이블 하나일 때, Create / Read / Update / Delete를 @libsql/client로만 깔끔히 묶은 예시입니다. API 라우트(Next Route Handler, Hono, Fastify 등)에서 그대로 가져다 써도 됩니다.
// src/lib/itemsRepository.ts
import { createClient, type InArgs } from '@libsql/client';
const client = createClient({
url: process.env.TURSO_DATABASE_URL!,
authToken: process.env.TURSO_AUTH_TOKEN!,
});
export type Item = {
id: number;
title: string;
body: string | null;
createdAt: string;
};
// 테이블은 미리: CREATE TABLE items (...);
const rowToItem = (row: Record<string, unknown>): Item => ({
id: Number(row.id),
title: String(row.title),
body: row.body == null ? null : String(row.body),
createdAt: String(row.created_at),
});
/** Create */
export async function createItem(title: string, body?: string) {
const r = await client.execute({
sql: `INSERT INTO items (title, body) VALUES (?, ?) RETURNING id, title, body, created_at`,
args: [title, body ?? null] as InArgs,
});
if (!r.rows[0]) throw new Error('insert failed');
return rowToItem(r.rows[0] as Record<string, unknown>);
}
/** Read (단일) */
export async function getItemById(id: number) {
const r = await client.execute({
sql: `SELECT id, title, body, created_at FROM items WHERE id = ? LIMIT 1`,
args: [id],
});
return r.rows[0] ? rowToItem(r.rows[0] as Record<string, unknown>) : null;
}
/** Read (목록) */
export async function listItems(limit = 50) {
const r = await client.execute({
sql: `SELECT id, title, body, created_at FROM items ORDER BY id DESC LIMIT ?`,
args: [limit],
});
return r.rows.map((row) => rowToItem(row as Record<string, unknown>));
}
/** Update */
export async function updateItem(id: number, title: string, body?: string) {
await client.execute({
sql: `UPDATE items SET title = ?, body = ? WHERE id = ?`,
args: [title, body ?? null, id] as InArgs,
});
return getItemById(id);
}
/** Delete */
export async function deleteItem(id: number) {
await client.execute({
sql: `DELETE FROM items WHERE id = ?`,
args: [id],
});
}
위 패턴의 장점은 ORM 없이도 바인딩·배치·에러 흐름을 제어할 수 있다는 점입니다. 팀이 Drizzle/Prisma를 이미 쓰면 아래 Drizzle ORM 통합 쪽이 유지보수에 더 맞을 수 있습니다.
Drizzle ORM 통합
설치 및 설정
npm install drizzle-orm @libsql/client
npm install -D drizzle-kit
// drizzle.config.ts
import type { Config } from 'drizzle-kit';
export default {
schema: './src/db/schema.ts',
out: './drizzle',
driver: 'turso',
dbCredentials: {
url: process.env.TURSO_DATABASE_URL!,
authToken: process.env.TURSO_AUTH_TOKEN!,
},
} satisfies Config;
스키마 정의
// src/db/schema.ts
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';
export const users = sqliteTable('users', {
id: integer('id').primaryKey({ autoIncrement: true }),
name: text('name').notNull(),
email: text('email').notNull().unique(),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
});
export const posts = sqliteTable('posts', {
id: integer('id').primaryKey({ autoIncrement: true }),
title: text('title').notNull(),
content: text('content'),
userId: integer('user_id')
.notNull()
.references(() => users.id),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
});
Drizzle 사용
// 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 });
// 사용 예제
const allUsers = await db.select().from(schema.users);
const newUser = await db.insert(schema.users).values({
name: 'Alice',
email: '[email protected]',
createdAt: new Date(),
}).returning();
Cloudflare Workers에서 사용
// worker.ts
import { Hono } from 'hono';
import { drizzle } from 'drizzle-orm/libsql';
import { createClient } from '@libsql/client/web';
import * as schema from './db/schema';
interface Env {
TURSO_DATABASE_URL: string;
TURSO_AUTH_TOKEN: string;
}
const app = new Hono<{ Bindings: Env }>();
app.get('/users', async (c) => {
const client = createClient({
url: c.env.TURSO_DATABASE_URL,
authToken: c.env.TURSO_AUTH_TOKEN,
});
const db = drizzle(client, { schema });
const users = await db.select().from(schema.users);
return c.json(users);
});
export default app;
Vercel·Netlify와의 통합
Vercel이나 Netlify에 올릴 때는, Turso 쪽은 보통 환경 변수 두 개(TURSO_DATABASE_URL, TURSO_AUTH_TOKEN)로 끝납니다. 제가 사이드 프로젝트·소규모 MVP를 올릴 때의 루틴은 대략 이렇습니다.
- Turso 대시보드/CLI에서 DB와 토큰을 만들고, 배포 콘솔에 동일 키로 넣는다.
- Edge/Node 런타임이 갈리는 프레임워크라면, DB에 붙는 코드가 어디서 실행되는지만 확인한다(예: Next.js의
edgevsnodejs). WebAssembly 전용 클라이언트 분기(@libsql/client/web)가 필요한 환경이 있는지 체크한다. - 빌드 시 마이그레이션을 돌릴지, CI에서만 돌릴지 팀 규칙을 정한다. Drizzle이면
drizzle-kit push또는 SQL 파일 배포 흐름이 흔하다. - 콜드 스타트가 잦은 서버리스라면, 연결을 매 요청마다 새로 열지 말고 모듈 스코프에 클라이언트를 두는 패턴이 유효하다(플랫폼 제한이 있으면 그에 맞게).
Netlify Edge Functions를 쓸 때도 개념은 비슷하고, Node API 라우트에 Turso를 두는 구성이 가장 덜 까다로웠습니다. “배포 자체”보다, 런타임이 TCP·fetch 제약을 어떻게 갖느냐를 한 번씩 훑어보는 게 시간을 아낍니다.
복제 설정과 다중 리전 전략
읽기 복제본 추가
# 복제본 생성 (Tokyo 리전)
turso db replicas create my-database --location nrt
# 복제본 목록
turso db replicas list my-database
# 결과:
# - iad (Primary)
# - nrt (Replica)
# - syd (Replica)
다중 리전 전략을 잡을 때는, 제품이 “읽기 많고 쓰기 적다”는지부터 솔직하게 씁니다. 예를 들어 쓰기는 한 Primary 근처에 두고, 전역 사용자에게는 읽기 복제를 붙이는 식이면 Turso의 복제 모델과 잘 맞습니다. 반대로 강한 동시 쓰기나 같은 행에 대한 경쟁이 끊이지 않으면, 앱 레벨 큐나 집계 지연 같은 설계를 같이 가져가야 합니다.
리전을 여러 개 두었다면, 앱이 어느 URL로 붙는지(호스트/라우팅)도 정리해 두어야 합니다. CDN 뒤에 API가 흩어져 있을수록, “가까운 DB 엔드포인트”를 고르는 쪽이 지연에 유리한 경우가 많습니다. 다만 이 부분은 사용자 정의 DNS·프록시와도 엮이므로, 문서에 리전 맵을 짧게라도 남기는 것을 권합니다.
Embedded Replicas
// 로컬에 SQLite 복제본 생성 (오프라인 지원)
import { createClient } from '@libsql/client';
const client = createClient({
url: 'file:./local.db',
syncUrl: process.env.TURSO_DATABASE_URL!,
authToken: process.env.TURSO_AUTH_TOKEN!,
syncInterval: 60, // 60초마다 동기화
});
// 로컬 DB에서 읽기 (즉시)
const users = await client.execute('SELECT * FROM users');
// 동기화
await client.sync();
Embedded Replica는 “디바이스나 한 프로세스 안에 로컬 SQLite 파일을 두고, 주기적·수동 동기화로 Turso와 맞춘다”는 패턴입니다. 읽기는 로컬에서 즉시 처리하고, 쓰기는 정책에 맞게 Turso 쪽으로 보내는 식의 앱(오프라인 퍼스트에 가깝다면 더욱)에 잘 맞습니다. syncInterval을 너무 촘촘하게 잡으면 모바일·엣지에서 배터리·네트워크 비용이 늘 수 있으니, 쓰기 빈도·일관성 요구를 보며 조절하는 편이 좋습니다. 프로덕션에 올리기 전에는 동기화 실패·충돌 시 UI·재시도 정책을 꼭 정해두세요.
가격·무료 티어 활용
이 글 작성 시점에 Turso는 무료 플랜에 다수 DB·월간 읽기 한도·스토리지를 넉넉히 주는 편으로 알려져 있습니다(앞선 frontmatter·FAQ 수치와 동일). 다만 SaaS 요금은 수시로 변할 수 있으니, 배포 전 항상 Turso 가격 페이지와 대시보드를 다시 확인하세요.
무료 티어를 쓸 때 제가 챙기는 팁은 다음과 같습니다.
- DB를 남용해 수백 개로 쪼개기보다, 스키마·환경(staging) 단위로 합리적으로 나누기.
- 읽기 폭주가 예상되면, 캐시 레이어(HTTP 캐시, KV, CDN)와 역할을 나눠 DB 읽기 횟수를 절약하기.
- 마이그레이션과 로컬 스택을 맞춰 두어, 실수로 스테이징/프로덕을 뒤섞지 않기.
유료로 넘어갈 때는 리전·스토리지·읽기/쓰기 단가뿐 아니라, 지원·SLA·백업 주기가 팀에 맞는지도 같이 보시길 권합니다.
프로덕션에서 느낀 점
솔직히 말하면, Turso는 마법의 해결책이 아니라 잘 맞는 문제가 있는 도구입니다. 저·중 트래픽, 읽기 중심, SQLite·Drizzle 생태에 이미 투자한 팀이면 DX가 좋고, 온콜이 “왜 DB 커넥션이…”로 시작하는 밤을 줄이는 데도 도움이 됐습니다. 반면 초기부터 PostgreSQL 락인(특정 확장, JSON 연산, 복잡한 동시성)이 강한 시스템이면, 이주 비용을 따져야 합니다.
운영 측면에서 의외로 큰 이득은 “로컬은 SQLite, 클라우드는 Turso”처럼 같은 SQL 머릿속으로 dev/prod를 맞출 수 있다는 점이었습니다. 마이그레이션을 CI에 묶고, 시드 스크립트만 지키면, 팀 온보딩이 빨랐습니다. 한편 쓰기 지연이나 리전 정책을 제품 요구와 잘 맞는지, PoC로 스파이크한 뒤 본다고 생각지 않으면 나중에 아픈 지점이 됩니다. 저는 대시보드·내부툴·토이프로젝트엔 꽤 자신 있게 꺼내고, 정산·재고처럼 쓰기 경쟁이 격한 도메인은 다른 DB와 비교표를 먼저 그립니다.
핵심 정리
✅ Turso의 장점
- Edge 최적화: 전 세계 35개 리전 자동 복제
- 0ms 읽기: 사용자 근처에서 로컬 읽기
- SQLite 호환: 익숙한 SQL 문법
- Drizzle ORM: 완벽한 통합
- 무료 플랜: 500개 DB, 10억 행 읽기
🚀 다음 단계
- Turso 공식 문서에서 심화 학습
- LibSQL GitHub에서 소스 탐색
- Discord에서 커뮤니티 참여
시작하기:
turso db create my-db로 5분 만에 Edge Database를 생성하고, 전 세계에서 0ms 읽기 성능을 경험하세요! 🚀