TechFeedTechFeed
Backend

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의 실무적 차이를 정리한다.

항목DrizzlePrismaTypeORM
스키마 정의TypeScript 코드Prisma Schema (.prisma)데코레이터 또는 클래스
타입 생성자동 추론 (코드 생성 없음)prisma generate 필요클래스에서 자동
엣지 런타임 지원완전 지원제한적 (Prisma Accelerate)미지원
번들 사이즈~30KB~2MB+ (바이너리 포함)~300KB
쿼리 예측 가능성높음 (SQL-like)보통 (추상화 높음)보통
학습 곡선낮음~보통낮음 (스키마 직관적)보통~높음
마이그레이션drizzle-kit (SQL 파일 생성)prisma migratetypeorm 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') }
Serverless 환경 주의: Serverless(Vercel, AWS Lambda)에서 postgres.js는 연결 풀링이 없어 콜드 스타트마다 새 연결을 만든다. 이 환경에서는 Neon(serverless 드라이버), PlanetScale, Supabase의 연결 풀링 모드를 사용하거나 drizzle-orm/neon-http 어댑터를 써야 한다.
DrizzleORMTypeScriptPrismaNext.jsPostgreSQLdrizzle-kit

관련 포스트

PostgreSQL 17 주요 변경점과 마이그레이션 가이드2026-03-17PostgreSQL 성능 튜닝 실전 가이드2026-02-20REST API 설계 모범 사례 20262026-02-22Redis 실전 활용 패턴 7가지2026-02-24