Astro 모노레포에서 Turso libSQL과 Cloudflare D1 비교: 실질적인 차이

발행: (2026년 6월 9일 PM 12:53 GMT+9)
9 분 소요
원문: Dev.to

출처: Dev.to

Turso libSQL vs Cloudflare D1 for an Astro monorepo: the practical difference 표지 이미지

MORINAGA

세 개의 Astro SSG 디렉터리 사이트를 위한 공유 ETL 데이터베이스를 구축하면서( 관련 글 ), 두 가지 명백한 SQLite‑at‑the‑edge 옵션이 있었습니다: Turso (libSQL, 어디서든 실행)와 Cloudflare D1 (Workers 내부 SQLite). 저는 Turso를 선택했습니다. 결정에 영향을 준 실질적인 차이를 정리합니다.

D1이 로컬 개발 문제를 깔끔히 해결하지 못함

Cloudflare D1은 Workers 런타임에 네이티브합니다. 서버‑사이드 렌더링에 Cloudflare Workers를 사용한다면 D1이 당연한 선택이죠—엣지에 바로 배치되고 설정이 전혀 필요 없으며 env.DB 바인딩이 자동으로 제공됩니다.

제 설정은 다릅니다. 사이트들은 Cloudflare Pages에서 정적 Astro 5 SSG 로 운영됩니다—Workers도 없고 서버 런타임도 없습니다. 데이터베이스를 채우는 ETL 파이프라인은 GitHub Actions에서 실행됩니다. 스택 어디에서도 Workers 환경이 사용되지 않죠.

GitHub Actions에서 D1을 사용하려면 Cloudflare REST API나 wrangler CLI를 이용해야 합니다. 두 방법 모두 동작하지만, 개발 중에 직접 쿼리할 수 있는 로컬 SQLite 파일을 제공하지는 않습니다. 로컬 ETL 실행이나 스키마 변경 테스트 시마다 원격 데이터베이스에 SELECT 요청을 보내야 합니다. wrangler에는 로컬 파일에 쓰는 --local 플래그가 있지만, 경로와 포맷이 프로덕션 D1 설정과 달라 두 개의 서로 다른 코드 경로를 관리해야 합니다.

Turso의 로컬 폴백이 계산을 바꾸는 이유

@libsql/client 패키지는 url 파라미터에 libsql:// 원격 URL이나 file:// 경로를 모두 받을 수 있습니다:

export function getClient(): Client {
  if (cached) return cached;
  const url = process.env.TURSO_DATABASE_URL ?? "file:./data/local.db";
  const authToken = process.env.TURSO_AUTH_TOKEN;
  cached = createClient({ url, authToken });
  return cached;
}

CI 환경에서는 TURSO_DATABASE_URL이 Turso 원격 URL로 설정됩니다. 제 노트북에서는 해당 변수가 없으므로 클라이언트가 file:./data/local.db—디스크에 있는 일반 SQLite 파일—를 엽니다. 동일한 @libsql/client 패키지, 동일한 쿼리 API, 동일한 스키마. 코드 경로는 전혀 다르지 않습니다.

덕분에 로컬에서 ETL 스크립트를 실행하고, 어떤 SQLite 뷰어든 사용해 데이터베이스를 살펴볼 수 있습니다. 스키마 마이그레이션도 프로덕션에서 쓰는 applyMigrations() 호출 하나로 동일하게 적용됩니다:

export async function applyMigrations(
  migrations: readonly string[]
): Promise<void> {
  const client = getClient();
  for (const sql of migrations) {
    await client.execute(sql);
  }
}

Docker 컨테이너도, Wrangler 플래그도, 로컬‑대‑원격 별도 코드도 필요 없습니다. 로컬에서 models 테이블을 만드는 동일한 SQL이 Turso에서도 같은 테이블을 생성합니다.

마이그레이션 패턴은 실제로 이렇게 생김

