Drizzle과 Vercel Postgres

이 튜토리얼은 Vercel Postgres와 함께 Drizzle ORM을 사용하는 방법을 설명합니다. Vercel Postgres는 Vercel Functions 및 프론트엔드 프레임워크와 통합되도록 설계된 서버리스 SQL 데이터베이스입니다.

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
  • @vercel/postgres 패키지를 설치해야 합니다. 이 패키지에 대한 자세한 내용은 여기를 참고하세요
npm
yarn
pnpm
bun
npm i @vercel/postgres

Drizzle ORM으로 데이터베이스에 연결하는 방법은 Vercel 문서를 확인하세요.

Vercel Postgres와 Drizzle ORM 설정

새로운 Vercel Postgres 데이터베이스 생성

대시보드에서 새로운 Vercel Postgres 데이터베이스를 생성할 수 있습니다.

새 데이터베이스를 생성하는 방법은 Vercel Postgres 문서를 참고하세요.

연결 문자열 변수 설정

Vercel Postgres 데이터베이스로 이동하여 .env.local 섹션에서 POSTGRES_URL을 복사합니다.

POSTGRES_URL.env.local 또는 .env 파일에 추가합니다.

POSTGRES_URL=<YOUR_DATABASE_URL>

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

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

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

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

export const db = drizzle();

테이블 생성

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.local' });

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

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

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

마이그레이션 생성:

npx drizzle-kit generate

이 마이그레이션은 drizzle.config.ts에 지정된 대로 drizzle/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

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

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

기본 파일 구조

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

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

쿼리 예제

예를 들어, src/db/queries 폴더를 생성하고 각 작업(insert, select, update, delete)에 대한 파일을 분리합니다.

데이터 삽입

삽입 쿼리에 대한 자세한 내용은 문서를 참고하세요.

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

데이터 조회

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

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

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

데이터 업데이트

업데이트 쿼리에 대한 자세한 내용은 문서를 참고하세요.

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

데이터 삭제

삭제 쿼리에 대한 자세한 내용은 문서를 참고하세요.

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