Drizzle with Supabase Database

이 νŠœν† λ¦¬μ–Όμ€ Supabase Database와 Drizzle ORM을 μ‚¬μš©ν•˜λŠ” 방법을 λ³΄μ—¬μ€λ‹ˆλ‹€. λͺ¨λ“  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 쿼리에 λŒ€ν•œ μžμ„Έν•œ λ‚΄μš©μ€ λ¬Έμ„œλ₯Ό μ°Έμ‘°ν•˜μ„Έμš”.

src/db/queries/select.ts
import { asc, between, count, eq, getTableColumns, 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({
      ...getTableColumns(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));
}