Supabase 데이터베이스와 Drizzle 사용하기

Supabase와 Drizzle ORM 설정하기

이 튜토리얼은 Supabase 데이터베이스와 함께 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 프로젝트 생성하기

새로운 Supabase 프로젝트는 대시보드에서 생성하거나, 이 링크를 따라가면 바로 만들 수 있습니다.

연결 문자열 변수 설정하기

  1. 데이터베이스 설정 페이지로 이동합니다.

  2. Connection String 섹션에서 URI를 복사합니다. 이때, **연결 풀링(connection pooling)**을 사용해야 합니다.

  3. 복사한 연결 문자열에서 비밀번호 부분을 실제 데이터베이스 비밀번호로 변경합니다.

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

DATABASE_URL=

연결 풀러(Connection Pooler)와 풀링 모드에 대해 더 자세히 알고 싶다면 공식 문서를 참고하세요.

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

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

import { config } from 'dotenv';
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';

// .env 또는 .env.local 파일에서 환경 변수 로드
config({ path: '.env' });

// 데이터베이스 연결 클라이언트 생성
const client = postgres(process.env.DATABASE_URL!);

// Drizzle ORM 인스턴스 생성 및 내보내기
export const db = drizzle({ client });

이 코드는 Drizzle ORM을 사용하여 데이터베이스에 연결하는 기본 설정을 보여줍니다. .env 파일에서 DATABASE_URL 환경 변수를 읽어와 데이터베이스에 연결하고, 이를 기반으로 Drizzle ORM 인스턴스를 생성합니다.

테이블 생성하기

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(), // 기본 키로 설정된 자동 증가 ID
  name: text('name').notNull(), // 이름 (필수)
  age: integer('age').notNull(), // 나이 (필수)
  email: text('email').notNull().unique(), // 이메일 (필수, 고유값)
});

// 게시물 테이블 정의
export const postsTable = pgTable('posts_table', {
  id: serial('id').primaryKey(), // 기본 키로 설정된 자동 증가 ID
  title: text('title').notNull(), // 제목 (필수)
  content: text('content').notNull(), // 내용 (필수)
  userId: integer('user_id') // 사용자 ID (필수)
    .notNull()
    .references(() => usersTable.id, { onDelete: 'cascade' }), // 사용자 테이블의 ID 참조, 삭제 시 연쇄 삭제
  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 설정 파일Drizzle Kit에서 사용하는 구성 파일로, 데이터베이스 연결 정보, 마이그레이션 폴더, 스키마 파일 등에 대한 모든 정보를 포함합니다.

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

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

// .env 파일에서 환경 변수 로드
config({ path: '.env' });

export default defineConfig({
  schema: './src/db/schema.ts', // 스키마 파일 경로
  out: './supabase/migrations', // 마이그레이션 파일 출력 경로
  dialect: 'postgresql', // 데이터베이스 종류
  dbCredentials: {
    url: process.env.DATABASE_URL!, // 데이터베이스 연결 URL
  },
});

이 설정 파일은 Drizzle Kit이 데이터베이스 스키마를 관리하고 마이그레이션을 생성하는 데 필요한 정보를 제공합니다.

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

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
푸시 커맨드는 로컬 개발 환경에서 새로운 스키마 디자인이나 변경 사항을 빠르게 테스트해야 할 때 유용합니다. 마이그레이션 파일을 관리하는 오버헤드 없이 빠르게 반복 작업을 할 수 있습니다.

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 폴더를 생성하고 각 작업(삽입, 선택, 업데이트, 삭제)별로 파일을 분리합니다.

데이터 삽입

데이터 삽입 쿼리에 대해 더 알아보려면 문서를 참고하세요.

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);
}

데이터 선택하기

선택 쿼리에 대한 더 자세한 내용은 문서에서 확인할 수 있습니다.

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';

// 특정 ID를 가진 사용자 정보를 가져오는 함수
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);
}

// 지난 24시간 동안 작성된 게시물을 가져오는 함수
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);
}

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

데이터 업데이트

데이터 업데이트 쿼리에 대해 더 알아보려면 문서를 참고하세요.

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<SelectPost>) {
  await db.update(postsTable).set(data).where(eq(postsTable.id, id));
}

위 코드는 postsTable에서 특정 ID를 가진 게시물을 업데이트하는 함수입니다. db.update 메서드를 사용하여 데이터를 업데이트하고, eq 함수를 통해 특정 ID를 조건으로 지정합니다.

데이터 삭제

삭제 쿼리에 대한 자세한 내용은 문서에서 확인할 수 있습니다.

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));
}