Turso / libSQL 패턴
빠른 가이드: 모든 Turso 데이터베이스 접근에는
@libsql/client를 사용하세요. 단일 쿼리에는execute(), 원자적 다중 명령문 작업(대화형 트랜잭션보다 선호)에는batch(), 이후 쿼리가 이전 결과에 의존하는 경우에만transaction()을 사용하세요. 파일 시스템 접근이 없는 에지/서버리스 런타임의 경우@libsql/client/web에서 임포트하세요. 제로 지연 시간 읽기를 위해 로컬 파일 URL +syncUrl로 임베디드 복제본을 구성하세요. 모든 쓰기는 프라이머리로 전달됩니다 — 15-50ms 쓰기 지연 시간을 염두에 두고 설계하세요. Turso는 내부적으로 SQLite입니다: 단일 작성자 모델,ALTER TABLE ... ADD CONSTRAINT없음, 저장 프로시저 없음.
<critical_requirements>
중요: 이 스킬을 사용하기 전에
모든 코드는 CLAUDE.md의 프로젝트 규칙을 따라야 합니다 (kebab-case, 명명된 내보내기, 임포트 순서,
import type, 명명된 상수)
(다중 명령문 원자적 작업에는 트랜잭션 모드와 함께 batch()를 반드시 사용하세요 — 단일 왕복으로 실행되어 대화형 transaction()보다 빠르고 안전합니다)
(파일 시스템 접근이 없는 에지/서버리스 런타임(Cloudflare Workers, Vercel Edge Functions)에서는 반드시 @libsql/client/web에서 임포트해야 합니다 — 기본 @libsql/client 임포트는 이러한 환경에서 실패하는 네이티브 바인딩을 가져옵니다)
(batch() 및 transaction()의 두 번째 인수로 트랜잭션 모드("write", "read", 또는 "deferred")를 반드시 지정해야 합니다 — 기본값은 "deferred"이며, INSERT/UPDATE/DELETE에 대한 쓰기 잠금 획득에 실패할 수 있습니다)
(짧은 수명의 프로세스에서 클라이언트가 더 이상 필요하지 않으면 반드시 client.close()를 호출해야 합니다 — 열린 클라이언트는 연결과 파일 핸들을 유지합니다)
(클라이언트가 실행 중인 동안 로컬 임베디드 복제본 데이터베이스 파일에 직접 접근해서는 안 됩니다 — 동시 접근은 데이터 손상을 초래합니다)
</critical_requirements>
자동 감지: Turso, libSQL, @libsql/client, createClient, turso.io, 임베디드 복제본, syncUrl, syncInterval, turso db, turso group, libsql, .turso.io, TURSO_DATABASE_URL, TURSO_AUTH_TOKEN
사용해야 할 때:
- 모든 런타임(Node.js, 에지, 서버리스)에서 Turso 호스팅 SQLite 데이터베이스 쿼리
- 원격 프라이머리에서 동기화된 제로 지연 시간 로컬 읽기를 위한 임베디드 복제본 설정
- 테넌트당 데이터베이스를 사용하는 멀티 테넌트 SaaS (Turso는 수백만 개의 데이터베이스 지원)
- 연결 풀링 복잡성 없이 데이터베이스가 필요한 서버리스/에지 함수
batch()또는 대화형transaction()을 사용한 원자적 다중 명령문 작업 실행- Turso CLI를 통한 데이터베이스 그룹 및 다중 리전 배치 관리
사용하지 말아야 할 때:
- 강력한 다중 작성자 일관성이 필요한 쓰기 집약적 워크로드 (Turso는 단일 작성자, 쓰기는 프라이머리로 전달)
- PostgreSQL 기능이 필요한 복잡한 관계형 쿼리 (변형 서브쿼리가 있는 CTE, 저장 프로시저, 고급 제약 조건)
- 여러 데이터베이스에 걸친 복잡한 분산 트랜잭션
- 대규모 분석 데이터셋 (SQLite 행 크기 및 동시성 제한 적용)
상세 리소스:
- examples/core.md — 클라이언트 설정, execute, batch, 트랜잭션, 임포트 경로
- examples/embedded-replicas.md — 로컬 복제본, 동기화, 오프라인 모드, 암호화
- reference.md — 의사 결정 프레임워크, 타입 정의, CLI 명령어, 조회 테이블
철학
Turso는 libSQL(SQLite의 포크)을 다중 리전 복제를 갖춘 관리형 서비스로 호스팅하여 SQLite를 에지로 가져옵니다. @libsql/client 드라이버는 원격 Turso 데이터베이스, 로컬 SQLite 파일, 인메모리 데이터베이스 또는 원격 프라이머리에서 동기화되는 임베디드 복제본에 연결하든 동일하게 작동하는 통합 API를 제공합니다.
핵심 원칙:
- 트랜잭션보다 배치 —
batch()는 모든 명령문을 단일 왕복으로 보내고 암시적 트랜잭션에서 실행합니다. 대화형transaction()은 여러 왕복이 필요하고 데이터베이스 잠금을 유지합니다(5초 타임아웃). 쿼리 간에 조건부 로직이 필요하지 않으면batch()를 사용하세요. - 쓰기는 항상 프라이머리에 도달 — 임베디드 복제본을 사용하더라도 쓰기는 원격 프라이머리 데이터베이스로 전달됩니다. 쓰기 지연 시간은 프라이머리 리전까지의 거리에 따라 15-50ms입니다. 이에 맞게 설계하세요: 낙관적 UI, 백그라운드 동기화, 쓰기 집약적인 핫 경로 피하기.
- 읽기를 위한 임베디드 복제본 — 원격 프라이머리에서 동기화된 로컬 SQLite 파일입니다. 읽기는 마이크로초 수준입니다. 쓰기는 원격으로 전달됩니다. 성공적인 쓰기 후 로컬 파일이 업데이트됩니다(쓰기 후 읽기 일관성).
- 두 가지 임포트 경로 —
@libsql/client는 Node.js용 네이티브 SQLite 바인딩을 포함하고file:URL을 지원합니다.@libsql/client/web은 에지 런타임(Cloudflare Workers, Vercel Edge Functions)용 순수 JS/WASM이며 로컬 파일을 열 수 없습니다. - SQLite 의미론 — Turso는 SQLite입니다.
ADD CONSTRAINT없음, 저장 프로시저 없음,LISTEN/NOTIFY없음, 단일 작성자 WAL 모드. Turso를 선택하기 전에 SQLite의 제한 사항을 알고 있어야 합니다.
핵심 패턴
패턴 1: 클라이언트 설정
createClient()로 클라이언트를 생성합니다. url은 연결 유형을 결정합니다: libsql://는 원격 Turso용, file:은 로컬 SQLite용(Node.js 전용), :memory:는 인메모리용(테스트). 항상 authToken에 환경 변수를 사용하세요 — 절대 자격 증명을 하드코딩하지 마세요.
전체 설정 패턴(싱글톤 모듈 및 잘못된 예 포함)은 examples/core.md를 참조하세요.
패턴 2: 쿼리 실행
execute()는 단일 SQL 문을 실행합니다. 항상 args와 함께 매개변수화된 쿼리를 사용하세요 — 절대 문자열 보간법을 사용하지 마세요.
// 위치 기반: args를 배열로
await client.execute({
sql: "SELECT * FROM users WHERE id = ?",
args: [userId],
});
// 명명된 기반: args를 객체로 (베어 이름은 SQL에서 :name, @name, $name과 일치)
await client.execute({
sql: "INSERT INTO users (name, email) VALUES (:name, :email)",
args: { name, email },
});
rows(Array<Row>), columns, rowsAffected, lastInsertRowid(bigint)가 포함된 ResultSet을 반환합니다. 타입이 지정된 결과 매핑 및 잘못된 예는 examples/core.md를 참조하세요.
패턴 3: 배치 작업
batch()는 단일 왕복으로 여러 명령문을 원자적으로 실행합니다. 모두 성공하거나 모두 롤백됩니다. 항상 두 번째 인수로 트랜잭션 모드를 지정하세요.
const results = await client.batch(
[
{ sql: "INSERT INTO users (name) VALUES (?)", args: ["Alice"] },
{
sql: "INSERT INTO audit_log (action, entity_id) VALUES (?, last_insert_rowid())",
args: ["user_created"],
},
],
"write", // INSERT/UPDATE/DELETE에 필수
);
변이에는 "write", SELECT 전용에는 "read"(복제본에서 병렬 실행 가능), 읽기 전용으로 시작하여 필요한 경우 에스컬레이션하려면 "deferred"를 사용하세요. last_insert_rowid()는 동일한 배치 내의 명령문에서 작동합니다. 다중 삽입 및 읽기 전용 배치 패턴은 examples/core.md를, 트랜잭션 모드 비교 테이블은 reference.md를 참조하세요.
패턴 4: 대화형 트랜잭션
오직 이후 쿼리가 이전 쿼리의 결과에 의존하는 경우에만 transaction()을 사용하세요. 데이터베이스 잠금을 유지하고(5초 유휴 타임아웃) 여러 왕복이 필요합니다. 항상 try/catch/finally와 함께 close()를 사용하세요.
const tx = await client.transaction("write");
try {
const { rows } = await tx.execute({
sql: "SELECT balance FROM accounts WHERE id = ?",
args: [fromId],
});
// ... 결과에 따른 조건부 로직 ...
await tx.commit();
} catch (error) {
await tx.rollback();
throw error;
} finally {
tx.close();
}
모든 명령문이 조건부 로직 없이 사전에 알려져 있다면 대신 batch()를 사용하세요. 재고 확인이 포함된 완전한 구매 예제는 examples/core.md를 참조하세요.
패턴 5: 다양한 런타임을 위한 임포트 경로
// Node.js, Bun, Deno (파일 시스템 접근 가능, 네이티브 바인딩)
import { createClient } from "@libsql/client";
// 파일 시스템이 없는 에지/서버리스 런타임 (Cloudflare Workers, Vercel Edge)
import { createClient } from "@libsql/client/web";
기본 @libsql/client는 에지 런타임에서 실패하는 네이티브 SQLite 바인딩을 번들로 제공합니다. @libsql/client/web은 순수 JS/WASM이지만 로컬 file: URL을 열 수 없습니다. 전체 Cloudflare Worker 예제는 examples/core.md를 참조하세요.
패턴 6: 임베디드 복제본
원격 Turso 프라이머리에서 동기화되는 로컬 SQLite 파일입니다. 읽기는 로컬(마이크로초), 쓰기는 원격으로 전달(15-50ms).
const client = createClient({
url: "file:local-replica.db",
syncUrl: process.env.TURSO_DATABASE_URL,
authToken: process.env.TURSO_AUTH_TOKEN,
syncInterval: 60, // 60초마다 자동 동기화
});
await client.sync(); // 첫 번째 읽기 전에 로컬 복제본 채우기
핵심 사항: 로컬 복제본에는 file: URL, 원격 프라이머리에는 syncUrl, 시작 시 sync() 호출, 읽기는 로컬, 쓰기는 원격으로 전달(쓰기 후 읽기 일관성).
사용해야 할 때: VM, VPS, 컨테이너 또는 파일 시스템 접근이 가능한 장기 실행 프로세스. 파일 시스템이 없는 서버리스/에지 런타임에서는 사용할 수 없습니다. 수동 동기화, 오프라인 모드 및 암호화를 포함한 전체 패턴은 examples/embedded-replicas.md를 참조하세요.
패턴 7: 데이터베이스 그룹 및 다중 리전
Turso는 데이터베이스를 그룹으로 구성합니다. 각 그룹에는 기본 리전과 선택적 복제본 위치가 있습니다. 그룹의 모든 데이터베이스는 해당 위치를 상속받습니다.
어떤 복제본이 읽기를 처리하든 모든 쓰기는 기본 리전으로 이동합니다. 더 많은 위치를 추가하면 전역적으로 읽기 지연 시간이 개선되지만 쓰기 지연 시간은 줄어들지 않습니다. 쓰기 지연 시간은 기본 리전까지의 거리에 따라 결정됩니다.
전체 Turso CLI 명령어 참조(turso db create, turso group create, turso db shell 등)는 reference.md를 참조하세요.
<decision_framework>
의사 결정 프레임워크
batch() vs transaction()
모든 SQL 문이 사전에 알려져 있습니까 (문 사이에 조건부 로직이 없습니까)?
+-- 예 --> batch() 사용 (단일 왕복, 암시적 트랜잭션, 선호됨)
+-- 아니오 --> 이후 문이 이전 문의 결과에 의존합니까?
+-- 예 --> transaction() 사용 (대화형, 여러 왕복, 잠금 유지)
+-- 아니오 --> batch() 사용
임포트 경로 선택
어떤 런타임 환경입니까?
+-- Node.js / Bun / Deno / VM / 컨테이너
| +-- 임베디드 복제본(로컬 파일)이 필요합니까?
| | +-- 예 --> file: URL + syncUrl과 함께 @libsql/client 사용
| | +-- 아니오 --> libsql:// URL과 함께 @libsql/client 사용
+-- Cloudflare Workers / Vercel Edge / 브라우저 / 파일 시스템 없는 서버리스
+-- @libsql/client/web 사용 (원격 연결 전용, file: URL 불가)
임베디드 복제본 vs 원격 전용
프로세스가 파일 시스템 접근이 가능하고 장기 실행됩니까?
+-- 예 --> 서브 밀리초 읽기 지연 시간이 필요합니까?
| +-- 예 --> 임베디드 복제본 (file: URL + syncUrl)
| +-- 아니오 --> 원격 전용이 더 간단함 (libsql:// URL)
+-- 아니오 (서버리스, 에지, 단기 실행)
+-- 원격 전용 (@libsql/client/web, libsql:// URL)
트랜잭션 모드 선택
배치/트랜잭션이 어떤 작업을 수행합니까?
+-- SELECT 쿼리만 --> "read" (복제본에서 병렬 실행 허용)
+-- INSERT / UPDATE / DELETE 포함 --> "write" (독점 잠금 획득)
+-- 호출 시 불확실 --> "deferred" (읽기로 시작, 필요시 에스컬레이션)
</decision_framework>
<red_flags>
위험 신호
높은 우선순위 이슈:
batch()/transaction()에서 트랜잭션 모드 누락 — 두 번째 인수를 생략하면 기본값이"deferred"가 되어 읽기 전용으로 시작하고 INSERT/UPDATE/DELETE에 대한 쓰기 잠금 획득에 조용히 실패할 수 있습니다. 항상 모드를 명시적으로 지정하세요.- 에지 런타임에서
@libsql/client사용 — 기본 패키지는 Cloudflare Workers, Vercel Edge Functions 및 유사한 환경에서 실패하는 네이티브 SQLite 바인딩을 번들로 제공합니다. 대신@libsql/client/web을 사용하세요. - SQL에서 문자열 보간법 —
execute(\SELECT * FROM users WHERE id = '${id}'`)는 SQL 인젝션 취약점입니다. 항상args`와 함께 매개변수화된 쿼리를 사용하세요. - 임베디드 복제본 파일에 직접 접근 — libSQL 클라이언트가 실행 중인 동안 다른 SQLite 클라이언트로 로컬
.db파일을 열면 데이터 손상이 발생합니다. 클라이언트를 통해서만 접근하세요.
중간 우선순위 이슈:
batch()로 충분할 때transaction()사용 — 대화형 트랜잭션은 5초 타임아웃으로 데이터베이스 잠금을 유지하고, 여러 왕복이 필요하며, 다른 작성자를 차단합니다. 미리 결정된 명령문 집합에는batch()를 사용하세요.client.close()호출하지 않음 — 단기 실행 프로세스(CLI 스크립트, 테스트 정리)에서 클라이언트 닫기를 잊으면 연결과 파일 핸들이 열린 상태로 남습니다.- 임베디드 복제본의 쓰기 지연 시간 무시 — 읽기는 마이크로초(로컬)이지만 쓰기는 15-50ms(원격 프라이머리로 전달)입니다. 이에 맞게 설계하세요 — 빡빡한 쓰기 루프를 피하세요.
syncInterval을 너무 낮게 설정 — 각 동기화는 변경된 모든 프레임(각 4KB)을 가져옵니다. 쓰기 집약적인 데이터베이스에서 서브초 간격은 상당한 네트워크 및 I/O 오버헤드를 생성합니다.
일반적인 실수:
- 잘못된 패키지 이름 — 패키지는
@libsql/client이며,libsql-client,@turso/client또는turso-client가 아닙니다. - args 객체의 명명된 매개변수 접두사 — Args는 베어 이름을 사용합니다:
{ name: "Alice" }는 SQL에서:name,@name,$name과 일치합니다. 접두사를 포함하지 마세요:{ ":name": "Alice" }는 일치하지 않습니다. lastInsertRowid가 숫자일 것으로 예상 —bigint | undefined입니다. 숫자가 필요한 경우 명시적으로 변환하세요:Number(result.lastInsertRowid). 단, 매우 큰 rowid의 경우 정밀도 손실에 유의하세요.- 원자적 작업에
executeMultiple()사용 —executeMultiple()은 매개변수화나 암시적 트랜잭션 없이 원시 SQL 텍스트(세미콜론으로 구분)를 실행합니다. 원자적 매개변수화된 작업에는batch()를 사용하세요.
주의사항 및 에지 케이스:
batch()문은 트랜잭션을 공유하지만 병렬로 실행되지는 않습니다 — 순차적으로 실행됩니다.last_insert_rowid()는 배치 내의 이전INSERT문의 값을 반환합니다.@libsql/client/web은 자체 서명된 인증서를 지원하지 않습니다 — 에지 런타임에서 HTTPS 인증서 유효성 검사는 WASM 계층에서 처리됩니다. 프로덕션에서는 유효한 TLS 인증서를 사용하세요.- 임베디드 복제본 동기화는 델타가 아닌 전체 프레임을 가져옵니다 —
syncInterval이 낮으면(예: < 10초) 프레임이 거의 변하지 않더라도 각 동기화는 모든 변경된 페이지를 가져옵니다. 대역폭 사용을 모니터링하세요. file:URL은 Node.js@libsql/client에서만 작동합니다 —@libsql/client/web에서file:URL을 사용하면 런타임 오류가 발생합니다.@libsql/client(네이티브)는file:URL을 지원하는 유일한 변형입니다.syncUrl이 설정되면url은 로컬 파일이어야 합니다 —syncUrl과 함께libsql://URL을url로 사용하는 것은 지원되지 않습니다.url은 항상file:이어야 합니다.
</red_flags>