테마 전환

Drizzle ORM 실전 가이드: Prisma보다 90% 가벼운 TypeScript ORM

Easton editorial illustration: component assembly loom

지난주 프로젝트를 Vercel에 배포했는데 홈페이지가 열릴 때까지 꼬박 3초가 걸렸습니다. 3초입니다. 사용자는 이미 떠나고도 남을 시간입니다.

Vercel의 번들 분석을 열어 14MB를 차지하는 Prisma Client 패키지를 보며 한숨이 나왔습니다. 단순한 사용자 조회일 뿐인데 왜 이렇게 많은 코드를 불러와야 할까요? 더 심각한 문제는 Lambda 콜드 스타트였습니다. Prisma를 쓰지 않은 함수는 600ms 만에 시작했지만, Prisma를 사용한 함수는 무려 2.5초가 걸렸습니다.

Prisma가 사용하기 편하지 않냐고 물을 수 있습니다. 저도 그렇게 생각했습니다. 타입 안전성, 자동 마이그레이션, Prisma Studio 시각화 도구까지 필요한 기능은 모두 갖췄습니다. 하지만 문제는 하나입니다. 너무 무겁습니다.

serverless 환경에서 콜드 스타트가 느리고, 번들 크기가 제한을 넘고, 복잡한 쿼리는 결국 raw SQL로 작성해야 하는 상황을 겪었다면 이 글이 도움이 될 것입니다. 여기서 다룰 Drizzle ORM의 핵심 패키지는 7.4kb에 불과해 Prisma보다 90% 이상 가볍지만 같은 수준의 타입 안전성을 제공합니다.

Next.js + Drizzle을 처음부터 설정하고 SQL-like API를 사용하는 방법을 살펴본 뒤, Drizzle과 Prisma의 성능 차이를 깊이 비교합니다. 무엇보다 어떤 상황에서 Drizzle을 선택해야 하고, 언제 Prisma가 여전히 더 나은 선택인지 구체적으로 설명하겠습니다.

Drizzle이 필요한 이유: 기존 ORM의 문제점

Prisma의 세 가지 문제점

Prisma는 많은 상황에서 분명 좋은 선택입니다. 하지만 오래 사용하다 보면 피하기 어려운 문제가 드러납니다.

문제 1: 통제하기 어려운 번들 크기

Prisma v5에서 생성되는 Client는 14MB에 이를 수 있습니다. 전체 Next.js 프로젝트가 2~3MB 정도일 수 있는데 Prisma 하나가 그 몇 배를 차지하는 셈입니다. Prisma 7에서는 Rust 바이너리를 제거해 1MB까지 줄였지만, 프로젝트가 구버전을 사용하거나 번들 크기에 특히 민감하다면 여전히 큰 문제입니다.

우리 팀의 실시간 채팅 앱은 Cloudflare Workers에 배포할 때 번들 크기가 1MB를 넘을 수 없었습니다. Prisma는 곧바로 제한을 초과했고 결국 다른 방식을 선택해야 했습니다.

문제 2: 느린 Serverless 콜드 스타트

GitHub Issue #10724에서는 Prisma의 느린 Lambda 콜드 스타트 문제가 몇 년째 논의되고 있습니다. 수치는 분명합니다.

  • ORM을 사용하지 않은 함수: 약 600ms
  • Prisma v5를 사용한 함수: 약 2.5초
  • Prisma v7을 사용한 함수: 약 1.5초(개선됐지만 여전히 느림)

그 원인은 Prisma가 시작할 때 거대한 DMMF(Data Model Meta Format) 문자열을 파싱해야 한다는 데 있습니다. 중간 규모 스키마에서도 이 문자열은 600만 자를 넘을 수 있습니다. 콜드 스타트 때마다 이를 다시 파싱하니 느릴 수밖에 없습니다.

오픈 소스 일정 관리 도구인 Cal.com은 이 문제 때문에 콜드 스타트 최적화 방법을 별도 기술 블로그로 다루기도 했습니다. 결론은 문제가 실제로 존재하며 여러 workaround로 완화할 수밖에 없다는 것이었습니다.

문제 3: 부족한 SQL 제어력

Prisma는 SQL을 추상화하고 Prisma Client의 DSL로 쿼리를 작성하게 하는 철학을 따릅니다. 대부분의 경우 편리하지만 복잡한 쿼리에서는 문제가 됩니다.

다단계 JOIN, 서브쿼리, 조건부 집계를 결합한 쿼리는 Prisma API로 표현하지 못할 수 있습니다. 결국 prisma.$queryRaw로 직접 SQL을 작성하게 됩니다.

어차피 마지막에는 SQL을 작성해야 한다면 처음부터 SQL-like API를 쓰는 편이 낫지 않을까요? 이것이 Drizzle의 접근 방식입니다.

개발자에게 실제로 필요한 것

개발자가 원하는 요구 사항을 정리하면 다음과 같습니다.

  1. 성능을 희생하지 않는 타입 안전성: TypeScript 타입 추론을 제공하되 ORM 때문에 프로젝트가 느려지면 안 됩니다.
  2. 새 DSL 대신 SQL 직접 사용: SQL이면 충분하므로 Prisma 전용 쿼리 문법을 또 익히고 싶지 않습니다.
  3. Serverless 친화성: 2025년인 지금 Vercel, Cloudflare Workers, AWS Lambda를 사용하지 않을 이유가 없습니다.
  4. 빠른 컴파일: 대규모 프로젝트에서 Prisma의 타입 추론은 TypeScript 컴파일 속도를 떨어뜨립니다. 이것도 숨은 비용입니다.

