Astro DB 완벽 가이드 — Turso·LibSQL 기반 서버리스 데이터베이스

Astro DB 완벽 가이드 — Turso·LibSQL 기반 서버리스 데이터베이스

이 글의 핵심

Astro DB는 libSQL 호환 저장소 위에서 Astro 프로젝트에 스키마·시드·쿼리를 일관되게 묶어 주는 공식 데이터 계층입니다. 이 글에서는 Turso 연동, Drizzle 기반 쿼리, 로컬·원격 개발 차이, 그리고 블로그형 CMS를 설계할 때의 보안까지 실무 관점에서 정리합니다.

이 글의 핵심

Astro DB는 Astro 생태계에 맞춘 SQL 데이터베이스 구성·개발·배포 경험을 한 번에 제공합니다. 로컬에서는 .astro/content.db에 가까운 방식으로 돌아가며, Turso와 같이 libSQL 원격 프로토콜을 제공하는 호스팅에 스키마를 푸시해 프로덕션과 동일한 모델을 유지할 수 있습니다. 내부적으로는 Drizzle ORM이 이미 연결되어 있어, astro:db에서 db 클라이언트와 테이블 정의·연산자를 그대로 가져다 쓰면 됩니다.

이 가이드는 다음을 순서대로 다룹니다. Astro DB의 역할과 libSQL·Turso의 관계, defineTable로 스키마를 정의하고 astro db push로 원격에 반영하는 흐름, Drizzle 스타일의 선택·조인·배치, 로컬과 --remote 개발·빌드의 차이, 그리고 사용자 데이터를 다룰 때의 인증·비밀 관리·입력 검증, 마지막으로 블로그·경량 CMS를 설계할 때의 테이블 예시입니다.


1. Astro DB의 핵심 개념

1-1. Astro DB가 해결하는 문제

전통적으로 프론트엔드 프레임워크는 “빌드 시점의 콘텐츠”와 “런타임의 영속 데이터”를 별도 도구로 엮어야 했습니다. Astro DB는 프로젝트 안의 db/config.ts를 단일 출처로 삼아, 개발 서버·시드·타입 생성·배포 시 스키마 푸시까지 같은 정의를 따르게 합니다. 그 결과 테이블·컬럼 이름을 바꿀 때 TypeScript가 즉시 불일치를 잡아 주고, 문서화되지 않은 “운영만의 스키마”가 생기기 어렵습니다.

1-2. libSQL과 Turso

libSQL은 SQLite에서 파생된 오픈소스 임베디드·클라이언트 라이브러리로, HTTP·WebSocket 등 원격 접속과 임베디드 복제(embedded replica) 같은 확장을 염두에 둡니다. Turso는 libSQL을 관리형으로 제공하는 플랫폼으로, Astro DB 문서에서도 프로덕션 연결 예시로 자주 인용됩니다. Astro DB는 “로컬 파일·메모리·원격 libSQL 서버”를 같은 추상화로 다룰 수 있게 설계되어 있습니다.

1-3. Drizzle이 기본 탑재된다는 의미

별도로 drizzle-kit 설정을 늘리지 않아도, import { db, eq } from 'astro:db' 형태로 Drizzle 쿼리 빌더를 사용합니다. 스키마는 astro:dbdefineTable로 선언되고, 런타임에는 Drizzle의 select·insert·where·innerJoin 등과 동일한 패턴으로 작성합니다. 즉 “Astro DB = 스키마 선언 + 호스팅 연동 + Drizzle 클라이언트”로 이해하면 됩니다.


2. 설치와 기본 구조

공식 문서에 따라 @astrojs/db 통합을 추가합니다.

npx astro add db

설치 후 프로젝트 루트에 db/config.ts가 생성됩니다. 여기서 테이블을 정의하고 defineDb로 내보냅니다. 개발 시 로컬 DB가 사용되며, 시드를 넣으려면 db/seed.ts를 둡니다.


3. 스키마 정의

3-1. 테이블과 컬럼 타입

defineTablecolumn으로 컬럼 타입을 지정합니다. 문자열·정수·불리언·날짜·JSON 등이 일반적입니다.

// db/config.ts (개념 예시)
import { defineDb, defineTable, column } from 'astro:db';

const Author = defineTable({
  columns: {
    id: column.number({ primaryKey: true }),
    name: column.text(),
  },
});

const Comment = defineTable({
  columns: {
    id: column.number({ primaryKey: true }),
    authorId: column.number({ references: () => Author.columns.id }),
    body: column.text(),
  },
});

export default defineDb({
  tables: { Author, Comment },
});

references는 다른 테이블의 식별 컬럼을 가리키며, 이후 조인 쿼리에서 관계를 명확히 하는 데 도움이 됩니다. 실제 프로젝트에서는 컬럼 이름·타입을 도메인에 맞게 조정하면 됩니다.

