🗄️ 데이터베이스

Drizzle ORM

"드리즐"로 발음

TypeScript 네이티브 ORM으로, SQL에 가까운 문법으로 완벽한 타입 안전성을 제공합니다. 경량화되어 서버리스/엣지 환경에 최적화되었으며, "If you know SQL, you know Drizzle"이라는 철학을 따릅니다.

📖 상세 설명

Drizzle ORM은 2022년에 등장한 TypeScript/JavaScript용 ORM으로, Prisma의 무거운 런타임과 달리 순수 TypeScript로 작성되어 번들 사이즈가 작고 Cold Start가 빠릅니다. SQL 문법에 가까운 API를 제공하여 SQL 지식이 있으면 쉽게 사용할 수 있으며, 생성된 쿼리가 예측 가능합니다.

핵심 철학: "If you know SQL, you know Drizzle"

SQL 문법을 TypeScript로 1:1 매핑하여, ORM 추상화로 인한 예측 불가능한 쿼리 생성을 방지합니다.

Drizzle vs Prisma 비교

특성 Drizzle Prisma
번들 사이즈 ~7KB (경량) ~2MB+ (무거움)
Cold Start 빠름 (서버리스 적합) 느림 (Engine 로드)
쿼리 스타일 SQL-like (예측 가능) Prisma Client (추상화)
스키마 정의 TypeScript 코드 .prisma 파일
Raw SQL 타입 안전 지원 $queryRaw (제한적)
지원 DB PostgreSQL, MySQL, SQLite 다양한 DB 지원

💻 코드 예제

1. 스키마 정의 (TypeScript)

schema.ts
import { pgTable, serial, text, timestamp, integer, boolean, varchar } from 'drizzle-orm/pg-core'; import { relations } from 'drizzle-orm'; // 사용자 테이블 export const users = pgTable('users', { id: serial('id').primaryKey(), email: varchar('email', { length: 255 }).notNull().unique(), name: text('name').notNull(), role: varchar('role', { length: 20 }).default('user'), createdAt: timestamp('created_at').defaultNow(), updatedAt: timestamp('updated_at').defaultNow(), }); // 게시글 테이블 export const posts = pgTable('posts', { id: serial('id').primaryKey(), title: varchar('title', { length: 255 }).notNull(), content: text('content'), published: boolean('published').default(false), authorId: integer('author_id').references(() => users.id), createdAt: timestamp('created_at').defaultNow(), }); // 댓글 테이블 export const comments = pgTable('comments', { id: serial('id').primaryKey(), content: text('content').notNull(), postId: integer('post_id').references(() => posts.id), authorId: integer('author_id').references(() => users.id), createdAt: timestamp('created_at').defaultNow(), }); // 관계 정의 export const usersRelations = relations(users, ({ many }) => ({ posts: many(posts), comments: many(comments), })); export const postsRelations = relations(posts, ({ one, many }) => ({ author: one(users, { fields: [posts.authorId], references: [users.id], }), comments: many(comments), }));

2. 기본 CRUD 작업

TypeScript
import { drizzle } from 'drizzle-orm/node-postgres'; import { eq, and, or, like, desc, asc, sql } from 'drizzle-orm'; import { Pool } from 'pg'; import * as schema from './schema'; const pool = new Pool({ connectionString: process.env.DATABASE_URL }); const db = drizzle(pool, { schema }); // CREATE - 단일 삽입 async function createUser(email: string, name: string) { const [user] = await db.insert(schema.users) .values({ email, name }) .returning(); return user; } // CREATE - 다중 삽입 async function createManyPosts(posts: typeof schema.posts.$inferInsert[]) { return await db.insert(schema.posts) .values(posts) .returning(); } // READ - 조건부 조회 async function getPublishedPosts(authorId?: number) { return await db.select() .from(schema.posts) .where( and( eq(schema.posts.published, true), authorId ? eq(schema.posts.authorId, authorId) : undefined ) ) .orderBy(desc(schema.posts.createdAt)) .limit(10); } // READ - JOIN 쿼리 (SQL-like) async function getPostsWithAuthor() { return await db.select({ postId: schema.posts.id, title: schema.posts.title, authorName: schema.users.name, authorEmail: schema.users.email, }) .from(schema.posts) .leftJoin(schema.users, eq(schema.posts.authorId, schema.users.id)) .where(eq(schema.posts.published, true)); } // UPDATE async function updatePost(postId: number, data: Partial) { const [updated] = await db.update(schema.posts) .set({ ...data, updatedAt: new Date() }) .where(eq(schema.posts.id, postId)) .returning(); return updated; } // DELETE async function deletePost(postId: number) { await db.delete(schema.posts) .where(eq(schema.posts.id, postId)); }