Drizzle은 바로 이런 요구를 겨냥합니다.

Drizzle ORM이란? 핵심 기능 살펴보기

Drizzle의 설계 철학

Drizzle의 슬로건은 **“If you know SQL, you know Drizzle”**입니다. 자신감 넘치는 문구지만 실제로 그대로 구현했습니다.

"If you know SQL, you know Drizzle."

기존 ORM은 SQL을 추상화해 개발자가 직접 SQL을 작성하지 않도록 합니다. Drizzle은 반대로 SQL을 추상화하지 않고 TypeScript API를 최대한 SQL 문법에 가깝게 만듭니다. 작성하는 코드는 SQL처럼 보이지만 완전한 타입 힌트를 제공합니다.

예시를 보면 바로 이해할 수 있습니다.

// Drizzle 쿼리
await db
  .select()
  .from(posts)
  .leftJoin(comments, eq(posts.id, comments.postId))
  .where(eq(posts.id, 10))

// 생성되는 SQL
SELECT * FROM posts
LEFT JOIN comments ON posts.id = comments.post_id
WHERE posts.id = 10

코드 구조가 SQL과 거의 같습니다. SQL을 아는 사람은 보기만 해도 사용법을 알 수 있습니다.

핵심 기능

1. 극도로 가벼운 크기

핵심 패키지 drizzle-orm7.4kb(min+gzip)에 불과하며 런타임 의존성이 전혀 없습니다. 비교하면 다음과 같습니다.

  • Drizzle: 약 7.4kb
  • TypeORM: 약 300kb
  • Prisma v7: 약 1MB
  • Prisma v5: 약 14MB

단순한 차이가 아니라 규모 자체가 다릅니다.

2. TypeScript-First, Client 생성 불필요

Prisma는 prisma generate를 실행해 Client를 생성해야 합니다. Drizzle에는 이 과정이 없습니다.

스키마를 정의하면 TypeScript가 타입을 직접 추론합니다. IntelliSense 자동 완성, 타입 검사, 오류 안내를 모두 제공하며 런타임 전에 컴파일 시점에서 문제를 발견할 수 있습니다.

3. SQL-like API로 거의 없는 학습 비용

SQL을 작성할 수 있다면 Drizzle은 10분 안에 익힐 수 있습니다.

// SELECT 조회
db.select().from(users).where(eq(users.id, 1))

// INSERT 삽입
db.insert(users).values({ name: 'John', email: '[email protected]' })

// UPDATE 수정
db.update(users).set({ name: 'Jane' }).where(eq(users.id, 1))

// DELETE 삭제
db.delete(users).where(eq(users.id, 1))

SQL을 아는 사람이라면 이 코드를 보고 문서를 찾을 필요가 없습니다.

4. 성능 손실 없음

Drizzle에는 런타임 추상화 계층이 없습니다. 작성한 쿼리는 중간 단계를 거치지 않고 SQL로 직접 변환됩니다.

시작할 때 DMMF를 파싱하고 내부 상태를 유지해야 하는 Prisma와 달리 Drizzle은 순수한 쿼리 빌더입니다. 숨은 비용도 예상치 못한 오버헤드도 없습니다.

5. Serverless-Ready

Drizzle은 주요 serverless 환경을 모두 지원합니다.

  • Vercel Edge Functions
  • Cloudflare Workers
  • AWS Lambda
  • Deno Deploy
  • Bun

serverless 데이터베이스 드라이버도 네이티브로 지원합니다.

  • Neon Serverless
  • PlanetScale
  • Turso(SQLite on the edge)
  • Supabase

우리 프로젝트는 Neon + Drizzle을 Vercel Edge에 배포해 콜드 스타트를 2.5초에서 700ms로 단축했습니다. 실제로 확인한 효과입니다.

적합한 사용 사례

Drizzle이 만능은 아니지만 다음 상황에는 특히 잘 맞습니다.

1. Serverless 애플리케이션

애플리케이션이 Lambda나 Edge Functions에서 실행된다면 Drizzle의 작은 크기와 빠른 콜드 스타트가 꼭 필요합니다.

2. 성능에 민감한 환경

실시간 애플리케이션, 금융 시스템, 데이터 분석 플랫폼처럼 쿼리 지연 시간에 민감한 환경에서는 Drizzle의 무추상화 설계가 실질적인 성능 향상으로 이어집니다.

3. 복잡한 SQL을 직접 제어해야 하는 프로젝트

복잡한 쿼리가 많고 SQL을 직접 최적화해야 한다면 Drizzle의 SQL-like API가 Prisma DSL보다 훨씬 유용합니다.

4. 번들 크기에 민감한 프런트엔드 프로젝트

SolidStart나 Qwik 같은 일부 풀스택 프레임워크는 ORM 코드를 클라이언트 번들에 포함합니다. 이런 경우 Drizzle의 7.4kb 크기가 큰 장점입니다.

반대로 팀이 SQL에 익숙하지 않거나 빠르게 프로토타입을 만들어야 하고, Prisma Studio, Prisma Migrate, Prisma Pulse 같은 통합 도구를 선호한다면 Prisma가 더 나을 수 있습니다.

Next.js + Drizzle 실전 설정

이제 이론을 넘어 실제로 설정해 보겠습니다. Next.js 15 + Drizzle + PostgreSQL 프로젝트를 처음부터 구성합니다.

환경 준비

먼저 Next.js 프로젝트를 만듭니다.

npx create-next-app@latest my-drizzle-app
cd my-drizzle-app

Drizzle 관련 의존성을 설치합니다.