3-2. 시드 데이터

db/seed.ts에서 db와 테이블을 astro:db로부터 가져와 insert합니다. 개발 서버는 이 파일이 바뀔 때 시드를 다시 넣는 등, 로컬 반복 작업에 맞춘 워크플로로 동작합니다. 프로덕션 데이터는 기본적으로 여기에 섞이지 않으며, 원격에 연결해 개발할 때만 주의가 필요합니다.


4. 마이그레이션과 스키마 푸시

Astro DB는 “마이그레이션 SQL 파일을 수십 개 쌓는” 방식과 “선언적 스키마를 CLI로 푸시하는” 방식의 중간에 가깝습니다. 원격 DB에 반영할 때는 다음이 핵심입니다.

  • astro db push --remote: 로컬 db/config.ts의 변경을 원격 libSQL DB에 적용합니다. 가능하면 데이터 손실 없이 적용할 수 있는지 검사합니다.
  • 깨지는 변경: 호환되지 않는 변경이면 --force-reset으로 원격 데이터를 초기화한 뒤 스키마를 맞출 수 있습니다. 프로덕션에서는 매우 신중해야 합니다.
  • 테이블 이름 변경: 한 번에 바꾸기 어렵다면, 기존 테이블에 deprecated: true를 주고 동일 구조의 새 테이블을 추가한 뒤 푸시하고, 코드·데이터를 옮긴 다음 예전 테이블 정의를 제거하는 단계적 절차를 문서에서 권장합니다.

데이터를 스크립트로 넣거나 변환해야 할 때는, 타입이 보장된 astro:db 모듈을 사용하는 TypeScript 파일을 작성하고 astro db execute ... --remote로 원격에 실행할 수 있습니다. 시드 파일 경로는 프로젝트 구조에 맞게 지정하면 됩니다.


5. 쿼리와 관계형 데이터

5-1. 기본 선택과 필터

페이지·엔드포인트·Actions 어디서든 db로 조회합니다. Drizzle의 eq, like, and 등은 astro:db에서 함께 제공됩니다.

import { db, Comment, like } from 'astro:db';

const rows = await db
  .select()
  .from(Comment)
  .where(like(Comment.body, '%키워드%'));

5-2. 조인

innerJoin 등으로 관계 테이블을 묶습니다. 스키마에 references가 있으면 조인 조건을 읽기 쉽게 유지할 수 있습니다.

import { db, eq, Comment, Author } from 'astro:db';

const rows = await db
  .select()
  .from(Comment)
  .innerJoin(Author, eq(Comment.authorId, Author.id));

5-3. 배치와 트랜잭션에 가까운 사용

원격 DB는 요청마다 네트워크 비용이 있으므로, 많은 insert를 개별로 보내기보다 db.batch로 묶는 패턴이 문서에서도 소개됩니다. 실패 시 롤백이 필요하면 Drizzle의 트랜잭션 API와 Astro 런타임 제약을 함께 확인하는 것이 좋습니다.


6. Drizzle ORM 통합

별도 클라이언트 팩토리 없이 import { db } from 'astro:db' 한 줄로 끝납니다. 테이블 객체는 스키마에서 생성된 심볼로 import하며, Drizzle 문서의 select·insert·delete 패턴을 그대로 따르면 됩니다. 유틸리티(eq, gt, count, sql 등)도 astro:db에서 가져옵니다.

주의할 점은, 일부 생태계 도구(예: drizzle-zod 자동 스키마)가 Astro DB의 defineTable 타입과 완전 호환되지 않을 수 있다는 이슈가 커뮤니티에 보고된 바 있다는 것입니다. 폼 검증은 Zod를 Actions 입력 스키마로 두는 방식이 실무에서 단순하고 명확합니다.


7. Remote vs Local 개발

7-1. 기본 동작

기본적으로 astro devastro build로컬 DB를 사용합니다. 테이블은 설정에 맞게 만들어지고 시드가 들어갑니다. Docker 없이도 개발을 시작할 수 있다는 점이 강점입니다.

7-2. 원격 DB에 붙여 개발·빌드

환경 변수로 원격 libSQL을 지정합니다.

  • ASTRO_DB_REMOTE_URL: Turso CLI의 turso db show로 얻은 URL 등
  • ASTRO_DB_APP_TOKEN: turso db tokens create로 발급한 토큰(원격에 필수)

빌드·개발 명령에 --remote를 붙이면 원격 DB에 읽기·쓰기가 가능합니다. 예: astro build --remote, astro dev --remote. 배포 플랫폼(Cloudflare Workers 등)에서는 비 Node 런타임용 web 모드 설정이 필요할 수 있으니 공식 DB 통합 문서의 “mode” 안내를 확인합니다.

7-3. URL 옵션과 임베디드 복제

