Turso & libSQL 완벽 가이드 — SQLite를 엣지로, 임베디드 복제와 초저지연 DB

Turso & libSQL 완벽 가이드 — SQLite를 엣지로, 임베디드 복제와 초저지연 DB

이 글의 핵심

Turso는 SQLite fork인 libSQL을 기반으로 하는 글로벌 분산 데이터베이스입니다. HTTP 프로토콜로 서버리스·엣지 런타임에 친화적이고, "임베디드 복제"로 애플리케이션이 로컬 SQLite 파일에서 읽으며 백그라운드로 엣지와 동기화해 지연 0에 가까운 읽기를 제공합니다. 이 글은 Turso의 핵심 개념·libSQL 차이·SDK 사용·Vector Search·프로덕션 패턴을 다룹니다.

Turso / libSQL이 해결하는 문제

전통적 DB 설계는 “중앙 서버 1개, 클라이언트가 네트워크로 연결” 입니다. 엣지 런타임·서버리스 시대에는 다음 한계가 드러났습니다.

  1. Cold start의 DB 연결 지연: TCP handshake + TLS + 인증
  2. 지리적 거리: DB가 미국 동부, 사용자가 서울 → 왕복 200ms
  3. Serverless에서 persistent connection 어려움: 풀 관리가 까다롭고 비쌈
  4. 멀티테넌트 복잡성: 테넌트 격리를 위한 schema 분리·RLS 오버헤드

Turso는 이를 뒤집어:

  • HTTP 프로토콜: Fetch로 연결, cold start 수십ms
  • 글로벌 리전: 30+ 지역에 primary/replica 배치
  • 임베디드 복제: 애플리케이션이 로컬 SQLite를 직접 읽어 0ms
  • 테넌트당 DB: 몇 초만에 DB 하나 생성, 프로젝트당 수천 개 가능

설치·프로젝트 생성

# CLI
curl -sSfL https://get.tur.so/install.sh | bash
turso auth signup

# 첫 DB
turso db create my-app --location nrt   # 도쿄
turso db show my-app
turso db tokens create my-app

SDK (Node/Bun/Deno/Workers 공용):

pnpm add @libsql/client

기본 사용

import { createClient } from "@libsql/client"

const db = createClient({
  url: process.env.TURSO_URL!,        // libsql://xxx.turso.io
  authToken: process.env.TURSO_TOKEN!,
})

await db.execute(`
  CREATE TABLE IF NOT EXISTS posts (
    id INTEGER PRIMARY KEY,
    title TEXT NOT NULL,
    body TEXT,
    created_at INTEGER DEFAULT (strftime('%s', 'now'))
  )
`)

await db.execute({
  sql: "INSERT INTO posts (title, body) VALUES (?, ?)",
  args: ["Hello Turso", "First post"],
})

const { rows } = await db.execute("SELECT * FROM posts ORDER BY id DESC LIMIT 10")
console.log(rows)

SQLite이므로 ? placeholder·LIMIT·strftime 등 표준 문법 모두 그대로.

트랜잭션

const tx = await db.transaction("write")
try {
  await tx.execute("UPDATE posts SET title = ? WHERE id = ?", ["new", 1])
  await tx.execute("INSERT INTO audit (action) VALUES (?)", ["update"])
  await tx.commit()
} catch (err) {
  await tx.rollback()
  throw err
}

Batch

await db.batch([
  "DELETE FROM posts WHERE id = 1",
  { sql: "INSERT INTO posts (title) VALUES (?)", args: ["B"] },
], "write")

Batch는 single round-trip으로 여러 문장을 보냅니다.

임베디드 복제 — 지연 0ms 읽기

import { createClient } from "@libsql/client"

const db = createClient({
  url: "file:local-replica.db",          // 로컬 파일
  syncUrl: process.env.TURSO_URL,         // 원격 primary
  authToken: process.env.TURSO_TOKEN,
  syncInterval: 60,                        // 60초마다 자동 pull
})

// 즉시 쿼리 (로컬 파일에서 0ms)
const { rows } = await db.execute("SELECT * FROM posts LIMIT 10")

// 쓰기는 자동으로 원격으로 전송
await db.execute({
  sql: "INSERT INTO posts (title) VALUES (?)",
  args: ["From edge"],
})

// 수동 동기화 (최신 데이터 필요 시)
await db.sync()

Node/Bun의 장기 실행 프로세스에 완벽히 맞습니다. Next.js의 standalone 서버, long-running 워커, Astro SSR 등이 대표 활용처입니다.