npm install drizzle-orm drizzle-kit
npm install @neondatabase/serverless  # Neon 데이터베이스를 사용하는 경우
# 또는
npm install postgres  # 기존 PostgreSQL을 사용하는 경우

여기서는 Neon을 권장합니다. Drizzle과 잘 맞는 serverless PostgreSQL입니다. 무료 계정을 만들고 데이터베이스를 생성한 뒤 연결 문자열을 받으면 됩니다.

데이터베이스 스키마 정의

db/schema.ts를 만들고 테이블 구조를 정의합니다.

import { pgTable, serial, text, timestamp, integer } from 'drizzle-orm/pg-core';
import { relations } from 'drizzle-orm';

// 사용자 테이블
export const users = pgTable('users', {
  id: serial('id').primaryKey(),
  name: text('name').notNull(),
  email: text('email').notNull().unique(),
  createdAt: timestamp('created_at').defaultNow(),
});

// 게시물 테이블
export const posts = pgTable('posts', {
  id: serial('id').primaryKey(),
  title: text('title').notNull(),
  content: text('content'),
  authorId: integer('author_id').references(() => users.id),
  createdAt: timestamp('created_at').defaultNow(),
});

// 관계 정의(일대다)
export const usersRelations = relations(users, ({ many }) => ({
  posts: many(posts),
}));

export const postsRelations = relations(posts, ({ one }) => ({
  author: one(users, {
    fields: [posts.authorId],
    references: [users.id],
  }),
}));

보이는 그대로 TypeScript 코드입니다. Prisma처럼 별도 스키마 파일을 사용하지 않으며 Client를 생성할 필요도 없습니다.

데이터베이스 연결 설정

db/index.ts를 만듭니다.

import { drizzle } from 'drizzle-orm/neon-http';
import { neon } from '@neondatabase/serverless';
import * as schema from './schema';

// 환경 변수에서 데이터베이스 연결 정보 읽기
const sql = neon(process.env.DATABASE_URL!);

// Drizzle 인스턴스 생성
export const db = drizzle(sql, { schema });

이것으로 끝입니다. .env.local에 다음 값을 추가합니다.

DATABASE_URL=postgres://user:[email protected]/dbname

Drizzle Kit 설정(마이그레이션 도구)

drizzle.config.ts를 만듭니다.

import { defineConfig } from 'drizzle-kit';

export default defineConfig({
  schema: './db/schema.ts',
  out: './drizzle',
  dialect: 'postgresql',
  dbCredentials: {
    url: process.env.DATABASE_URL!,
  },
});

마이그레이션 파일을 생성합니다.

npx drizzle-kit generate

drizzle/ 디렉터리에 SQL 마이그레이션 파일이 생성됩니다. 생성된 SQL을 확인하고 문제가 없다면 실행합니다.

npx drizzle-kit push

이제 데이터베이스 테이블이 만들어졌습니다.

최종 프로젝트 구조

my-drizzle-app/
├── app/                  # Next.js 애플리케이션
│   ├── page.tsx
│   └── actions.ts        # Server Actions
├── db/
│   ├── schema.ts         # 테이블 및 관계 정의
│   └── index.ts          # 데이터베이스 연결
├── drizzle/
│   └── migrations/       # 마이그레이션 파일(자동 생성)
├── drizzle.config.ts     # Drizzle Kit 설정
├── .env.local            # 환경 변수
└── package.json

전체 설정은 5분이면 충분합니다. 복잡한 Prisma Schema 문법도, 오래 걸리는 prisma generate도 없이 순수 TypeScript 코드만 사용합니다.

Drizzle SQL-like API 실전 활용

설정을 마쳤으니 Drizzle로 쿼리를 작성하는 방법을 살펴보겠습니다. 가장 자주 쓰는 작업과 Next.js Server Actions에서의 실제 사용법을 다룹니다.

기본 CRUD 작업

데이터 조회(SELECT)

import { db } from '@/db';
import { users, posts } from '@/db/schema';
import { eq, like, and, or, desc } from 'drizzle-orm';

// 모든 사용자 조회
const allUsers = await db.select().from(users);

// 사용자 한 명 조회
const user = await db.select().from(users).where(eq(users.id, 1));

// 유사 검색
const result = await db.select().from(users).where(like(users.name, '%John%'));

// 복합 조건
const admins = await db
  .select()
  .from(users)
  .where(
    and(
      eq(users.role, 'admin'),
      gt(users.createdAt, new Date('2024-01-01'))
    )
  );

// 정렬 및 개수 제한
const latestPosts = await db
  .select()
  .from(posts)
  .orderBy(desc(posts.createdAt))
  .limit(10);

SQL의 논리를 함수 호출로 바꾼 것뿐입니다.

데이터 삽입(INSERT)

// 한 건 삽입
await db.insert(users).values({
  name: 'John Doe',
  email: '[email protected]',
});

// 여러 건 삽입
await db.insert(users).values([
  { name: 'Alice', email: '[email protected]' },
  { name: 'Bob', email: '[email protected]' },
]);

// 삽입된 데이터 반환
const [newUser] = await db
  .insert(users)
  .values({ name: 'Charlie', email: '[email protected]' })
  .returning();

console.log(newUser.id); // 자동 생성된 ID

데이터 수정(UPDATE)

// 한 건 수정
await db
  .update(users)
  .set({ name: 'Jane Doe' })
  .where(eq(users.id, 1));

// 여러 건 수정
await db
  .update(posts)
  .set({ published: true })
  .where(eq(posts.authorId, 1));

