03 — TypeORM vs Drizzle (ORM vs SQL DSL)
한 줄 답: Drizzle은 ORM이 아니다 — SQL의 TypeScript DSL이다. 런타임에 메타데이터를 들고 다니지 않으며, TypeORM은 런타임 메타로 모든 마법을 한다. 런타임 제로 코스트가 필요한 서버리스·엣지에 Drizzle, OOP 도메인 객체와 관계 풍부 모델에 TypeORM.
Why — 왜 Drizzle이 TypeScript 진영의 떠오르는 별이 됐나
Drizzle은 2022년 등장해 2023~2024년에 폭발적으로 성장했다. GitHub stars는 2024년 ~20k에 도달했고 — State of JS 2023에서 “가장 흥미로운 ORM” 1위로 뽑혔다.
세 가지 이유.
- 런타임 제로 코스트: 메타데이터를 컴파일 타임에 TS 타입으로만 표현 — 런타임에는 그냥 SQL 생성 함수. 서버리스 cold start가 거의 없다.
- SQL 그대로의 사고: ORM 추상이 거의 없다.
select().from().where()는 SQL 키워드 그대로. SQL을 아는 팀에게 학습 비용이 0에 가깝다. - ESM 우선: 2022년 이후 등장한 도구라 ESM이 기본. TypeORM의 CJS 친화 + ESM 부분 지원과 대조.
Drizzle의 캐치프레이즈: “It’s like ORM but it isn’t” — 의도적으로 ORM이 아니길 자처한다.
How — 두 도구의 근본 메커니즘이 어떻게 다른가
1) TypeORM — 런타임 메타데이터 기반
import 'reflect-metadata'
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm'
@Entity()
class User {
@PrimaryGeneratedColumn()
id!: number
@Column()
email!: string
}
// 런타임: TypeORM이 reflect-metadata를 *읽어* SQL 생성
const users = await userRepo.find({ where: { email: 'x@x.com' } })- 런타임에 reflect-metadata가 메모리에 살아 있어야 한다.
- TypeORM 코어는 그 메타를 읽고 SQL을 만든다 — 일종의 작은 인터프리터.
2) Drizzle — SQL DSL, 런타임 메타 없음
import { pgTable, serial, text } from 'drizzle-orm/pg-core'
import { eq } from 'drizzle-orm'
// 테이블 정의 — *런타임 메타가 아니라 컴파일 타임 객체*
export const users = pgTable('users', {
id: serial('id').primaryKey(),
email: text('email').notNull(),
})
// 사용
const result = await db.select().from(users).where(eq(users.email, 'x@x.com'))
// ↑ 거의 SQL 그대로
// → "select * from users where email = 'x@x.com'"pgTable(...)은 런타임에 단순 객체다. 데코레이터 메타데이터 없음, reflect-metadata 없음.select().from().where()는 SQL 빌더. 런타임에는 문자열 SQL 생성기가 동작할 뿐.- TypeScript 컴파일러가 컬럼 타입을 추론해 반환 타입까지 완전히 채워준다.
3) 두 메커니즘의 근본 차이
What — 구체적 시나리오로 비교
시나리오 A — 기본 select
TypeORM:
const users = await userRepo.find({
where: { age: MoreThan(18) },
order: { createdAt: 'DESC' },
take: 10
})Drizzle:
import { gt, desc } from 'drizzle-orm'
const result = await db
.select()
.from(users)
.where(gt(users.age, 18))
.orderBy(desc(users.createdAt))
.limit(10)판정: Drizzle이 SQL에 더 가깝다. TypeORM의 MoreThan, Like 같은 연산자 wrapper가 더 abstract. SQL에 익숙한 팀은 Drizzle이 바로 읽힌다.
시나리오 B — Join
TypeORM:
const posts = await postRepo.find({
relations: { author: true, comments: true }
})
// 또는 QueryBuilder
const posts = await postRepo
.createQueryBuilder('post')
.leftJoinAndSelect('post.author', 'author')
.leftJoinAndSelect('post.comments', 'comment')
.getMany()Drizzle:
const result = await db
.select()
.from(posts)
.leftJoin(users, eq(posts.authorId, users.id))
.leftJoin(comments, eq(comments.postId, posts.id))
// ← *완전히 SQL 그대로*판정: TypeORM의 relations는 마법처럼 편하지만 N+1을 숨긴다. Drizzle은 N+1이 일어날 수 없다 — 모든 join이 명시적. 디버깅·성능 분석 모두 Drizzle이 투명.
시나리오 C — 서버리스 cold start
TypeORM (AWS Lambda):
// 차가운 람다 호출 첫 응답: ~300-500ms
// (reflect-metadata 로드 + DataSource.initialize + 메타 빌드)Drizzle (AWS Lambda):
// 차가운 람다 호출 첫 응답: ~50-100ms
// (SQL 빌더 함수 import만)판정: Drizzle 압승. 서버리스/엣지가 매출에 직결되면 Drizzle이 기본 선택. Vercel·Cloudflare Workers·AWS Lambda의 공식 권장 중 하나.
시나리오 D — 복잡한 도메인 객체 (OOP)
TypeORM:
@Entity()
class Order {
@PrimaryGeneratedColumn()
id!: number
@Column()
amount!: number
// 도메인 메서드를 *클래스에 직접* 정의
isOverdue(): boolean {
return this.createdAt < daysAgo(30) && this.status === 'pending'
}
applyDiscount(rate: number): void {
this.amount = this.amount * (1 - rate)
}
}Drizzle:
export const orders = pgTable('orders', { ... })
// ← *클래스가 아니다*. 메서드를 *붙일 곳이 없다*.
// 도메인 로직은 *별도 모듈*로 짜야 함
function isOverdue(order: Order): boolean { ... }
function applyDiscount(order: Order, rate: number): Order { ... }판정: TypeORM 우세. DDD/OOP 스타일 도메인 모델이면 클래스에 메서드를 붙이는 TypeORM이 자연스럽다. Drizzle은 함수형 접근을 강제.
시나리오 E — 마이그레이션
TypeORM:
npm run typeorm migration:generate -- -n AddAge
# ← 종종 부정확한 SQL 생성Drizzle:
npx drizzle-kit generate
# ← schema.ts 파일 diff → SQL 마이그레이션 생성
# ← 일반적으로 *안정적*판정: Drizzle 우세. drizzle-kit은 Prisma migrate에 가까운 안정성을 보인다.
What-if — 잘못 선택하면
1) OOP 도메인 모델인데 Drizzle
→ 클래스 메서드·상속·polymorphism을 못 쓴다. 함수형 스타일을 강제받음. 대응: 풍부한 도메인 객체가 필요하면 TypeORM 또는 MikroORM.
2) 서버리스인데 TypeORM
→ Lambda cold start가 300-500ms. p99 응답이 목표를 못 맞춤. 대응: Drizzle 또는 Prisma + Driver Adapters.
3) SQL에 익숙하지 않은 팀인데 Drizzle
→ Drizzle은 SQL 추상이 거의 없다. SQL을 모르면 Drizzle 자체를 못 쓴다. 대응: SQL 학습부터, 또는 TypeORM/Prisma의 추상에 의지.
4) NestJS 통합이 필요한데 Drizzle
→ Drizzle은 NestJS 공식 모듈이 없다. DI 통합을 직접 짜야 한다.
대응: NestJS 친화도가 매출이면 TypeORM (@nestjs/typeorm).
5) “Drizzle도 ORM의 일종”이라 기대
→ Identity Map·Unit of Work·자동 cascade 같은 ORM 기능을 기대했다가 없어서 좌절. 대응: 처음부터 SQL 빌더라 인정하고 그 위에 도메인 레이어를 직접 쌓는다.
Insight — 흥미로운 이야기
”Drizzle은 Knex의 후예다”
Drizzle의 DNA는 Knex.js (2013, JavaScript의 SQL query builder)에 가깝다. Knex는 ORM이 아니라 query builder였고 — 한때 Bookshelf.js의 기반 레이어였다.
Drizzle은 Knex의 사고를 TypeScript로 옮긴 도구다 — 완전한 TS 타입 추론과 ESM 우선을 더해서. *“ORM이 아니다”*라는 자처가 명예가 아니라 사양이다.
”Vercel·Neon의 밀어주기”
Drizzle은 Vercel과 Neon(Postgres serverless)의 공식 권장에 들어 있다. 두 회사는 서버리스 Postgres를 시장에 밀고 있고 — TypeORM의 cold start가 그 시장에서 치명적이라는 것을 안다.
2024년 Vercel의 Postgres 가이드는 Drizzle과 Prisma (Driver Adapters)를 동시에 추천한다. TypeORM은 등장하지 않는다. 이게 생태계의 신호다.
”Drizzle의 명시성 철학”
Drizzle의 README에는 *“We do not want to be a magic library”*라고 직접 쓰여 있다. TypeORM의 데코레이터 마법, Prisma의 코드젠 마법, MikroORM의 Unit of Work 마법 — 모두를 거부하는 입장.
이게 현대 TypeScript 진영의 트렌드와 맞아떨어진다. tRPC도, Drizzle도, Hono도 — *“마법 없이 타입만”*의 같은 가족이다. 명시성을 사양으로 삼은 도구들.
요약 + 다이어그램
TypeORM vs Drizzle = ORM vs SQL DSL. Drizzle은 런타임 메타가 0이라 서버리스·엣지에 강하고, TypeORM은 런타임 메타로 OOP 도메인을 풍부하게 표현한다. 서버리스 + SQL 친숙 팀이면 Drizzle, OOP 도메인 + NestJS면 TypeORM.
다음 문서:
04-typeorm-vs-mikroorm.mdx— 그럼 진짜 Unit of Work를 가진 MikroORM은?