Drizzle ORM 실전 가이드 — TypeScript 네이티브 ORM
Drizzle ORM 소개, Prisma·TypeORM과 비교, 스키마 정의, drizzle-kit 마이그레이션, 쿼리 빌더, Next.js App Router 통합, 서버리스 환경 주의사항.
한 줄 요약: Drizzle ORM은 TypeScript-first 설계, SQL과 유사한 직관적인 쿼리 빌더, 런타임 오버헤드 최소화로 Prisma의 대안으로 빠르게 자리잡은 ORM이다. Next.js, Bun, Deno 환경 모두에서 동작한다.
- Prisma를 쓰고 있지만 번들 사이즈, 엣지 런타임 호환성, 생성 코드의 불투명함에 불만이 있는 개발자
- TypeScript 프로젝트에서 SQL을 최대한 직접 제어하면서도 타입 안전성을 원하는 경우
- Next.js App Router 또는 Cloudflare Workers에서 ORM을 도입하려는 개발자
- Drizzle ORM을 처음 접하고 기본 설정부터 실전 쿼리까지 빠르게 익히고 싶은 경우
Drizzle ORM 소개 — 왜 주목받는가
Drizzle ORM은 2022년 공개돼 2024~2025년에 빠르게 성장한 TypeScript-native ORM이다. State of JS 2024 설문에서 ORM 만족도 1위를 기록했으며, npm 주간 다운로드가 100만을 돌파했다.
주요 특징:
- SQL-like 쿼리 빌더: Drizzle 쿼리는 SQL 문법과 유사하게 작성된다. SQL을 아는 개발자라면 즉시 적응할 수 있고, 생성되는 쿼리를 예측하기 쉽다.
- 제로 런타임 추상화: Prisma처럼 별도의 바이너리 엔진이 없다. Drizzle은 순수 TypeScript/JavaScript로 동작하며 엣지 런타임(Cloudflare Workers, Vercel Edge, Deno Deploy)과 완전히 호환된다.
- 타입 안전성: 스키마 정의에서 자동으로 TypeScript 타입이 추론된다. 별도의 타입 생성 명령이 필요 없다.
- 드라이버 독립성: Postgres.js, node-postgres, mysql2, better-sqlite3, Turso(libSQL) 등 다양한 드라이버와 연결할 수 있다.
Drizzle의 철학은 "ORM이 SQL을 숨기지 않아야 한다"는 것이다. 복잡한 쿼리를 작성할 때 ORM 추상화 때문에 SQL을 돌아가는 방법을 쓰거나 원시 쿼리로 탈출하는 상황을 최소화하도록 설계됐다.
Prisma, TypeORM과 비교
세 ORM의 실무적 차이를 정리한다.
| 항목 | Drizzle | Prisma | TypeORM |
|---|---|---|---|
| 스키마 정의 | TypeScript 코드 | Prisma Schema (.prisma) | 데코레이터 또는 클래스 |
| 타입 생성 | 자동 추론 (코드 생성 없음) | prisma generate 필요 | 클래스에서 자동 |
| 엣지 런타임 지원 | 완전 지원 | 제한적 (Prisma Accelerate) | 미지원 |
| 번들 사이즈 | ~30KB | ~2MB+ (바이너리 포함) | ~300KB |
| 쿼리 예측 가능성 | 높음 (SQL-like) | 보통 (추상화 높음) | 보통 |
| 학습 곡선 | 낮음~보통 | 낮음 (스키마 직관적) | 보통~높음 |
| 마이그레이션 | drizzle-kit (SQL 파일 생성) | prisma migrate | typeorm migration |
| 활성 유지보수 | 활발 (2025년 기준) | 활발 | 느림 |
TypeORM은 2024년 이후 메인테이너 활동이 줄었고, 데코레이터 의존성 때문에 최신 TypeScript 설정과 충돌하는 경우가 있다. 신규 프로젝트에서 TypeORM을 선택할 이유는 줄었다.
스키마 정의 — TypeScript로 테이블 구조 작성
Drizzle 스키마는 일반 TypeScript 파일이다. drizzle-orm/pg-core, drizzle-orm/mysql-core, drizzle-orm/sqlite-core 중 데이터베이스에 맞는 모듈을 가져와 테이블을 정의한다.
schema.ts — PostgreSQL 스키마 정의 예시// schema.ts import { pgTable, serial, text, varchar, integer, timestamp, boolean, pgEnum, index, uniqueIndex } from 'drizzle-orm/pg-core' import { relations } from 'drizzle-orm' // Enum 정의 export const userRoleEnum = pgEnum('user_role', ['admin', 'user', 'guest']) // users 테이블 export const users = pgTable('users', { id: serial('id').primaryKey(), email: varchar('email', { length: 255 }).notNull().unique(), name: text('name').notNull(), role: userRoleEnum('role').default('user').notNull(), createdAt: timestamp('created_at').defaultNow().notNull(), updatedAt: timestamp('updated_at').defaultNow().notNull(), isActive: boolean('is_active').default(true).notNull(), }, (table) => ({ emailIdx: uniqueIndex('email_idx').on(table.email), createdAtIdx: index('created_at_idx').on(table.createdAt), })) // posts 테이블 export const posts = pgTable('posts', { id: serial('id').primaryKey(), title: varchar('title', { length: 500 }).notNull(), content: text('content'), authorId: integer('author_id').notNull().references(() => users.id, { onDelete: 'cascade' }), publishedAt: timestamp('published_at'), createdAt: timestamp('created_at').defaultNow().notNull(), }) // Relations 정의 export const usersRelations = relations(users, ({ many }) => ({ posts: many(posts), })) export const postsRelations = relations(posts, ({ one }) => ({ author: one(users, { fields: [posts.authorId], references: [users.id], }), }))
drizzle-kit — 마이그레이션 관리
drizzle-kit은 Drizzle 스키마를 기반으로 SQL 마이그레이션 파일을 생성하고 데이터베이스에 적용하는 CLI 도구다. Prisma migrate와 달리 생성된 SQL 파일을 직접 확인하고 수정할 수 있어 투명성이 높다.
drizzle.config.ts 및 마이그레이션 워크플로우// drizzle.config.ts import { defineConfig } from 'drizzle-kit' export default defineConfig({ schema: './src/db/schema.ts', out: './drizzle', // 마이그레이션 파일 저장 경로 dialect: 'postgresql', dbCredentials: { url: process.env.DATABASE_URL!, }, verbose: true, strict: true, }) // --- 마이그레이션 워크플로우 --- // 1. 스키마 변경 후 마이그레이션 파일 생성 // npx drizzle-kit generate // 2. 생성된 SQL 파일 확인 (drizzle/0001_xxx.sql) // 3. 마이그레이션 적용 // npx drizzle-kit migrate // 4. 현재 스키마와 DB 상태 비교 (적용 없이 확인) // npx drizzle-kit check // 5. Drizzle Studio — 브라우저 GUI로 DB 확인 // npx drizzle-kit studio
쿼리 빌더 실전 — select, insert, update, delete
Drizzle의 쿼리는 SQL 문법을 TypeScript로 그대로 표현한다. 복잡한 JOIN, 서브쿼리, 집계 함수도 타입 안전하게 작성할 수 있다.
db/queries.ts — 주요 쿼리 패턴import { db } from './client' import { users, posts } from './schema' import { eq, and, gte, like, desc, count, sql } from 'drizzle-orm' // SELECT — 기본 조회 const allUsers = await db.select().from(users) // WHERE 조건 const activeAdmins = await db .select({ id: users.id, email: users.email, name: users.name }) .from(users) .where(and(eq(users.isActive, true), eq(users.role, 'admin'))) .orderBy(desc(users.createdAt)) .limit(10) // JOIN const postsWithAuthors = await db .select({ postId: posts.id, title: posts.title, authorName: users.name, authorEmail: users.email, }) .from(posts) .innerJoin(users, eq(posts.authorId, users.id)) .where(gte(posts.createdAt, new Date('2026-01-01'))) // Relations API (Drizzle 고유 방식) const usersWithPosts = await db.query.users.findMany({ where: eq(users.isActive, true), with: { posts: { orderBy: [desc(posts.createdAt)], limit: 5, }, }, }) // INSERT const [newUser] = await db .insert(users) .values({ email: 'dev@example.com', name: '홍길동' }) .returning() // UPDATE await db .update(users) .set({ isActive: false, updatedAt: new Date() }) .where(eq(users.id, 42)) // DELETE await db.delete(posts).where(eq(posts.authorId, 42)) // 집계 함수 const [{ total }] = await db .select({ total: count() }) .from(users) .where(eq(users.isActive, true))
Next.js App Router 통합 — 서버 컴포넌트에서 Drizzle 사용
Next.js App Router에서 Drizzle은 서버 컴포넌트, 서버 액션, Route Handler 모두에서 직접 사용할 수 있다. 엣지 런타임 호환성 덕분에 Vercel Edge 함수에서도 동작한다.
src/db/client.ts 및 Next.js 서버 컴포넌트 통합// src/db/client.ts — DB 클라이언트 싱글턴 import { drizzle } from 'drizzle-orm/postgres-js' import postgres from 'postgres' import * as schema from './schema' const connectionString = process.env.DATABASE_URL! // 개발 환경: HMR로 인한 중복 연결 방지 const globalForDb = globalThis as unknown as { conn: postgres.Sql } const conn = globalForDb.conn ?? postgres(connectionString) if (process.env.NODE_ENV !== 'production') globalForDb.conn = conn export const db = drizzle(conn, { schema }) // --- // app/users/page.tsx — 서버 컴포넌트에서 직접 DB 조회 import { db } from '@/db/client' import { users } from '@/db/schema' import { desc } from 'drizzle-orm' export default async function UsersPage() { const allUsers = await db .select() .from(users) .orderBy(desc(users.createdAt)) .limit(20) return ( <ul> {allUsers.map((user) => ( <li key={user.id}>{user.name} — {user.email}</li> ))} </ul> ) } // --- // app/actions/users.ts — 서버 액션 'use server' import { db } from '@/db/client' import { users } from '@/db/schema' import { revalidatePath } from 'next/cache' export async function createUser(formData: FormData) { const email = formData.get('email') as string const name = formData.get('name') as string await db.insert(users).values({ email, name }) revalidatePath('/users') }
drizzle-orm/neon-http 어댑터를 써야 한다.