ASTRO_DB_REMOTE_URL에는 file:·memory:·libsql:·http(s):·ws(s): 등 스킴이 쓰일 수 있고, encryptionKey, syncUrl, syncInterval 같은 쿼리 파라미터로 암호화·원격과의 동기화 간격을 조정할 수 있습니다. 읽기 위주 부하에 맞춰 로컬 복제본을 두는 전략을 Turso·libSQL 문서와 함께 검토하면 됩니다.


8. 인증과 보안

8-1. 비밀과 토큰

ASTRO_DB_APP_TOKEN서버와 CI에서만 노출되어야 합니다. 클라이언트 번들·공개 환경 변수에 넣으면 누구나 DB에 접근할 수 있습니다. 호스팅 대시보드에서 시크릿으로 등록하고, 로컬은 .env를 git에서 제외합니다.

8-2. 쓰기 경로는 서버에서만

사용자가 폼이나 API로 데이터를 보낼 때는 반드시 서버 측에서만 db.insert 등을 실행합니다. Astro Actions를 쓰면 defineActioninput에 Zod 스키마를 두어 타입·런타임 검증을 한 번에 처리할 수 있습니다. 공개 엔드포인트라면 요청당 속도 제한·봇 방지·인증 세션 검사를 고려합니다.

8-3. 권한 모델

SQLite 계열은 PostgreSQL의 Row Level Security와 동일한 개념이 기본 내장되어 있지 않습니다. “어떤 행을 누가 바꿀 수 있는가”는 애플리케이션 레이어에서 세션·JWT·세션 쿠키 검증 후 쿼리에 where로 소유자를 제한하는 방식으로 구현하는 경우가 많습니다. 관리 화면은 별도 인증·역할을 두는 것이 안전합니다.


9. 실전: 블로그·경량 CMS 설계

9-1. 테이블 예시

블로그라면 Post, Author, Category, 다대다가 필요하면 PostCategory 같은 연결 테이블을 둡니다. 댓글이 있으면 CommentpostId를 둡니다.

// 개념 스케치 — 실제 컬럼은 요구사항에 맞게 조정
import { defineDb, defineTable, column } from 'astro:db';

const Author = defineTable({
  columns: {
    id: column.number({ primaryKey: true }),
    slug: column.text(),
    displayName: column.text(),
  },
});

const Post = defineTable({
  columns: {
    id: column.number({ primaryKey: true }),
    slug: column.text({ unique: true }),
    title: column.text(),
    body: column.text(),
    publishedAt: column.date(),
    authorId: column.number({ references: () => Author.columns.id }),
  },
});

export default defineDb({ tables: { Author, Post } });

unique 등 옵션은 공식 테이블·컬럼 레퍼런스에서 지원 여부를 확인하세요.

9-2. 읽기와 쓰기 분리

  • 공개 목록·글 상세: 빌드 시점에 정적으로 가져가거나, SSR로 db.select 후 캐시 헤더를 설정합니다.
  • 관리자 초안·발행: Actions로 인증된 사용자만 insert·update합니다. 마크다운 저장 시 XSS를 막기 위해 저장 형식(순수 텍스트·검증된 HTML)을 정책으로 정합니다.

9-3. Astro Studio에서 Turso로 이전

기존에 Astro Studio를 썼다면, Turso에 DB를 만들고 환경 변수를 맞춘 뒤 astro db push --remote로 스키마를 올리고, 덤프를 turso db shell 등으로 가져오는 절차가 문서화되어 있습니다. 이전이 끝나면 Studio 쪽 정리 여부를 팀 정책에 따라 결정하면 됩니다.


10. 트러블슈팅 요약

증상점검
푸시가 거부된다깨지는 스키마 변경 여부, 프로덕션 백업 후 --force-reset 또는 단계적 deprecated 전략
원격에 쓰기가 안 된다ASTRO_DB_* 환경 변수, 빌드·dev에 --remote 여부, 어댑터·모드 설정
타입 오류db/config.ts 수정 후 재시작, 테이블 import 이름 대소문자
성능 이슈N+1 쿼리 줄이기, batch, 조인으로 왕복 감소, 필요 시 캐시

11. 정리

Astro DB는 스키마를 코드로 고정하고 Turso 같은 libSQL 호스팅으로 자연스럽게 이어 주는 경로를 제공합니다. Drizzle을 직접 깔고 설정 파일을 늘리는 대신 astro:db에 집중하면, Astro 앱 안에서 읽기·쓰기·배포 스크립트가 같은 모델을 공유합니다. 로컬은 빠르게 돌리고, 프로덕션은 원격 플래그와 시크릿·서버 측 검증으로 잠그는 것이 실무에서 가장 중요한 습관입니다.

배포 전에는 git add·commit·pushnpm run deploy(프로젝트 스크립트 기준)를 사용하고, 환경 변수는 호스팅 콘솔에 반영했는지 다시 한 번 확인하시기 바랍니다.