멀티테넌트 아키텍처

Turso의 가장 독창적인 가치 제안입니다. 테넌트당 완전히 분리된 SQLite DB:

import { createClient as createAdmin } from "@tursodatabase/api"

const admin = createAdmin({
  org: "my-org",
  token: process.env.TURSO_API_TOKEN!,
})

// 회원가입 시 DB 생성
async function createTenant(tenantId: string) {
  const name = `tenant-${tenantId}`
  await admin.databases.create(name, {
    group: "default",
    schema: "tenant-template",   // 템플릿 DB로부터 스키마 복제
  })
  const token = await admin.databases.createToken(name, {
    expiration: "never",
    authorization: "full-access",
  })
  // token을 테넌트 메타 테이블에 저장
  return { dbName: name, token }
}
  • 스토리지·권한·백업이 테넌트별 완전 분리
  • “한 테넌트의 느린 쿼리가 다른 테넌트에 영향”이 0
  • 해지 시 DB 한 줄 삭제로 데이터 완전 제거(GDPR 친화)
  • 수천 테넌트가 있어도 비용은 실제 사용 스토리지 기준

Vector Search (libSQL Native)

libSQL은 SQLite에 F32/F16 벡터 컬럼과 ANN 인덱스를 네이티브로 추가했습니다.

CREATE TABLE documents (
  id INTEGER PRIMARY KEY,
  title TEXT,
  embedding F32_BLOB(1024)
);

CREATE INDEX docs_idx ON documents(
  libsql_vector_idx(embedding, 'metric=cosine')
);

-- 유사도 검색
SELECT id, title, vector_distance_cos(embedding, vector32(?)) AS score
FROM documents
ORDER BY embedding <#> vector32(?)
LIMIT 10;
import { createClient } from "@libsql/client"
const db = createClient({ ... })

async function addDoc(title: string, text: string) {
  const embedding = await embed(text)  // OpenAI/Workers AI 등
  await db.execute({
    sql: "INSERT INTO documents (title, embedding) VALUES (?, vector32(?))",
    args: [title, JSON.stringify(embedding)],
  })
}

async function search(query: string) {
  const qvec = await embed(query)
  const { rows } = await db.execute({
    sql: `
      SELECT id, title, vector_distance_cos(embedding, vector32(?)) AS score
      FROM documents
      ORDER BY embedding <#> vector32(?)
      LIMIT 10`,
    args: [JSON.stringify(qvec), JSON.stringify(qvec)],
  })
  return rows
}

“DB 한 통에 관계형+벡터” 가 동시에 가능해 RAG 애플리케이션의 스택이 크게 단순해집니다.

Cloudflare Workers 통합

import { Hono } from "hono"
import { createClient } from "@libsql/client/web"

type Bindings = { TURSO_URL: string; TURSO_TOKEN: string }
const app = new Hono<{ Bindings: Bindings }>()

app.get("/posts", async (c) => {
  const db = createClient({
    url: c.env.TURSO_URL,
    authToken: c.env.TURSO_TOKEN,
  })
  const { rows } = await db.execute("SELECT * FROM posts ORDER BY id DESC LIMIT 20")
  return c.json(rows)
})

export default app

Workers에서는 임베디드 복제는 불가능하지만, Turso의 도쿄/싱가포르/프랑크푸르트 엣지와 Workers 엣지가 대부분 같은 지역이라 왕복이 10-30ms로 매우 빠릅니다.

Next.js App Router

// lib/db.ts
import { drizzle } from "drizzle-orm/libsql"
import { createClient } from "@libsql/client"

const client = createClient({
  url: process.env.TURSO_URL!,
  authToken: process.env.TURSO_TOKEN!,
})

export const db = drizzle(client)
// app/posts/page.tsx
import { db } from "@/lib/db"
import { posts } from "@/db/schema"
import { desc } from "drizzle-orm"

export default async function Page() {
  const rows = await db.select().from(posts).orderBy(desc(posts.id)).limit(20)
  return <ul>{rows.map(p => <li key={p.id}>{p.title}</li>)}</ul>
}

Drizzle ORM의 libSQL 드라이버는 일급 지원이고, Prisma도 2024년 중반부터 지원합니다.

Drizzle 스키마 & 마이그레이션

// db/schema.ts
import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core"

