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, 서브쿼리, 집계 함수도 타입 안전하게 작성할 수 있다.
Drizzle ORM 실전 가이드 — TypeScript 네이티브 ORM — 데이터 흐름도 (출처: 공식 문서 및 벤치마크 데이터 기반)
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 어댑터를 써야 한다.