// 수정된 데이터 반환
const [updatedUser] = await db
  .update(users)
  .set({ name: 'Updated Name' })
  .where(eq(users.id, 1))
  .returning();

데이터 삭제(DELETE)

// 한 건 삭제
await db.delete(users).where(eq(users.id, 1));

// 여러 건 삭제
await db.delete(posts).where(eq(posts.published, false));

// 삭제된 데이터 반환
const deleted = await db
  .delete(users)
  .where(eq(users.id, 1))
  .returning();

고급 쿼리

JOIN 작업

// LEFT JOIN: 사용자와 게시물 조회
const usersWithPosts = await db
  .select({
    userId: users.id,
    userName: users.name,
    postId: posts.id,
    postTitle: posts.title,
  })
  .from(users)
  .leftJoin(posts, eq(users.id, posts.authorId));

// INNER JOIN: 게시물이 있는 사용자만 조회
const activeAuthors = await db
  .select()
  .from(users)
  .innerJoin(posts, eq(users.id, posts.authorId));

서브쿼리

// 게시물이 5개보다 많은 사용자 조회
const sq = db
  .select({ authorId: posts.authorId, count: count() })
  .from(posts)
  .groupBy(posts.authorId)
  .having(gt(count(), 5))
  .as('sq');

const prolificAuthors = await db
  .select()
  .from(users)
  .innerJoin(sq, eq(users.id, sq.authorId));

집계 함수

import { count, sum, avg } from 'drizzle-orm';

// 전체 사용자 수 집계
const [{ total }] = await db
  .select({ total: count() })
  .from(users);

// 작성자별 게시물 수 집계
const postCounts = await db
  .select({
    authorId: posts.authorId,
    count: count(),
  })
  .from(posts)
  .groupBy(posts.authorId);

Next.js Server Actions에서 사용하기

app/actions.ts를 만듭니다.

'use server';

import { db } from '@/db';
import { users } from '@/db/schema';
import { eq } from 'drizzle-orm';
import { revalidatePath } from 'next/cache';

// 사용자 생성
export async function createUser(formData: FormData) {
  const name = formData.get('name') as string;
  const email = formData.get('email') as string;

  try {
    await db.insert(users).values({ name, email });
    revalidatePath('/users');
    return { success: true };
  } catch (error) {
    return { success: false, error: '사용자 생성에 실패했습니다.' };
  }
}

// 모든 사용자 조회
export async function getUsers() {
  return await db.select().from(users);
}

// 사용자 삭제
export async function deleteUser(id: number) {
  try {
    await db.delete(users).where(eq(users.id, id));
    revalidatePath('/users');
    return { success: true };
  } catch (error) {
    return { success: false, error: '사용자 삭제에 실패했습니다.' };
  }
}

페이지에서 사용합니다.

// app/users/page.tsx
import { getUsers } from '../actions';

export default async function UsersPage() {
  const users = await getUsers();

  return (
    <div>
      <h1>사용자 목록</h1>
      <ul>
        {users.map(user => (
          <li key={user.id}>
            {user.name} - {user.email}
          </li>
        ))}
      </ul>
    </div>
  );
}

TypeScript 타입 안전성

Drizzle의 가장 만족스러운 부분은 완전한 타입 추론입니다.

// 반환 타입 자동 추론
const users = await db.select().from(users);
// 타입: { id: number; name: string; email: string; createdAt: Date }[]

// 반환 필드를 직접 지정해도 타입 자동 추론
const result = await db
  .select({
    id: users.id,
    name: users.name,
  })
  .from(users);
// 타입: { id: number; name: string }[]

// 오류는 컴파일 시점에 잡힙니다.
await db.select().from(users).where(eq(users.id, '1'));
// ❌ TypeScript 오류: 'string' 타입은 'number' 타입에 할당할 수 없습니다.

IntelliSense가 사용 가능한 필드, 함수, 조건 연산자를 모두 알려 줍니다. 점 하나를 입력하면 필요한 항목이 모두 나타납니다.

API를 외우거나 문서를 계속 찾을 필요가 없습니다. TypeScript 컴파일러 자체가 가장 좋은 문서입니다.

Drizzle과 Prisma 심층 비교

Drizzle의 장점을 충분히 살펴봤으니 이제 객관적으로 전반을 비교해 보겠습니다. 자신의 프로젝트에 어느 쪽이 맞는지 판단하는 데 필요한 내용입니다.

성능 비교

~7.4kb
Drizzle 번들
Prisma 14MB와 비교
~600ms
콜드 스타트 시간
Prisma 2.5초와 비교
0
런타임 의존성
순수 TypeScript
Source: 프로덕션 환경 모니터링 데이터
비교 항목DrizzlePrisma v5Prisma v7비고
번들 크기~7.4kb~14MB~1MBDrizzle이 가장 가벼움
콜드 스타트 시간~600ms~2.5초~1.5초Serverless 환경
런타임 의존성0개Rust 바이너리0개Drizzle과 Prisma v7 모두 의존성 없음
메모리 사용량~5MB~80MB~30MB(추정)실제 런타임 메모리
타입 검사 속도빠름보통보통Drizzle의 추론이 더 단순함

실제 사례 데이터

우리 팀의 한 프로젝트를 Prisma v5에서 Drizzle로 마이그레이션한 결과입니다.

  • 첫 요청 시간: 3초 → 700ms(76% 단축)
  • 프로덕션 번들 크기: 18MB → 4MB(78% 감소)
  • Lambda 콜드 스타트: 2.4초 → 650ms(73% 단축)

이 수치는 벤치마크 환경에서 만든 것이 아니라 실제 프로덕션 환경의 모니터링 데이터입니다.