export const posts = sqliteTable("posts", {
  id: integer("id").primaryKey({ autoIncrement: true }),
  title: text("title").notNull(),
  body: text("body"),
  createdAt: integer("created_at").default(sql`(strftime('%s', 'now'))`).notNull(),
})
pnpm drizzle-kit generate
pnpm drizzle-kit migrate

백업·복구

Turso는 자동으로 point-in-time 복구를 지원합니다(유료).

# 즉시 덤프
turso db shell my-app .dump > backup.sql

# 특정 시점 복구
turso db shell my-app --from-db-state '{"name":"my-app","timestamp":"2026-04-10T12:00:00Z"}' .schema

임베디드 복제 파일 자체도 백업으로 쓸 수 있어 “일반 SQLite 툴로” 분석·이관이 가능한 것이 큰 장점입니다.

보안 / 멀티 DB 토큰

# 읽기 전용 토큰
turso db tokens create my-app --readonly

# 특정 attachDB만 접근 가능
turso db tokens create my-app --attach

각 클라이언트(프런트/BFF/워커)에 필요한 최소 권한 토큰만 발급하세요.

성능 팁

  1. 임베디드 복제 쓰기 — 가능한 모든 장기 실행 환경에서 활성화
  2. 인덱스 — SQLite의 쿼리 플래너는 작지만 강력; EXPLAIN QUERY PLAN 적극 사용
  3. 배치 쓰기db.batch()로 여러 INSERT를 한 번에
  4. WAL 체크포인트 — Turso는 자동 관리
  5. JSON 컬럼TEXT + json_extract로 간단한 문서 저장 패턴
  6. Vector 인덱스 빌드 — 대량 insert 후 한 번에 CREATE INDEX

한계·주의

  • 큰 쓰기 처리량: SQLite 기반이라 단일 writer. 초당 수만 write가 필요하면 Postgres/Mysql이 적합
  • 복잡한 동시 트랜잭션: Serializable 수준이지만 Postgres만큼의 MVCC는 아님
  • 쿼리 능력: Window function·CTE는 있으나 Postgres만큼 풍부하지는 않음
  • Workers 임베디드 복제 불가: fs 없음 → 일반 원격 모드만

트러블슈팅

SQLITE_BUSY

여러 writer가 동시에 접근 시 발생. Turso는 기본적으로 큐잉하지만 장기적으로는 write path를 serialize하거나 재시도 백오프 구현.

임베디드 복제 파일 증가

WAL + 여러 복제 포인트로 파일이 커질 수 있음. 주기적 PRAGMA wal_checkpoint(TRUNCATE) 또는 파일 재생성.

벡터 검색 속도 저하

  • ANN 인덱스가 실제로 사용되는지 EXPLAIN QUERY PLAN 확인
  • 임베딩 차원이 너무 높으면(>1536) 검색 성능 저하 → 차원 축소

Workers에서 timeout

  • 원격 리전이 너무 멀 때. turso db locations list로 가까운 그룹 선택
  • 쿼리가 실제로 큰지 확인 → .limit(N) 강제

체크리스트

  • 프로덕션 DB의 primary 리전을 주 사용자 위치와 일치
  • 장기 실행 서버에서는 임베디드 복제 활성화
  • 테넌트당 DB 패턴 고려 (SaaS)
  • Vector 컬럼으로 RAG 인프라 단순화
  • Drizzle ORM + 마이그레이션 자동화
  • 백업 정책 (자동 PITR + .dump 주기적)
  • 토큰 최소 권한
  • 모니터링: 요청 수·지연·스토리지

마무리

Turso는 “엣지·서버리스·멀티테넌트”라는 2020년대 백엔드 요구에 SQLite라는 40년 검증된 엔진을 재해석해 답한 매력적인 DB입니다. 임베디드 복제라는 혁신적 패턴이 “DB는 네트워크 건너편에 있다”는 전제를 뒤집고, 테넌트당 DB라는 과감한 모델이 멀티테넌트 설계를 단순화합니다. 모든 워크로드에 맞지는 않지만, 블로그·노트 앱·SaaS 관리 패널·RAG 파일럿 같은 전형적 현대 웹 애플리케이션에서는 Postgres보다 훨씬 가볍고 빠른 선택이 될 수 있습니다. 첫 DB 생성·첫 쿼리까지 5분이면 충분하니 오늘 한 번 시도해보세요.

관련 글

  • Cloudflare D1 완벽 가이드
  • Neon Postgres Serverless 가이드
  • SQLite 완벽 가이드
  • 엣지 컴퓨팅 완벽 가이드