Drizzle와 Supabase Database

이 튜토리얼은 Drizzle ORM을 Supabase Database와 함께 사용하는 방법을 설명합니다. 모든 Supabase 프로젝트에는 완전한 Postgres 데이터베이스가 제공됩니다.

This guide assumes familiarity with:
  • Drizzle ORM과 Drizzle kit을 설치해야 합니다. 다음 명령어를 실행하여 설치할 수 있습니다:
npm
yarn
pnpm
bun
npm i drizzle-orm
npm i -D drizzle-kit
  • 환경 변수 관리를 위해 dotenv 패키지를 설치해야 합니다. 이 패키지에 대한 자세한 내용은 여기를 참조하세요.
npm
yarn
pnpm
bun
npm i dotenv
  • Postgres 데이터베이스 연결을 위해 postgres 패키지를 설치해야 합니다. 이 패키지에 대한 자세한 내용은 여기를 참조하세요.
npm
yarn
pnpm
bun
npm i postgres
  • 최신 버전의 Supabase CLI를 설치해야 합니다 (Supabase CLI를 사용한 마이그레이션을 원하는 경우에만 필요)

Drizzle ORM으로 데이터베이스에 연결하는 방법을 배우려면 Supabase 문서를 확인하세요.

Supabase와 Drizzle ORM 설정

새 Supabase 프로젝트 생성

대시보드에서 새 Supabase 프로젝트를 생성하거나 이 링크를 따라 생성할 수 있습니다.

연결 문자열 변수 설정

Database Settings로 이동하여 Connection String 섹션에서 URI를 복사합니다. connection pooling을 사용하는지 확인하세요. 비밀번호 자리표시자를 실제 데이터베이스 비밀번호로 교체해야 합니다.

.env 또는 .env.local 파일에 DATABASE_URL 변수를 추가합니다.

DATABASE_URL=<YOUR_DATABASE_URL>

Connection Pooler와 풀링 모드에 대한 자세한 내용은 문서를 참조하세요.

Drizzle ORM을 데이터베이스에 연결

src/db 디렉토리에 index.ts 파일을 생성하고 데이터베이스 설정을 구성합니다:

src/db/index.ts
import { config } from 'dotenv';
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';

config({ path: '.env' }); // or .env.local

const client = postgres(process.env.DATABASE_URL!);
export const db = drizzle({ client });

테이블 생성

src/db 디렉토리에 schema.ts 파일을 생성하고 테이블을 선언합니다:

src/db/schema.ts
import { integer, pgTable, serial, text, timestamp } from 'drizzle-orm/pg-core';

export const usersTable = pgTable('users_table', {
  id: serial('id').primaryKey(),
  name: text('name').notNull(),
  age: integer('age').notNull(),
  email: text('email').notNull().unique(),
});

export const postsTable = pgTable('posts_table', {
  id: serial('id').primaryKey(),
  title: text('title').notNull(),
  content: text('content').notNull(),
  userId: integer('user_id')
    .notNull()
    .references(() => usersTable.id, { onDelete: 'cascade' }),
  createdAt: timestamp('created_at').notNull().defaultNow(),
  updatedAt: timestamp('updated_at')
    .notNull()
    .$onUpdate(() => new Date()),
});

export type InsertUser = typeof usersTable.$inferInsert;
export type SelectUser = typeof usersTable.$inferSelect;

export type InsertPost = typeof postsTable.$inferInsert;
export type SelectPost = typeof postsTable.$inferSelect;

Drizzle 설정 파일 구성

Drizzle config - Drizzle Kit에서 사용하는 설정 파일로, 데이터베이스 연결, 마이그레이션 폴더, 스키마 파일에 대한 모든 정보를 포함합니다.

프로젝트 루트에 drizzle.config.ts 파일을 생성하고 다음 내용을 추가합니다:

drizzle.config.ts
import { config } from 'dotenv';
import { defineConfig } from 'drizzle-kit';

config({ path: '.env' });

export default defineConfig({
  schema: './src/db/schema.ts',
  out: './supabase/migrations',
  dialect: 'postgresql',
  dbCredentials: {
    url: process.env.DATABASE_URL!,
  },
});

데이터베이스에 변경사항 적용

drizzle-kit generate 명령어를 사용하여 마이그레이션을 생성한 다음 drizzle-kit migrate 명령어로 실행할 수 있습니다.

마이그레이션 생성:

npx drizzle-kit generate

이러한 마이그레이션은 drizzle.config.ts에 지정된 대로 supabase/migrations 디렉토리에 저장됩니다. 이 디렉토리에는 데이터베이스 스키마를 업데이트하는 데 필요한 SQL 파일과 여러 마이그레이션 단계에서 스키마의 스냅샷을 저장하는 meta 폴더가 포함됩니다.

생성된 마이그레이션 예시:

CREATE TABLE IF NOT EXISTS "posts_table" (
	"id" serial PRIMARY KEY NOT NULL,
	"title" text NOT NULL,
	"content" text NOT NULL,
	"user_id" integer NOT NULL,
	"created_at" timestamp DEFAULT now() NOT NULL,
	"updated_at" timestamp NOT NULL
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "users_table" (
	"id" serial PRIMARY KEY NOT NULL,
	"name" text NOT NULL,
	"age" integer NOT NULL,
	"email" text NOT NULL,
	CONSTRAINT "users_table_email_unique" UNIQUE("email")
);
--> statement-breakpoint
DO $$ BEGIN
 ALTER TABLE "posts_table" ADD CONSTRAINT "posts_table_user_id_users_table_id_fk" FOREIGN KEY ("user_id") REFERENCES "users_table"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
 WHEN duplicate_object THEN null;
END $$;

마이그레이션 실행:

npx drizzle-kit migrate

마이그레이션 프로세스에 대해 자세히 알아보세요. Supabase CLI를 사용하여 마이그레이션을 적용할 수도 있습니다:

  • 이미 존재하는 테이블의 경우, npx drizzle-kit generate로 생성된 마이그레이션 파일을 수동으로 검토하고 안전하지 않은 순수 생성 문(예: CREATE SCHEMA "auth";)을 주석 처리하거나 조정하면서 안전한 조건부 생성(예: CREATE TABLE IF NOT EXISTS "auth"."users")이 적절히 처리되도록 해야 합니다.

또는 Drizzle kit push 명령어를 사용하여 변경사항을 데이터베이스에 직접 푸시할 수 있습니다:

npx drizzle-kit push
IMPORTANT
Push 명령어는 로컬 개발 환경에서 새로운 스키마 디자인이나 변경사항을 빠르게 테스트해야 하는 상황에 적합하며, 마이그레이션 파일 관리의 오버헤드 없이 빠른 반복을 가능하게 합니다.

Supabase CLI를 사용하여 마이그레이션을 적용하려면 다음 단계를 따라야 합니다:

Drizzle Kit을 사용하여 마이그레이션 생성:

npx drizzle-kit generate

로컬 Supabase 프로젝트 초기화:

supabase init

원격 프로젝트에 연결:

supabase link

데이터베이스에 변경사항 푸시:

supabase db push

기본 파일 구조

이것은 프로젝트의 기본 파일 구조입니다. src/db 디렉토리에는 index.ts의 연결 설정과 schema.ts의 스키마 정의를 포함한 데이터베이스 관련 파일이 있습니다.

📦 <project root>
 ├ 📂 src
 │   ├ 📂 db
 │   │  ├ 📜 index.ts
 │   │  └ 📜 schema.ts
 ├ 📂 supabase
 │   ├ 📂 migrations
 │   │  ├ 📂 meta
 │   │  │  ├ 📜 _journal.json
 │   │  │  └ 📜 0000_snapshot.json
 │   │  └ 📜 0000_watery_spencer_smythe.sql
 │   └ 📜 config.toml
 ├ 📜 .env
 ├ 📜 drizzle.config.ts
 ├ 📜 package.json
 └ 📜 tsconfig.json

쿼리 예제

예를 들어, src/db/queries 폴더를 생성하고 각 작업에 대한 별도 파일을 만듭니다: insert, select, update, delete.

데이터 삽입

insert 쿼리에 대한 자세한 내용은 문서를 참조하세요.

src/db/queries/insert.ts
import { db } from '../index';
import { InsertPost, InsertUser, postsTable, usersTable } from '../schema';

export async function createUser(data: InsertUser) {
  await db.insert(usersTable).values(data);
}

export async function createPost(data: InsertPost) {
  await db.insert(postsTable).values(data);
}

데이터 조회

select 쿼리에 대한 자세한 내용은 문서를 참조하세요.

IMPORTANT

getColumnsdrizzle-orm@1.0.0-beta.2부터 사용 가능합니다(여기에서 자세히 읽어보세요)

1.0 이전 버전(예: 0.45.1)을 사용하는 경우 getTableColumns를 사용하세요

src/db/queries/select.ts
import { asc, between, count, eq, getColumns, sql } from 'drizzle-orm';
import { db } from '../index';
import { SelectUser, postsTable, usersTable } from '../schema';

export async function getUserById(id: SelectUser['id']): Promise<
  Array<{
    id: number;
    name: string;
    age: number;
    email: string;
  }>
> {
  return db.select().from(usersTable).where(eq(usersTable.id, id));
}

export async function getUsersWithPostsCount(
  page = 1,
  pageSize = 5,
): Promise<
  Array<{
    postsCount: number;
    id: number;
    name: string;
    age: number;
    email: string;
  }>
> {
  return db
    .select({
      ...getColumns(usersTable),
      postsCount: count(postsTable.id),
    })
    .from(usersTable)
    .leftJoin(postsTable, eq(usersTable.id, postsTable.userId))
    .groupBy(usersTable.id)
    .orderBy(asc(usersTable.id))
    .limit(pageSize)
    .offset((page - 1) * pageSize);
}

export async function getPostsForLast24Hours(
  page = 1,
  pageSize = 5,
): Promise<
  Array<{
    id: number;
    title: string;
  }>
> {
  return db
    .select({
      id: postsTable.id,
      title: postsTable.title,
    })
    .from(postsTable)
    .where(between(postsTable.createdAt, sql`now() - interval '1 day'`, sql`now()`))
    .orderBy(asc(postsTable.title), asc(postsTable.id))
    .limit(pageSize)
    .offset((page - 1) * pageSize);
}

또는 관계형 쿼리 문법을 사용할 수 있습니다.

데이터 업데이트

update 쿼리에 대한 자세한 내용은 문서를 참조하세요.

src/db/queries/update.ts
import { eq } from 'drizzle-orm';
import { db } from '../index';
import { SelectPost, postsTable } from '../schema';

export async function updatePost(id: SelectPost['id'], data: Partial<Omit<SelectPost, 'id'>>) {
  await db.update(postsTable).set(data).where(eq(postsTable.id, id));
}

데이터 삭제

delete 쿼리에 대한 자세한 내용은 문서를 참조하세요.

src/db/queries/delete.ts
import { eq } from 'drizzle-orm';
import { db } from '../index';
import { SelectUser, usersTable } from '../schema';

export async function deleteUser(id: SelectUser['id']) {
  await db.delete(usersTable).where(eq(usersTable.id, id));
}