개발 경험 비교

스키마 정의 방식

// Drizzle(TypeScript 코드)
export const users = pgTable('users', {
  id: serial('id').primaryKey(),
  name: text('name').notNull(),
});

// Prisma(전용 DSL)
model User {
  id   Int    @id @default(autoincrement())
  name String
}

Drizzle의 장점: TypeScript 자체이므로 IDE 지원이 뛰어나고 조건부 타입, 제네릭 등 TypeScript의 모든 기능을 사용할 수 있습니다.

Prisma의 장점: Prisma Schema가 더 간결하고 읽기 쉽습니다.

쿼리 API 스타일

// Drizzle(SQL-like)
await db
  .select()
  .from(users)
  .leftJoin(posts, eq(users.id, posts.authorId))
  .where(gt(posts.views, 1000));

// Prisma(체이닝 API)
await prisma.user.findMany({
  include: {
    posts: {
      where: { views: { gt: 1000 } },
    },
  },
});

Drizzle의 장점: SQL에 더 가까워 복잡한 쿼리를 표현하기 쉽습니다.

Prisma의 장점: SQL에 익숙하지 않은 개발자에게 더 친절하고 중첩 쿼리의 의미가 명확합니다.

기능 비교

Drizzle의 장점

  1. 극도로 가벼움: 핵심 패키지가 7.4kb라 번들 크기에 민감한 프로젝트에 적합합니다.
  2. Serverless 네이티브 지원: 콜드 스타트가 빠르고 Edge 환경에 잘 맞습니다.
  3. 강력한 SQL 제어력: 복잡한 쿼리와 성능 최적화에 더 유연합니다.
  4. Client 생성 불필요: 스키마를 바꾼 뒤 prisma generate를 실행하지 않고 바로 사용할 수 있습니다.
  5. Tree-shakable: 실제 사용하는 코드만 번들에 포함합니다.

Prisma의 장점

  1. 성숙한 생태계: 2021년에 출시돼 커뮤니티가 크고 튜토리얼이 많으며 알려진 문제가 적습니다.
  2. 완성도 높은 개발 도구: Prisma Studio(시각적 데이터베이스 관리), Prisma Migrate(자동 마이그레이션), Prisma Pulse(실시간 구독)를 제공합니다.
  3. 지능적인 관계 쿼리: N+1 문제를 자동으로 처리하고 중첩 쿼리가 더 직관적입니다.
  4. 초보자 친화성: SQL을 깊이 이해하지 않아도 되며 DSL의 학습 곡선이 더 완만합니다.
  5. 자세한 오류 안내: 런타임 오류 메시지가 더 친절합니다.

선택 기준

Drizzle을 선택할 상황

✅ Serverless/Edge 환경(Vercel, Cloudflare Workers, Deno Deploy)
✅ 성능에 민감한 환경(실시간 앱, 금융 시스템, 고동시성 API)
✅ 번들 크기가 제한된 환경(1MB 미만 제한)
✅ 팀의 SQL 기반이 탄탄하고 쿼리를 직접 제어하고 싶은 경우
✅ 복잡한 SQL 최적화가 필요한 프로젝트

Prisma를 선택할 상황

✅ 팀이 SQL에 익숙하지 않아 빠르게 시작해야 하는 경우
✅ 개발 경험과 완성도 높은 도구 체계를 중시하는 경우
✅ 시각적 데이터베이스 관리(Prisma Studio)가 필요한 경우
✅ 복잡한 관계 쿼리와 데이터 모델링이 필요한 경우
✅ 프로젝트가 serverless 환경이 아니고 번들 크기에 민감하지 않은 경우

개인적인 권장 사항

새 프로젝트라면 다음과 같이 선택하겠습니다.

  • 개인 프로젝트/스타트업 프로젝트: Drizzle(성능이 좋고 비용이 낮으며 세밀하게 최적화할 수 있음)
  • 기업 프로젝트/팀 협업: 팀의 배경에 따라 선택합니다. SQL에 강하면 Drizzle, 익숙하지 않으면 Prisma가 적합합니다.
  • Serverless-First: 고민 없이 Drizzle을 선택합니다.
  • 기존 서버 배포: 둘 다 괜찮지만 Prisma의 도구 체계가 더 완성돼 있습니다.

혼합 방식도 있습니다. 성능이 중요한 핵심 모듈에는 Drizzle을 사용하고 관리 화면에는 Prisma를 사용합니다. 두 ORM은 충돌 없이 함께 쓸 수 있습니다.

마이그레이션 가이드와 모범 사례

Prisma에서 Drizzle로 마이그레이션하기

기존 Prisma 프로젝트에서 Drizzle을 시험하고 싶다면 점진적으로 마이그레이션할 수 있습니다.

1단계: 스키마 변환

// Prisma Schema
model User {
  id        Int      @id @default(autoincrement())
  name      String
  email     String   @unique
  posts     Post[]
  createdAt DateTime @default(now())
}

// Drizzle Schema로 변환
export const users = pgTable('users', {
  id: serial('id').primaryKey(),
  name: text('name').notNull(),
  email: text('email').notNull().unique(),
  createdAt: timestamp('created_at').defaultNow(),
});

2단계: 쿼리 재작성

// Prisma 쿼리
const user = await prisma.user.findUnique({
  where: { id: 1 },
  include: { posts: true },
});

// Drizzle 쿼리
const [user] = await db
  .select()
  .from(users)
  .where(eq(users.id, 1))
  .leftJoin(posts, eq(users.id, posts.authorId));

