Prisma에서 Drizzle ORM으로 — 번들 사이즈·엣지 호환성·Raw SQL 타입 안전성 차이와 프로덕션 마이그레이션 전략
Cloudflare Workers에 배포하려는 순간 벽에 부딪혔습니다. Node.js net/tls 모듈 미지원 문제 때문에 기존 PostgreSQL 드라이버가 동작하지 않았고, Prisma Accelerate 없이는 엣지에서 연결이 안 된다는 걸 그때서야 알았죠. 그 이후로 Drizzle을 들여다보기 시작했고, 2025년 말부터 2026년에 걸쳐 몇 개의 프로젝트를 직접 전환해봤습니다. 번들 사이즈의 극단적인 차이, 엣지 런타임 네이티브 지원, 코드 생성 단계 없는 즉각적인 타입 반영 — 이 세 가지가 전환의 핵심 이유였는데, 막상 해보니 주의해야 할 지점도 꽤 있었습니다.
대상 독자는 이미 Prisma를 쓰고 있거나 쓸지 고민 중인 Node.js/TypeScript 백엔드·풀스택 개발자입니다.
어떤 상황에 어떤 선택인가
본론에 앞서 의사결정 기준을 먼저 정리합니다. 이 다이어그램 하나로 방향이 잡힌다면 나머지 본문은 그 근거를 뒷받침하는 자료로 활용하세요.
핵심 개념
설계 철학: "SQL을 알면 Drizzle을 안다"
Prisma와 Drizzle은 출발점부터 다릅니다.
Prisma는 .prisma라는 자체 DSL로 스키마를 정의하고, prisma generate로 클라이언트 코드를 생성합니다. 데이터베이스를 잘 모르는 팀원도 빠르게 생산적이 될 수 있다는 게 큰 장점이고, 실제로 이 추상화가 많은 팀에 잘 맞습니다.
Drizzle은 반대 방향을 택했습니다. 모토가 "If you know SQL, you know Drizzle ORM" 입니다. TypeScript 파일 안에서 직접 스키마를 정의하고, 코드 생성 단계가 없습니다. 컬럼 하나 추가하면 다음 타이핑부터 바로 타입에 반영됩니다.
// Drizzle 스키마 정의 — 순수한 TypeScript 파일
import { pgTable, serial, text, timestamp, decimal } from 'drizzle-orm/pg-core'
export const orders = pgTable('orders', {
id: serial('id').primaryKey(),
userId: text('user_id').notNull(),
amount: decimal('amount', { precision: 10, scale: 2 }).notNull(),
createdAt: timestamp('created_at').defaultNow(),
})// Prisma 스키마 정의 — 전용 DSL 파일
model Order {
id Int @id @default(autoincrement())
userId String
amount Decimal
createdAt DateTime @default(now())
}두 방식 모두 타입 안전성을 제공하지만, 그 타이밍과 방식이 다릅니다.
번들 사이즈: 숫자가 말해주는 것
| ORM | 크기 | 비고 |
|---|---|---|
| Drizzle ORM | ~7.4 KB (gzip 기준) | 외부 의존성 0개 |
| Prisma v7 | ~1.6 MB (패키지 설치 크기) | WASM 쿼리 컴파일러 도입 |
| Prisma v6 이하 | ~14 MB (패키지 설치 크기) | Rust 바이너리 엔진 |
Drizzle 7.4 KB와 Prisma v7 1.6 MB를 단순 나눗셈하면 약 216배 차이입니다. 단, 두 수치의 측정 기준(gzip vs. 설치 크기)이 다르므로 배수는 참고용으로 봐주세요. 동일 기준으로 비교해도 격차는 체감할 만큼 크다는 사실은 변하지 않습니다.
2025년 11월 출시된 Prisma v7이 Rust 바이너리를 WASM으로 교체하면서 크기를 크게 줄인 것은 인상적인 개선입니다. 그럼에도 Drizzle과의 격차는 여전합니다.
이게 실제로 왜 중요하냐면 — 서버리스 함수의 콜드 스타트입니다. Vercel Edge Functions, Cloudflare Workers, AWS Lambda@Edge에서 번들 크기는 함수 초기화 시간에 직결됩니다. Drizzle의 7.4 KB는 500ms 미만 콜드 스타트를 훨씬 수월하게 달성하게 해줍니다.
엣지 런타임 호환성: 이게 핵심 차이였습니다
Cloudflare Workers는 Node.js net/tls 모듈 API를 지원하지 않습니다. Workers가 2023년부터 connect() API로 TCP 자체를 지원하지만, 기존 PostgreSQL 드라이버들이 의존하는 net/tls 모듈이 동작하지 않기 때문에 표준 방식으로는 DB에 연결할 수 없습니다. Prisma v7 이전에는 이를 해결하려면 Prisma Accelerate라는 유료 프록시 서비스가 필수였죠.
Drizzle은 처음부터 이 제약을 고려해 설계됐습니다. HTTP 기반 드라이버 어댑터들이 이 문제를 근본적으로 해결합니다.
// Cloudflare Workers + Neon Postgres
import { drizzle } from 'drizzle-orm/neon-http'
import { neon } from '@neondatabase/serverless'
// HTTP 기반 연결 — Node.js net/tls 모듈 불필요
const sql = neon(env.DATABASE_URL)
const db = drizzle(sql)// Cloudflare D1 (Workers 내장 SQLite)
import { drizzle } from 'drizzle-orm/d1'
const db = drizzle(env.DB)Prisma v7에서도 네이티브 엣지 지원이 추가됐지만, 설정 복잡도 면에서는 여전히 Drizzle이 더 단순합니다.
Raw SQL 타입 안전성: 두 가지 접근법
"어차피 Raw SQL 쓰면 타입 안전성이 무슨 의미냐"고 생각할 수 있는데, 두 ORM의 접근 방식이 꽤 다릅니다.
Drizzle의 sql<T> 템플릿 리터럴
// 반환 타입을 제네릭으로 수동 지정
const result = await db.execute(
sql<{ count: number; revenue: string }>`
SELECT COUNT(*) as count, SUM(amount) as revenue
FROM ${orders}
WHERE created_at > ${startDate}
`
)
// result는 { count: number; revenue: string }[] 타입
// 쿼리 빌더와 Raw SQL 혼합도 가능
const complexQuery = await db
.select({
id: users.id,
postCount: sql<number>`COUNT(${posts.id})`,
})
.from(users)
.leftJoin(posts, eq(posts.userId, users.id))
.groupBy(users.id)장점은 생성 단계 없이 즉각 타입이 반영된다는 것, 단점은 T를 개발자가 수동으로 지정해야 한다는 것입니다. 스키마가 바뀌었을 때 컴파일 타임에 자동으로 잡아주지 않습니다.
Prisma TypedSQL
-- prisma/sql/getOrderStats.sql
SELECT COUNT(*) as count, SUM(amount) as revenue
FROM "Order"
WHERE created_at > $1// prisma generate 후 자동 생성된 타입으로 호출
import { getOrderStats } from '@prisma/client/sql'
const result = await prisma.$queryRawTyped(getOrderStats(startDate))
// 완전 자동 타입 추론 — 수동 지정 불필요Prisma TypedSQL은 .sql 파일로부터 입출력 타입을 완전히 자동 생성합니다. 더 견고한 타입 안전성이지만, prisma generate를 실행하기 전까지는 타입이 stale 상태로 남는다는 trade-off가 있습니다.
실전 적용
엣지 런타임 배포 시나리오
가장 많이 받는 질문이 "Cloudflare Workers에서 DB를 어떻게 붙이냐"입니다.
// Hono + Drizzle + Neon (Cloudflare Workers)
import { Hono } from 'hono'
import { drizzle } from 'drizzle-orm/neon-http'
import { neon } from '@neondatabase/serverless'
import { orders } from './schema'
import { eq, desc } from 'drizzle-orm'
type Bindings = {
DATABASE_URL: string
}
const app = new Hono<{ Bindings: Bindings }>()
app.get('/orders/:userId', async (c) => {
const sql = neon(c.env.DATABASE_URL)
const db = drizzle(sql)
const userOrders = await db
.select()
.from(orders)
.where(eq(orders.userId, c.req.param('userId')))
.orderBy(desc(orders.createdAt))
.limit(20)
return c.json(userOrders)
})
export default appHono 공식 문서에 Drizzle 예시가 비중 있게 수록되어 있는 이유가 이 단순함입니다.
관계형 쿼리 — Prisma vs Drizzle 비교
Prisma에서 Drizzle로 넘어올 때 가장 많이 헷갈리는 부분입니다. 핵심은 반환 데이터 구조가 다르다는 점입니다. 코드를 1:1로 교체하면 바로 런타임 오류로 이어질 수 있습니다.
// Prisma — include로 관계 조회, 중첩 객체 반환
const postsWithAuthor = await prisma.post.findMany({
include: {
author: { select: { id: true, name: true } }
},
where: { published: true }
})
// 반환 구조: { id, title, author: { id, name } }
// Drizzle select join — 평탄한 객체 반환 (구조가 다름!)
const postsWithAuthor = await db
.select({
id: posts.id,
title: posts.title,
authorId: users.id,
authorName: users.name,
})
.from(posts)
.innerJoin(users, eq(posts.authorId, users.id))
.where(eq(posts.published, true))
// 반환 구조: { id, title, authorId, authorName } — 중첩 없음
// Prisma include와 동일한 중첩 구조를 원하면 relational query API 사용
const postsWithAuthor = await db.query.posts.findMany({
where: eq(posts.published, true),
with: {
author: { columns: { id: true, name: true } }
}
})
// 반환 구조: { id, title, author: { id, name } }Prisma의 include 결과 구조를 그대로 유지하려면 select join 방식이 아니라 relational query API의 with를 써야 합니다. 복잡한 중첩 관계 조회가 많은 프로젝트라면 이 부분을 먼저 확인해보세요.
프로덕션 마이그레이션 전략
라이브 서비스를 다운타임 없이 전환하는 방법으로 병렬 실행 전략이 효과적입니다.
먼저 현재 DB 구조를 Drizzle 스키마로 가져옵니다.
# drizzle.config.ts 설정 후 실행
npx drizzle-kit introspect생성된 스키마 파일은 반드시 수동 검토하세요. NOT NULL 컬럼이 nullable로 잘못 생성되는 경우가 있습니다. 특히 원본 DB 스키마와 컬럼 제약 조건을 직접 대조해보는 것을 권장합니다.
병렬 실행 단계의 코드입니다.
import { PrismaClient } from '@prisma/client'
import { drizzle } from 'drizzle-orm/postgres-js'
import postgres from 'postgres'
import { eq, desc } from 'drizzle-orm'
import * as schema from './drizzle/schema'
const prisma = new PrismaClient()
const queryClient = postgres(process.env.DATABASE_URL!)
const db = drizzle(queryClient, { schema })
// 새로 작성하는 쿼리는 Drizzle로
export async function getRecentOrders(userId: string) {
return db
.select()
.from(schema.orders)
.where(eq(schema.orders.userId, userId))
.orderBy(desc(schema.orders.createdAt))
}
// 기존 Prisma 쿼리는 건드리지 않고 유지
export async function getUserProfile(id: string) {
return prisma.user.findUnique({ where: { id } })
}20개 모델, 100개 쿼리 호출 지점 규모의 중소 SaaS를 이 방식으로 직접 전환해봤습니다. 혼자 작업하면 24일, 팀이 나눠서 하면 12주 안에 다운타임 없이 완료할 수 있었습니다.
주요 함정 — 미리 알면 좋은 것들
1. 테이블명 케이싱 문제
// Prisma 기본 테이블명은 PascalCase
// Drizzle 전환 시 반드시 동일한 이름 사용
export const users = pgTable('User', { // 'user' 아님
id: text('id').primaryKey(),
name: text('name').notNull(),
})2. CUID 기본값 재구현
import { createId } from '@paralleldrive/cuid2'
export const posts = pgTable('Post', {
// Prisma의 @default(cuid()) 대체
id: text('id').primaryKey().$defaultFn(() => createId()),
title: text('title').notNull(),
})3. 자동 타임스탬프 갱신
Drizzle은 .$onUpdate() 훅으로 컬럼 수준에서 자동 갱신을 설정할 수 있습니다.
export const posts = pgTable('Post', {
id: text('id').primaryKey(),
title: text('title').notNull(),
updatedAt: timestamp('updated_at')
.defaultNow()
.$onUpdate(() => new Date()),
})
// $onUpdate를 설정하지 않은 컬럼은 UPDATE마다 명시적으로 지정
await db
.update(posts)
.set({ title: newTitle, updatedAt: new Date() })
.where(eq(posts.id, postId))4. Decimal 타입 처리
Drizzle은 소수점 정밀도 보존을 위해 Decimal 컬럼을 string으로 반환합니다. Prisma는 Prisma.Decimal 객체(decimal.js 기반)로 반환합니다. 두 방식 모두 + 연산이나 parseFloat 적용 시 정밀도를 잃을 수 있습니다.
import Decimal from 'decimal.js'
const order = await db.select().from(orders).where(eq(orders.id, 1))
// string → Decimal 객체로 변환 후 연산 (정밀도 유지)
const amount = new Decimal(order[0].amount)
const withTax = amount.mul(1.1).toFixed(2)
// 단순 표시 목적이라면 parseFloat도 가능하지만 금융 계산에는 적합하지 않음장단점 분석
두 ORM 비교표
| 기준 | Drizzle ORM | Prisma v7 |
|---|---|---|
| 번들 크기 | ~7.4 KB (gzip) | ~1.6 MB (패키지 설치 크기) |
| 외부 의존성 | 0개 | 있음 |
| 엣지 런타임 | 네이티브 지원 | v7부터 지원 (설정 복잡) |
| 코드 생성 단계 | 없음 | prisma generate 필수 |
| 스키마 정의 | TypeScript 파일 | .prisma DSL |
| Raw SQL 타입 | 수동 sql<T> 지정 |
완전 자동 TypedSQL |
| 관계형 쿼리 API | join / with 기반 |
include / select 직관적 |
| 자동 타임스탬프 갱신 | .$onUpdate() 훅 |
@updatedAt 자동 |
| Decimal 반환 타입 | string |
Prisma.Decimal 객체 |
| 생태계 성숙도 | 빠르게 성장 중 | 성숙, 풍부한 문서 |
| 콜드 스타트 | 유리 | 상대적으로 불리 |
실무에서 흔한 실수
| 실수 | 증상 | 해결 |
|---|---|---|
introspect 결과 무검토 사용 |
NOT NULL 컬럼이 nullable로 잘못 생성 | 생성된 스키마 파일 수동 검토 |
| 테이블명 케이싱 불일치 | 런타임에 테이블 없음 오류 | Prisma 기본값 PascalCase 확인 후 동일하게 지정 |
| Decimal 직접 연산 | 정밀도 손실 | decimal.js 객체로 처리 |
updatedAt 누락 |
업데이트 시각이 갱신되지 않음 | .$onUpdate() 설정 또는 모든 UPDATE에 명시 |
| 병렬 실행 없이 일괄 전환 | 다운타임 또는 롤백 필요 | 병렬 실행으로 점진적 전환 |
마치며
두 ORM의 핵심 차이를 정리하면 이렇습니다.
Drizzle은 SQL 개발자가 TypeScript로 SQL을 쓰는 경험을 추구합니다. 번들이 작고, 엣지에서 바로 돌고, 코드 생성 없이 즉각적인 타입이 필요하다면 Drizzle이 맞습니다.
Prisma는 데이터베이스 추상화를 통해 팀 생산성을 높이는 방향입니다. TypedSQL의 완전 자동 타입, 직관적인 관계형 쿼리 API, 풍부한 생태계가 필요하다면 Prisma v7이 여전히 좋은 선택입니다.
2025~2026년 기준으로 Drizzle은 npm 주간 다운로드에서 Prisma와의 격차를 빠르게 좁히고 있고, Hono·Astro DB 같은 엣지 퍼스트 생태계에서 Drizzle 예시가 비중 있게 다뤄지고 있습니다. 그렇다고 Prisma가 레거시가 됐다는 게 아니라, 대형 엔터프라이즈 모노리스에서는 Prisma, 서버리스·엣지 프로젝트에서는 Drizzle이라는 구도가 명확해진 것입니다.
지금 전환을 고려하고 있다면 이 순서로 접근해보세요.
지금 시작하는 3단계
npx drizzle-kit introspect로 기존 DB 스냅샷 — 마이그레이션 베이스라인 확보. 생성된 스키마의 nullable 여부를 원본 DB와 대조해 수동 검토까지.- Prisma와 병렬 실행 — 별도 연결로 두 클라이언트를 공존시키고, 새 쿼리부터 Drizzle로 작성하면서 점진적 전환.
- 엣지 배포가 목표라면 드라이버 어댑터부터 — Neon HTTP, Cloudflare D1 중 환경에 맞는 드라이버를 먼저 연결하고, 실제 배포 파이프라인에서 번들 크기와 콜드 스타트를 직접 측정해보기.
참고 자료
- Drizzle ORM 공식 문서 - Overview
- Drizzle ORM - Magic sql`` operator
- Drizzle ORM - Vercel Edge Functions 튜토리얼
- Prisma 공식 문서 - Prisma vs Drizzle 비교
- Prisma 공식 - TypedSQL 소개
- Prisma 공식 - 엣지 배포 개요
- Bytebase - Drizzle ORM vs Prisma: 2026년 어떤 TypeScript ORM을 선택해야 하는가
- Encore - Drizzle vs Prisma in 2026
- Better Stack - Drizzle vs Prisma: TypeScript ORM 선택 가이드
- MakerKit - Drizzle vs Prisma ORM in 2026: 실용적 비교
- toolchew - Prisma에서 Drizzle ORM으로 마이그레이션하는 방법: 단계별 가이드
- ormlab - Prisma에서 Drizzle로 헤드에이크 없이 마이그레이션하기
- DEV Community - 라이브 SaaS에서 Prisma를 Drizzle로 전환한 경험
- PkgPulse - Node.js ORM 현황 2026
- GitHub - drizzle-team/drizzle-orm