Turso
Turso
SQLite 기반 엣지 데이터베이스. libSQL 사용, 복제 지원.
Turso
SQLite 기반 엣지 데이터베이스. libSQL 사용, 복제 지원.
Turso는 ChiselStrike가 개발한 SQLite 기반의 엣지 분산 데이터베이스입니다. SQLite의 오픈소스 포크인 libSQL을 핵심 엔진으로 사용하여 SQLite의 간결함과 호환성을 유지하면서도 서버 모드, 복제, 확장 기능 등 현대적인 분산 데이터베이스 기능을 제공합니다. 전 세계 35개 이상의 엣지 로케이션에서 데이터베이스 복제본을 운영하여 사용자에게 가장 가까운 위치에서 밀리초 단위의 응답 시간을 보장합니다.
Turso의 핵심 혁신은 Embedded Replicas 기능입니다. 이는 애플리케이션 프로세스 내에 SQLite 데이터베이스 파일의 로컬 복제본을 유지하여 네트워크 왕복 없이 읽기 쿼리를 처리할 수 있게 합니다. 쓰기 작업은 Primary 데이터베이스로 라우팅되고, 변경사항은 자동으로 모든 복제본에 동기화됩니다. 이 방식은 특히 Vercel, Cloudflare Workers, Fly.io 등의 엣지 컴퓨팅 환경에서 극적인 성능 향상을 가져옵니다.
Multi-tenancy를 위해 Turso는 "Database per User" 패턴을 지원합니다. 각 테넌트에 대해 독립된 데이터베이스 인스턴스를 생성하여 데이터 격리를 보장하고, 개별 테넌트의 백업/복원/마이그레이션이 간단해집니다. Schema Migrations는 그룹 내 모든 데이터베이스에 동시에 적용할 수 있어 SaaS 애플리케이션 개발에 적합합니다. 월 500개 데이터베이스까지 무료로 사용할 수 있어 개인 프로젝트와 스타트업에게 매력적입니다.
개발자 경험 측면에서 Turso는 Drizzle ORM, Prisma, SQLAlchemy 등 주요 ORM과의 통합을 지원하며, TypeScript/JavaScript, Rust, Python, Go 등 다양한 언어용 SDK를 제공합니다. Turso CLI를 통해 데이터베이스 생성, 복제본 배포, 성능 모니터링을 간편하게 수행할 수 있습니다. 또한 SQLite 호환성 덕분에 로컬 개발 시 일반 SQLite를 사용하고 프로덕션에서만 Turso로 전환하는 것이 가능해 개발/테스트 환경 구성이 단순합니다.
# Turso CLI 설치
curl -sSfL https://get.tur.so/install.sh | bash
# 로그인
turso auth login
# 데이터베이스 생성 (가장 가까운 리전에 자동 배포)
turso db create my-app-db
# 다른 리전에 복제본 추가
turso db replicate my-app-db nrt # 도쿄
turso db replicate my-app-db icn # 서울
turso db replicate my-app-db sin # 싱가포르
# 데이터베이스 URL 확인
turso db show my-app-db --url
# 인증 토큰 생성
turso db tokens create my-app-db
# 데이터베이스 셸 접속
turso db shell my-app-db
# 그룹 생성 (멀티테넌시)
turso group create my-saas-group
turso db create tenant-001 --group my-saas-group
turso db create tenant-002 --group my-saas-group
import { createClient } from "@libsql/client";
import { drizzle } from "drizzle-orm/libsql";
import { sql } from "drizzle-orm";
// 1. 기본 클라이언트 연결
const client = createClient({
url: process.env.TURSO_DATABASE_URL!,
authToken: process.env.TURSO_AUTH_TOKEN,
});
// 2. Embedded Replica 설정 (로컬 복제본으로 읽기 최적화)
const clientWithReplica = createClient({
url: process.env.TURSO_DATABASE_URL!,
authToken: process.env.TURSO_AUTH_TOKEN,
syncUrl: process.env.TURSO_DATABASE_URL, // 동기화 대상
syncInterval: 60, // 60초마다 자동 동기화
});
// 수동 동기화 트리거
await clientWithReplica.sync();
// 3. 기본 쿼리 실행
async function basicQueries() {
// 테이블 생성
await client.execute(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`);
// 데이터 삽입 (Prepared Statement)
const result = await client.execute({
sql: "INSERT INTO users (email, name) VALUES (?, ?)",
args: ["user@example.com", "홍길동"],
});
console.log("Inserted ID:", result.lastInsertRowid);
// 배치 삽입 (트랜잭션)
await client.batch([
{ sql: "INSERT INTO users (email, name) VALUES (?, ?)", args: ["a@test.com", "사용자A"] },
{ sql: "INSERT INTO users (email, name) VALUES (?, ?)", args: ["b@test.com", "사용자B"] },
{ sql: "INSERT INTO users (email, name) VALUES (?, ?)", args: ["c@test.com", "사용자C"] },
], "write");
// 데이터 조회
const users = await client.execute("SELECT * FROM users LIMIT 10");
for (const row of users.rows) {
console.log(`${row.id}: ${row.name} (${row.email})`);
}
}
// 4. Drizzle ORM과 함께 사용
import { integer, text, sqliteTable } from "drizzle-orm/sqlite-core";
const users = sqliteTable("users", {
id: integer("id").primaryKey({ autoIncrement: true }),
email: text("email").notNull().unique(),
name: text("name").notNull(),
createdAt: text("created_at").default(sql`CURRENT_TIMESTAMP`),
});
const db = drizzle(client, { schema: { users } });
async function drizzleQueries() {
// 삽입
await db.insert(users).values({
email: "drizzle@test.com",
name: "Drizzle User",
});
// 조회 with 조건
const results = await db
.select()
.from(users)
.where(sql`name LIKE ${"홍%"}`);
return results;
}
// 5. 트랜잭션 처리
async function transferCredits(fromId: number, toId: number, amount: number) {
const transaction = await client.transaction("write");
try {
await transaction.execute({
sql: "UPDATE accounts SET credits = credits - ? WHERE id = ?",
args: [amount, fromId],
});
await transaction.execute({
sql: "UPDATE accounts SET credits = credits + ? WHERE id = ?",
args: [amount, toId],
});
await transaction.commit();
console.log("Transfer successful");
} catch (error) {
await transaction.rollback();
throw error;
}
}
// lib/turso.ts - Turso 클라이언트 설정
import { createClient } from "@libsql/client/web";
export const turso = createClient({
url: process.env.TURSO_DATABASE_URL!,
authToken: process.env.TURSO_AUTH_TOKEN,
});
// app/api/users/route.ts - API Route
import { NextRequest, NextResponse } from "next/server";
import { turso } from "@/lib/turso";
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const page = parseInt(searchParams.get("page") || "1");
const limit = 20;
const offset = (page - 1) * limit;
const result = await turso.execute({
sql: "SELECT * FROM users ORDER BY created_at DESC LIMIT ? OFFSET ?",
args: [limit, offset],
});
const countResult = await turso.execute("SELECT COUNT(*) as total FROM users");
const total = countResult.rows[0].total as number;
return NextResponse.json({
users: result.rows,
pagination: {
page,
limit,
total,
totalPages: Math.ceil(total / limit),
},
});
}
export async function POST(request: NextRequest) {
const body = await request.json();
const { email, name } = body;
try {
const result = await turso.execute({
sql: "INSERT INTO users (email, name) VALUES (?, ?)",
args: [email, name],
});
return NextResponse.json({
id: result.lastInsertRowid,
message: "User created"
}, { status: 201 });
} catch (error: any) {
if (error.message.includes("UNIQUE constraint")) {
return NextResponse.json({ error: "Email already exists" }, { status: 409 });
}
throw error;
}
}
// app/users/page.tsx - Server Component
import { turso } from "@/lib/turso";
interface User {
id: number;
email: string;
name: string;
created_at: string;
}
export default async function UsersPage() {
const result = await turso.execute("SELECT * FROM users ORDER BY created_at DESC LIMIT 50");
const users = result.rows as unknown as User[];
return (
사용자 목록
{users.map((user) => (
-
{user.name} ({user.email})
))}
);
}