3단계: 점진적 교체

Prisma와 Drizzle을 함께 사용할 수 있습니다.

// 성능에 민감한 쿼리는 Drizzle을 사용합니다.
import { db } from '@/db/drizzle';
const latestPosts = await db
  .select()
  .from(posts)
  .orderBy(desc(posts.createdAt))
  .limit(50);

// 복잡한 관계 쿼리는 당분간 Prisma에 남겨 둡니다.
import { prisma } from '@/db/prisma';
const userWithRelations = await prisma.user.findUnique({
  where: { id: 1 },
  include: {
    posts: { include: { comments: { include: { author: true } } } },
  },
});

점진적으로 교체하면 위험을 줄일 수 있습니다.

Drizzle 모범 사례

1. 연결 풀 관리

Serverless 환경에서는 연결 풀을 특히 주의해야 합니다.

import { drizzle } from 'drizzle-orm/neon-http';
import { neon, neonConfig } from '@neondatabase/serverless';

// 연결 풀 설정
neonConfig.fetchConnectionCache = true;

const sql = neon(process.env.DATABASE_URL!);
export const db = drizzle(sql, { schema });

2. 준비된 쿼리

자주 실행하는 쿼리는 미리 준비해 성능을 높일 수 있습니다.

import { db } from '@/db';
import { users } from '@/db/schema';
import { eq } from 'drizzle-orm';

// 쿼리 준비
const getUserById = db
  .select()
  .from(users)
  .where(eq(users.id, placeholder('id')))
  .prepare('get_user_by_id');

// 준비된 쿼리 사용
const user = await getUserById.execute({ id: 1 });

3. 트랜잭션 처리

await db.transaction(async (tx) => {
  // 사용자 생성
  const [user] = await tx
    .insert(users)
    .values({ name: 'John', email: '[email protected]' })
    .returning();

  // 연결된 게시물 생성
  await tx.insert(posts).values({
    title: 'First Post',
    authorId: user.id,
  });

  // 어느 단계에서든 실패하면 전체 트랜잭션을 롤백합니다.
});

4. 타입 재사용

추론된 타입을 내보내 프런트엔드에서 사용합니다.

// db/schema.ts
export const users = pgTable('users', {
  id: serial('id').primaryKey(),
  name: text('name').notNull(),
  email: text('email').notNull(),
});

// 추론된 타입 내보내기
export type User = typeof users.$inferSelect;
export type NewUser = typeof users.$inferInsert;

// 프런트엔드에서 사용
import type { User } from '@/db/schema';

function UserCard({ user }: { user: User }) {
  return <div>{user.name}</div>;
}

5. 오류 처리

import { db } from '@/db';
import { users } from '@/db/schema';

try {
  await db.insert(users).values({
    name: 'John',
    email: '[email protected]',
  });
} catch (error) {
  // PostgreSQL 오류 코드
  if (error.code === '23505') {
    console.error('이미 존재하는 이메일입니다.');
  } else {
    console.error('데이터베이스 오류', error);
  }
}

결론

핵심을 정리해 보겠습니다.

Drizzle ORM은 TypeScript ORM 분야에서 보기 드문 접근을 택합니다. SQL을 추상화하지 않고 그대로 받아들입니다. 7.4kb의 핵심 패키지, 런타임 의존성 없음, 네이티브 SQL에 가까운 성능을 통해 가벼움은 타협이 아니라 더 나은 선택이 될 수 있음을 보여 줍니다.

Drizzle이 Prisma를 완전히 대체하는 것은 아닙니다. Prisma는 더 성숙한 생태계, 완성도 높은 도구 체계, 초보자에게 친절한 경험을 제공합니다. 하지만 프로젝트가 다음 조건에 해당한다면 Drizzle이 더 적합할 수 있습니다.

  • serverless 환경에서 실행됨
  • 성능과 번들 크기에 민감함
  • 복잡한 SQL을 직접 제어해야 함
  • 팀이 SQL 기반을 갖추고 있음

우리 팀의 실제 데이터는 분명합니다. Prisma에서 Drizzle로 마이그레이션한 뒤 콜드 스타트는 73% 빨라졌고, 번들은 78% 줄었으며, 첫 요청 시간은 3초에서 700ms로 단축됐습니다. 사소한 최적화가 아니라 질적인 도약입니다.

Prisma의 느린 콜드 스타트와 큰 번들 때문에 고민하고 있다면 Drizzle을 시험해 보세요. 학습 비용도 낮습니다. SQL을 안다면 10분이면 시작할 수 있습니다.

마지막으로 유용한 링크를 정리합니다.

직접 사용해 보면 이전 방식으로 돌아가기 어려울지도 모릅니다.

Next.js + Drizzle ORM 전체 설정 및 마이그레이션 가이드

환경 준비, 스키마 정의, 데이터베이스 연결, 쿼리 사용법, Prisma 마이그레이션까지 Drizzle ORM을 처음부터 설정하는 전체 과정

Estimated time: PT2H

  1. 1

    Step 1: 1단계: 환경 준비와 의존성 설치

    Next.js 프로젝트를 만듭니다.
  2. 2

    Step 2: 2단계: 데이터베이스 Schema 정의

    db/schema.ts를 만들고 테이블 구조를 정의합니다.
  3. 3

    Step 3: 3단계: 데이터베이스 연결과 Drizzle Kit 설정

    db/index.ts를 만듭니다.
  4. 4

    Step 4: 4단계: Drizzle SQL-like API로 쿼리 작성

    기본 CRUD 작업:
  5. 5

    Step 5: 5단계: Next.js Server Actions에서 사용

    app/actions.ts를 만듭니다.
  6. 6

    Step 6: {user.name}

    {user.email}
  7. 7

    Step 7: 6단계: Prisma에서 Drizzle로 마이그레이션(선택 사항)

    기존 Prisma 프로젝트가 있다면 점진적으로 마이그레이션할 수 있습니다.

