Neon Postgres와 Drizzle 사용하기

Neon과 Drizzle ORM 설정하기

이 튜토리얼은 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 프로젝트 생성하기

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

Neon 프로젝트는 neondb라는 이름의 Postgres 데이터베이스를 기본으로 제공합니다. 이 튜토리얼에서는 이 데이터베이스를 사용할 예정입니다.

연결 문자열 변수 설정하기

프로젝트 콘솔에서 Connection Details 섹션으로 이동하여 데이터베이스 연결 문자열을 찾습니다. 연결 문자열은 다음과 비슷한 형태일 것입니다:

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

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

DATABASE_URL=NEON_DATABASE_CONNECTION_STRING

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

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

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

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

// DATABASE_URL 환경 변수를 사용하여 Neon 데이터베이스 연결 생성
const sql = neon(process.env.DATABASE_URL!);

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

이 코드는 Drizzle ORM을 사용하여 Neon 데이터베이스에 연결하는 기본 설정을 보여줍니다. .env 파일에 DATABASE_URL을 설정해야 합니다.

테이블 생성하기

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' }), // usersTable의 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; // 게시물 선택 타입

이 코드는 users_tableposts_table 두 개의 테이블을 정의합니다. 각 테이블은 필요한 컬럼과 제약 조건을 포함하고 있습니다. 또한, 삽입 및 선택 작업에 사용할 타입도 정의합니다.

Drizzle 설정 파일 설정하기

Drizzle 설정 파일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!,  // 데이터베이스 연결 URL
  },
});

이 설정 파일은 Drizzle Kit이 데이터베이스와 상호작용하는 데 필요한 정보를 제공합니다. schema는 스키마 파일의 위치를, out은 마이그레이션 파일이 저장될 경로를 지정합니다. dialect는 사용할 데이터베이스 종류를 설정하며, dbCredentials에는 데이터베이스 연결 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 파일에 스키마 정의, 그리고 migrate.ts 파일에 마이그레이션 스크립트가 포함되어 있습니다. 마이그레이션 스크립트는 migrations 디렉토리에 저장된 마이그레이션을 적용하는 역할을 합니다.

📦 <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)별로 파일을 분리합니다.

데이터 삽입

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

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

데이터 선택하기

선택 쿼리에 대한 더 자세한 내용은 문서를 참고하세요.

src/queries/select.ts
import { asc, between, count, eq, getTableColumns, sql } from 'drizzle-orm';
import { db } from '../db';
import { SelectUser, usersTable, postsTable } 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/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<SelectPost>) {
  await db.update(postsTable).set(data).where(eq(postsTable.id, id));
}

위 코드는 postsTable에서 특정 id를 가진 게시물을 업데이트하는 함수입니다. Partial<SelectPost> 타입의 data를 받아 해당 게시물의 정보를 업데이트합니다.

데이터 삭제

삭제 쿼리에 대해 더 알아보려면 문서를 참고하세요.

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