Neon Postgres와 함께 사용하기

이 튜토리얼은 Neon Postgres 데이터베이스에서 Drizzle ORM을 사용하는 방법을 설명합니다. Neon 계정이 없다면 여기에서 가입하세요.

This guide assumes familiarity with:
  • Drizzle ORM과 Drizzle kit을 설치해야 합니다. 다음 명령어로 설치할 수 있습니다:
npm
yarn
pnpm
bun
npm i drizzle-orm
npm i -D drizzle-kit
npm
yarn
pnpm
bun
npm i @neondatabase/serverless
  • 환경 변수 관리를 위해 dotenv 패키지를 설치해야 합니다.
npm
yarn
pnpm
bun
npm i dotenv

Neon과 Drizzle ORM 설정

새 Neon 프로젝트 생성

Neon Console에 로그인하여 Projects 섹션으로 이동합니다. 프로젝트를 선택하거나 New Project 버튼을 클릭하여 새 프로젝트를 생성합니다.

Neon 프로젝트에는 neondb라는 이름의 바로 사용 가능한 Postgres 데이터베이스가 제공됩니다. 이 튜토리얼에서는 이 데이터베이스를 사용합니다.

연결 문자열 변수 설정

프로젝트 콘솔의 Connection Details 섹션으로 이동하여 데이터베이스 연결 문자열을 확인합니다. 다음과 유사한 형태입니다:

postgres://username:password@ep-cool-darkness-123456.us-east-2.aws.neon.tech/neondb

.env 또는 .env.local 파일에 DATABASE_URL 환경 변수를 추가합니다. 이 변수를 사용하여 Neon 데이터베이스에 연결합니다.

DATABASE_URL=NEON_DATABASE_CONNECTION_STRING

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

db.ts 파일을 생성하고 데이터베이스 설정을 구성합니다:

src/db.ts
import { drizzle } from "drizzle-orm/neon-http";
import { neon } from "@neondatabase/serverless";
import { config } from "dotenv";

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

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

테이블 생성

schema.ts 파일을 생성하고 테이블을 정의합니다:

src/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/schema.ts",
  out: "./migrations",
  dialect: "postgresql",
  dbCredentials: {
    url: process.env.DATABASE_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 "public"."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 디렉토리에는 db.ts의 연결 설정, schema.ts의 스키마 정의, 그리고 migrations 디렉토리에 저장된 마이그레이션을 적용하는 migrate.ts 파일 등 데이터베이스 관련 파일들이 있습니다.

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

쿼리 예제

예를 들어, src/queries 폴더를 생성하고 각 작업(insert, select, update, delete)별로 파일을 분리할 수 있습니다.

데이터 삽입

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

src/queries/insert.ts
import { db } from '../db';
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/queries/select.ts
import { asc, between, count, eq, getColumns, sql } from 'drizzle-orm';
import { db } from '../db';
import { SelectUser, usersTable, postsTable } 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/queries/update.ts
import { eq } from 'drizzle-orm';
import { db } from '../db';
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/queries/delete.ts
import { db } from '../db';
import { eq } from 'drizzle-orm';
import { SelectUser, usersTable } from '../schema';

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