FAQ

Drizzle ORM과 Prisma는 무엇이 다르며, 언제 Drizzle을 선택해야 하나요?
핵심 차이:

성능:
• Drizzle: 7.4kb, 콜드 스타트 600ms, 런타임 의존성 없음
• Prisma v5: 14MB, 콜드 스타트 2.5초, Rust 바이너리 필요
• Prisma v7: 1MB, 콜드 스타트 1.5초, 개선됐지만 여전히 Drizzle보다 느림

개발 경험:
• Drizzle: SQL-like API, SQL을 알면 바로 사용할 수 있고 Client 생성 불필요
• Prisma: 전용 DSL을 익혀야 하며 prisma generate 실행 필요

Drizzle을 선택할 상황:
✅ Serverless/Edge 환경(Vercel, Cloudflare Workers, Deno Deploy)
✅ 성능에 민감한 환경(실시간 앱, 금융 시스템, 고동시성 API)
✅ 번들 크기가 제한된 환경(1MB 미만 제한)
✅ 팀의 SQL 기반이 탄탄하고 쿼리를 직접 제어하고 싶은 경우
✅ 복잡한 SQL 최적화가 필요한 프로젝트

Prisma를 선택할 상황:
✅ 팀이 SQL에 익숙하지 않아 빠르게 시작해야 하는 경우
✅ 개발 경험과 완성도 높은 도구 체계를 중시하는 경우
✅ 시각적 데이터베이스 관리(Prisma Studio)가 필요한 경우
✅ 복잡한 관계 쿼리와 데이터 모델링이 필요한 경우
✅ 프로젝트가 serverless 환경이 아니고 번들 크기에 민감하지 않은 경우
Drizzle ORM의 SQL-like API란 구체적으로 무엇인가요?
Drizzle의 API는 TypeScript 코드가 최대한 SQL 문법에 가깝도록 설계됐습니다.

비교 예시:

SQL:
```sql
SELECT * FROM posts
LEFT JOIN comments ON posts.id = comments.post_id
WHERE posts.id = 10
```

Drizzle:
```typescript
await db
.select()
.from(posts)
.leftJoin(comments, eq(posts.id, comments.postId))
.where(eq(posts.id, 10))
```

핵심 특징:
• 코드 구조가 SQL과 거의 같습니다.
• SQL을 아는 사람은 보기만 해도 사용법을 알 수 있습니다.
• SQL을 안다면 학습 비용이 거의 없습니다.
• 완전한 타입 추론과 IntelliSense 자동 완성을 제공합니다.
• 컴파일 시점에 오류를 발견할 수 있습니다.

Prisma DSL과 비교:
```typescript
// Prisma에서는 새로운 쿼리 문법을 익혀야 합니다.
await prisma.user.findMany({
include: {
posts: {
where: { views: { gt: 1000 } },
},
},
});
```

Drizzle은 네이티브 SQL에 더 가까워 복잡한 쿼리를 표현하기 쉽습니다.
Serverless 환경에서 Drizzle ORM의 성능 우위는 어느 정도인가요?
실제 사례 데이터(Prisma v5에서 Drizzle로 마이그레이션):

성능 향상:
• 첫 요청 시간: 3초 → 700ms(76% 단축)
• 프로덕션 번들 크기: 18MB → 4MB(78% 감소)
• Lambda 콜드 스타트: 2.4초 → 650ms(73% 단축)

원인 분석:

1. 번들 크기:
• Drizzle 핵심 패키지는 7.4kb(min+gzip)에 불과합니다.
• Prisma v5에서 생성된 Client는 14MB에 이를 수 있습니다.
• Cloudflare Workers처럼 1MB 제한이 있는 환경에서는 Prisma가 제한을 바로 초과합니다.

2. 콜드 스타트 시간:
• Prisma는 시작할 때 거대한 DMMF(Data Model Meta Format) 문자열을 파싱해야 합니다.
• 중간 규모 스키마에서도 이 문자열은 600만 자를 넘을 수 있습니다.
• 콜드 스타트마다 이를 파싱해야 하므로 시작이 느립니다.
• Drizzle은 런타임 추상화 계층 없이 쿼리를 SQL로 직접 변환하므로 숨은 비용이 없습니다.

3. 메모리 사용량:
• Drizzle: 약 5MB
• Prisma v5: 약 80MB
• Prisma v7: 약 30MB(추정)

이 수치는 벤치마크 환경에서 만든 것이 아니라 실제 프로덕션 환경의 모니터링 데이터입니다.
Prisma에서 Drizzle로 어떻게 마이그레이션하나요? 과정이 복잡한가요?
한 번에 모두 교체하지 않고 점진적으로 마이그레이션할 수 있습니다.

1단계: 스키마 변환

Prisma Schema → Drizzle Schema:
• Prisma의 model 정의를 pgTable 정의로 변환합니다.
• 필드 타입 대응: Int → serial/integer, String → text, DateTime → timestamp
• 관계 정의를 Prisma의 관계 문법에서 Drizzle의 relations 함수로 변환합니다.

2단계: 쿼리 재작성

Prisma 쿼리 → Drizzle 쿼리:
• findUnique/findMany → select().from().where()
• include → leftJoin/innerJoin
• create → insert().values()
• update → update().set().where()