3. Query API (관계형 쿼리)

TypeScript
// Drizzle Query API - Prisma-like 문법도 지원 async function getPostWithRelations(postId: number) { return await db.query.posts.findFirst({ where: eq(schema.posts.id, postId), with: { author: true, comments: { with: { author: true, }, orderBy: [desc(schema.comments.createdAt)], limit: 10, }, }, }); } // 복잡한 필터링 async function searchPosts(query: string, filters: { published?: boolean; authorId?: number; fromDate?: Date; }) { return await db.query.posts.findMany({ where: and( like(schema.posts.title, `%${query}%`), filters.published !== undefined ? eq(schema.posts.published, filters.published) : undefined, filters.authorId ? eq(schema.posts.authorId, filters.authorId) : undefined, filters.fromDate ? sql`${schema.posts.createdAt} >= ${filters.fromDate}` : undefined ), with: { author: { columns: { name: true, email: true }, }, }, orderBy: [desc(schema.posts.createdAt)], }); }

4. 트랜잭션과 Raw SQL

TypeScript
// 트랜잭션 async function transferCredits(fromUserId: number, toUserId: number, amount: number) { return await db.transaction(async (tx) => { // 잔액 확인 const [fromUser] = await tx.select() .from(schema.users) .where(eq(schema.users.id, fromUserId)) .for('update'); // SELECT FOR UPDATE if (fromUser.credits < amount) { throw new Error('Insufficient credits'); } // 차감 await tx.update(schema.users) .set({ credits: sql`credits - ${amount}` }) .where(eq(schema.users.id, fromUserId)); // 추가 await tx.update(schema.users) .set({ credits: sql`credits + ${amount}` }) .where(eq(schema.users.id, toUserId)); return { success: true }; }); } // 타입 안전한 Raw SQL async function getPostStats() { const result = await db.execute<{ author_id: number; author_name: string; post_count: number; avg_comments: number; }>(sql` SELECT u.id as author_id, u.name as author_name, COUNT(p.id) as post_count, AVG(comment_counts.cnt)::numeric as avg_comments FROM users u LEFT JOIN posts p ON u.id = p.author_id LEFT JOIN ( SELECT post_id, COUNT(*) as cnt FROM comments GROUP BY post_id ) comment_counts ON p.id = comment_counts.post_id GROUP BY u.id, u.name ORDER BY post_count DESC `); return result.rows; }

5. Drizzle Kit (마이그레이션)

drizzle.config.ts
import type { Config } from 'drizzle-kit'; export default { schema: './src/db/schema.ts', out: './drizzle', driver: 'pg', dbCredentials: { connectionString: process.env.DATABASE_URL!, }, } satisfies Config;
CLI 명령어
# 스키마 변경 감지 및 마이그레이션 파일 생성 npx drizzle-kit generate:pg # 마이그레이션 적용 npx drizzle-kit push:pg # DB 스키마 시각화 (Drizzle Studio) npx drizzle-kit studio # 기존 DB에서 스키마 추출 npx drizzle-kit introspect:pg

💬 현업 대화 예시

풀스택 개발자
"Vercel Edge Functions에서 Prisma 쓰니까 Cold Start가 2초 넘어가더라. Drizzle로 바꾸니까 200ms 이하로 떨어졌어."
백엔드 개발자
"SQL 아는 사람이면 Drizzle 바로 쓸 수 있어. 쿼리가 뭐가 나올지 예측되니까 성능 튜닝도 편하고."
시니어 개발자
"Prisma가 관계형 쿼리는 편한데, 복잡한 집계나 서브쿼리 들어가면 결국 Raw SQL 써야 해. Drizzle은 그런 거 다 타입 안전하게 쓸 수 있어서 좋아."
테크 리드
"Drizzle Studio 켜보면 테이블 구조 바로 보이고 데이터 편집도 되니까 개발할 때 pgAdmin 안 켜도 돼."

⚠️ 주의사항

생태계가 상대적으로 작음

Prisma에 비해 플러그인, 통합, 튜토리얼이 적습니다. 복잡한 케이스에서 참고할 자료가 부족할 수 있습니다.

SQL 지식 필요

SQL에 가까운 API이므로 SQL을 모르면 러닝 커브가 있습니다. Prisma처럼 SQL 없이 ORM만으로 해결하기 어렵습니다.

마이그레이션 도구 제한

Drizzle Kit은 기본적인 마이그레이션을 지원하지만, Prisma Migrate처럼 정교한 다운그레이드나 데이터 마이그레이션 기능은 제한적입니다.

🔗 관련 용어

📚 더 배우기