🗄️ 데이터베이스

Turso

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로 데이터베이스 설정

# 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

TypeScript/Node.js에서 Turso 사용

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;
  }
}

Next.js App Router에서 Turso 활용

// 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})
  • ))}
); }

🗣️ 실무에서 이렇게 말해요

  • "엣지에서 API 레이턴시 줄이려고 Turso 도입 검토해봤는데, Vercel Edge Functions랑 조합하면 50ms 이하로 나와요"
  • "Embedded Replica 켜면 읽기가 로컬에서 처리되니까 데이터베이스 연결 없이도 조회가 돼요"
  • "SaaS 멀티테넌시 구현할 때 Database per User 패턴이 편한데, Turso 무료 플랜이 500개 DB까지 지원해서 초기에 부담 없어요"
  • "libSQL 기반이라 SQLite 마이그레이션 도구 그대로 쓸 수 있어서 전환이 쉬웠어요"
  • "Turso는 SQLite의 오픈소스 포크인 libSQL을 기반으로 한 엣지 분산 데이터베이스입니다. 전 세계 엣지 로케이션에서 복제본을 운영하여 초저지연 응답을 제공합니다."
  • "Embedded Replicas는 앱 프로세스 내에 SQLite 파일을 로컬로 유지하여 네트워크 왕복 없이 읽기를 처리하고, 쓰기만 Primary로 라우팅하는 아키텍처입니다."
  • "기존 SQLite와 달리 Turso는 HTTP를 통한 원격 접근, 다중 리전 복제, 그리고 서버 모드를 지원하여 분산 환경에서 사용할 수 있습니다."
  • "멀티테넌시 시나리오에서 테넌트별 독립 DB를 쉽게 생성/삭제할 수 있고, 그룹 단위로 스키마 마이그레이션을 일괄 적용할 수 있습니다."
  • "syncInterval 60초면 실시간 데이터가 아닌 경우엔 괜찮은데, 재고 같은 민감한 데이터는 수동 sync() 호출 추가하는 게 좋겠어요"
  • "배치 작업은 client.batch() 쓰면 라운드트립 줄일 수 있어요. 개별 execute 10번보다 훨씬 빨라요"
  • "Drizzle 스키마에 sqliteTable 쓰면 Turso랑 로컬 SQLite 둘 다 호환되니까 테스트 환경 구성이 편해져요"
  • "트랜잭션 타입을 'write'로 명시해야 Primary로 라우팅됩니다. 기본이 'read'라서 쓰기가 실패할 수 있어요"

⚠️ 주의사항

🔗 관련 용어

📚 더 배우기