각 앱은 자체 마이그레이션 배열을 정의합니다. AI 도구 ETL의 run.ts 진입점은 시작 시 applyMigrations([CREATE_MODELS_TABLE, CREATE_REVIEWS_TABLE, ...]) 를 호출합니다. 테이블이 이미 존재한다면 CREATE TABLE IF NOT EXISTS 가 아무 일도 하지 않으므로, 이 과정은 멱등(idempotent)합니다. 별도의 마이그레이션 러너가 필요 없죠.

이는 ETL 퍼블리시 단계와 같은 철학입니다—기사 퍼블리시 파이프라인 이 프론트매터의 published_urls 를 확인한 뒤에만 포스팅하므로, 재실행해도 중복 포스팅이 발생하지 않습니다. 데이터베이스 마이그레이션 체크도 같은 패턴을 따릅니다: 체크‑후‑실행, 설계상 멱등.

D1이 실제로 유리할 상황

스택의 어느 부분이라도 Cloudflare Workers 안에서 실행된다면—예를 들어 검색 엔드포인트, API 라우트, 미들웨어 레이어—D1이 더 강력한 선택이 됩니다. env.DB 바인딩은 Turso 엣지에 네트워크 호출을 하는 것보다 빠르고, 동일 데이터센터 내 쿼리라 인증 토큰 관리가 필요 없기 때문이죠.

제 아키텍처는 완전 정적입니다. 디렉터리 콘텐츠에 대해 신선도 vs. 속도 트레이드오프가 SSG에 유리하게 작용하기 때문입니다( 관련 글 ). Workers가 없으니 D1의 핵심 장점이 적용되지 않습니다.

만약 사이트 검색이나 재검증 웹훅을 위해 Cloudflare Worker를 추가한다면 다시 고민해볼 것입니다. Turso(ETL은 GitHub Actions에서 읽고 쓰기) + D1(Workers에서 쿼리) 하이브리드 구성이 현재 원하고 있는 복잡성보다 더 복잡해질 테니까요.

아직 확실히 알지 못하는 세 가지

  • 동시 쓰기 성능
    ETL 파이프라인에서는 워크플로우에 max-parallel: 1을 명시적으로 설정했습니다. 쓰기는 순차적으로 제어됩니다. 동시 쓰기가 어떻게 동작하는지는 아직 테스트하지 않았고, 무료 티어에서 Turso의 동시 쓰기 동작도 스트레스 테스트해 보지는 않았습니다. 30일 뒤에 더 알게 될 것입니다.

  • 스키마 진화 시 마이그레이션 안전성
    기존 테이블에 nullable 컬럼을 추가하는 것은 간단합니다. 컬럼 이름을 바꾸거나 타입을 변경하려면 테이블을 재구성해야 합니다. 아직 그런 상황을 겪어보지 않았으며, 실제로 할 경우 applyMigrations() 접근법은 순서를 신중히 정해야 합니다.

  • 대규모 사용 시 D1 비용
    Turso 무료 티어는 월 500개 데이터베이스와 10억 행 읽기를 지원합니다. Cloudflare D1도 비슷합니다. 현재 세 사이트가 매일 ETL을 돌리는 규모라면 비용은 전혀 발생하지 않습니다. 트래픽이 충분히 늘어나면 실제 비용을 공개하겠지만, 추정치는 제공하지 않을 예정입니다.

데이터베이스 선택 자체가 이 프로젝트의 핵심은 아니었습니다. 로컬 파일 폴백이 바로 제가 Turso를 선택한 전부이며, 그 외는 제 사용 사례에선 두 옵션이 대체로 동등했습니다.

세 개의 AI‑큐레이션 디렉터리 사이트를 운영하는 6개월 실험의 일부입니다. 여기서 제시한 기술적 주장들은 실제이며, 이 글은 AI의 도움을 받아 작성되었습니다.

0 조회
Back to Blog

관련 글

더 보기 »

Eidentic 소개

Today we're releasing Eidentic, an open-source TypeScript SDK for building AI agents with self-improving memory and the production fundamentals built in — not b...

Typescript의 타입

Introdução Tipos são uma forma de definir a “forma” ou o contrato dos dados que estamos usando no código. Pensando em Javascript puro, ele é dinâmico: você pode...