3단계: 점진적 교체

Prisma와 Drizzle을 함께 사용할 수 있습니다.
```typescript
// 성능에 민감한 쿼리는 Drizzle을 사용합니다.
import { db } from '@/db/drizzle';
const latestPosts = await db
.select()
.from(posts)
.orderBy(desc(posts.createdAt))
.limit(50);

// 복잡한 관계 쿼리는 당분간 Prisma에 남겨 둡니다.
import { prisma } from '@/db/prisma';
const userWithRelations = await prisma.user.findUnique({
where: { id: 1 },
include: {
posts: { include: { comments: { include: { author: true } } } },
},
});
```

점진적으로 교체하면 위험을 줄일 수 있습니다. 마이그레이션은 주로 문법을 변환하는 작업이라 복잡하지 않습니다.
Drizzle ORM은 어떤 데이터베이스와 실행 환경을 지원하나요?
지원 데이터베이스:

• PostgreSQL(Neon Serverless 권장)
• MySQL
• SQLite(Turso 및 SQLite on the edge 포함)
• SQL Server

지원 serverless 환경:

• Vercel Edge Functions
• Cloudflare Workers
• AWS Lambda
• Deno Deploy
• Bun

네이티브로 지원하는 serverless 데이터베이스 드라이버:

• Neon Serverless(PostgreSQL)
• PlanetScale(MySQL)
• Turso(SQLite on the edge)
• Supabase(PostgreSQL)

우리 프로젝트는 Neon + Drizzle을 Vercel Edge에 배포해 콜드 스타트를 2.5초에서 700ms로 단축했습니다.

핵심 장점:
• 주요 serverless 환경을 모두 지원합니다.
• serverless 데이터베이스 드라이버를 네이티브로 지원합니다.
• 런타임 의존성이 없어 모든 JavaScript 환경에서 실행할 수 있습니다.
Drizzle ORM의 타입 안전성은 어떠하며 Prisma와 비교하면 어떤가요?
Drizzle은 완전한 타입 안전성을 제공하면서 Prisma보다 단순합니다.

타입 추론:

Drizzle(자동 추론):
```typescript
// 반환 타입 자동 추론
const users = await db.select().from(users);
// 타입: { id: number; name: string; email: string; createdAt: Date }[]

// 반환 필드를 직접 지정해도 타입 자동 추론
const result = await db
.select({
id: users.id,
name: users.name,
})
.from(users);
// 타입: { id: number; name: string }[]

// 오류는 컴파일 시점에 잡힙니다.
await db.select().from(users).where(eq(users.id, '1'));
// ❌ TypeScript 오류: 'string' 타입은 'number' 타입에 할당할 수 없습니다.
```

핵심 특징:
• Client를 생성하지 않아도 TypeScript가 타입을 직접 추론합니다.
• IntelliSense가 사용 가능한 필드, 함수, 조건 연산자를 모두 자동 완성합니다.
• 런타임까지 기다리지 않고 컴파일 시점에 문제를 발견합니다.
• Drizzle의 추론 구조가 단순해 타입 검사 속도가 빠릅니다.

Prisma와 비교:
• Prisma는 prisma generate를 실행해 Client를 생성해야 합니다.
• 대규모 프로젝트에서는 Prisma의 타입 추론이 TypeScript 컴파일을 느리게 할 수 있습니다.
• Drizzle은 타입 추론이 더 단순해 컴파일이 빠릅니다.

프런트엔드에서 사용할 타입 내보내기:
```typescript
// 추론된 타입 내보내기
export type User = typeof users.$inferSelect;
export type NewUser = typeof users.$inferInsert;
```
Drizzle ORM은 초보자에게 적합한가요? 학습 곡선은 어떤가요?
SQL을 안다면 Drizzle의 학습 곡선은 매우 완만합니다.

시작하는 데 걸리는 시간:
• SQL을 아는 사람: 10분이면 시작할 수 있습니다.
• SQL을 모르는 사람: 먼저 SQL 기초를 배워야 합니다.

학습 비용이 낮은 이유:
• SQL-like API: 코드 구조가 SQL과 거의 같습니다.
• Prisma처럼 새로운 DSL을 익힐 필요가 없습니다.
• 완전한 타입 힌트와 IntelliSense 자동 완성을 제공합니다.
• TypeScript 컴파일러 자체가 가장 좋은 문서입니다.

예시:
```typescript
// SELECT 조회
db.select().from(users).where(eq(users.id, 1))

// INSERT 삽입
db.insert(users).values({ name: 'John', email: '[email protected]' })

// UPDATE 수정
db.update(users).set({ name: 'Jane' }).where(eq(users.id, 1))

// DELETE 삭제
db.delete(users).where(eq(users.id, 1))
```

SQL을 아는 사람은 이 코드를 보고 문서를 찾을 필요가 없습니다.

Prisma와 비교:
• Prisma는 전용 DSL 문법을 익혀야 합니다.
• SQL에 익숙하지 않은 개발자에게는 더 친절합니다.
• 하지만 SQL을 안다면 Drizzle이 더 직관적입니다.

권장 사항:
• 팀의 SQL 기반이 탄탄하다면 Drizzle을 바로 선택합니다.
• 팀이 SQL에 익숙하지 않다면 Prisma를 선택하거나, SQL 기초를 익힌 뒤 Drizzle을 선택할 수 있습니다.

8분 읽기 · 게시일: 2025년 12월 20일 · 수정일: 2026년 9월 4일

댓글

GitHub로 로그인하여 댓글을 남기세요

Easton BlogEaston Blog