Drizzle ORM Migration이란?
Drizzle Kit은 Drizzle ORM의 마이그레이션 도구로, TypeScript 스키마 파일로부터 SQL 마이그레이션을 자동 생성합니다. Prisma와 달리 순수 SQL 마이그레이션 파일을 생성하므로 DBA 리뷰와 커스텀 수정이 자유롭습니다. 이 글에서는 drizzle-kit 설정부터 generate/migrate/push 워크플로, 시드 데이터, CI/CD 통합까지 실무 운영에 필요한 심화 내용을 다룹니다.
drizzle-kit 설정
drizzle.config.ts 파일로 마이그레이션 설정을 관리합니다:
// drizzle.config.ts
import { defineConfig } from 'drizzle-kit';
export default defineConfig({
schema: './src/db/schema/*.ts', // 스키마 파일 경로
out: './drizzle/migrations', // 마이그레이션 출력 디렉토리
dialect: 'postgresql', // pg | mysql | sqlite
dbCredentials: {
url: process.env.DATABASE_URL!,
},
verbose: true, // 상세 로그
strict: true, // 위험한 변경 시 확인 프롬프트
});
// package.json scripts
{
"scripts": {
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate",
"db:push": "drizzle-kit push",
"db:studio": "drizzle-kit studio",
"db:check": "drizzle-kit check",
"db:drop": "drizzle-kit drop"
}
}
스키마 정의와 마이그레이션 생성
먼저 TypeScript로 스키마를 정의합니다:
// src/db/schema/users.ts
import {
pgTable, serial, varchar, timestamp,
integer, boolean, index, uniqueIndex,
} from 'drizzle-orm/pg-core';
export const users = pgTable('users', {
id: serial('id').primaryKey(),
email: varchar('email', { length: 255 }).notNull(),
name: varchar('name', { length: 100 }).notNull(),
role: varchar('role', { length: 20 }).default('user').notNull(),
isActive: boolean('is_active').default(true).notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
}, (table) => [
uniqueIndex('users_email_idx').on(table.email),
index('users_role_idx').on(table.role),
]);
// src/db/schema/orders.ts
import { users } from './users';
export const orders = pgTable('orders', {
id: serial('id').primaryKey(),
userId: integer('user_id').notNull()
.references(() => users.id, { onDelete: 'cascade' }),
totalAmount: integer('total_amount').notNull(),
status: varchar('status', { length: 20 }).default('pending').notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
}, (table) => [
index('orders_user_id_idx').on(table.userId),
index('orders_status_idx').on(table.status),
]);
스키마를 정의하거나 변경한 후 마이그레이션을 생성합니다:
$ npx drizzle-kit generate
# 출력:
# [✓] 2 tables detected
# [✓] Created migration: 0001_initial_schema.sql
# drizzle/migrations/0001_initial_schema.sql
생성된 SQL 마이그레이션 파일
Drizzle Kit은 순수 SQL 파일을 생성합니다. 직접 수정할 수 있습니다:
-- drizzle/migrations/0001_initial_schema.sql
CREATE TABLE IF NOT EXISTS "users" (
"id" serial PRIMARY KEY NOT NULL,
"email" varchar(255) NOT NULL,
"name" varchar(100) NOT NULL,
"role" varchar(20) DEFAULT 'user' NOT NULL,
"is_active" boolean DEFAULT true NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL
);
CREATE TABLE IF NOT EXISTS "orders" (
"id" serial PRIMARY KEY NOT NULL,
"user_id" integer NOT NULL,
"total_amount" integer NOT NULL,
"status" varchar(20) DEFAULT 'pending' NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL
);
CREATE UNIQUE INDEX IF NOT EXISTS "users_email_idx" ON "users" ("email");
CREATE INDEX IF NOT EXISTS "users_role_idx" ON "users" ("role");
CREATE INDEX IF NOT EXISTS "orders_user_id_idx" ON "orders" ("user_id");
CREATE INDEX IF NOT EXISTS "orders_status_idx" ON "orders" ("status");
ALTER TABLE "orders" ADD CONSTRAINT "orders_user_id_users_id_fk"
FOREIGN KEY ("user_id") REFERENCES "users"("id")
ON DELETE cascade ON UPDATE no action;
팁: 생성된 SQL에 CREATE INDEX CONCURRENTLY나 데이터 마이그레이션 로직을 직접 추가할 수 있습니다. 이것이 Drizzle Kit의 가장 큰 장점입니다.
generate vs push vs migrate
| 명령어 | 동작 | 사용 시점 |
|---|---|---|
generate |
스키마 diff → SQL 파일 생성 | 스키마 변경 후 마이그레이션 생성 |
migrate |
생성된 SQL 파일을 DB에 적용 | 프로덕션 배포, CI/CD |
push |
스키마를 DB에 직접 반영 (파일 없음) | 로컬 개발, 프로토타이핑 |
check |
마이그레이션 파일과 스키마 일관성 검증 | CI에서 검증 단계 |
# 개발 환경: push로 빠르게 반영
npx drizzle-kit push
# 프로덕션: generate → review → migrate
npx drizzle-kit generate # SQL 파일 생성
git diff drizzle/migrations # 변경 내용 리뷰
npx drizzle-kit migrate # DB에 적용
프로그래매틱 마이그레이션 실행
NestJS 애플리케이션 시작 시 자동으로 마이그레이션을 실행하려면:
// src/db/migrate.ts
import { drizzle } from 'drizzle-orm/node-postgres';
import { migrate } from 'drizzle-orm/node-postgres/migrator';
import { Pool } from 'pg';
export async function runMigrations() {
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
});
const db = drizzle(pool);
console.log('Running migrations...');
await migrate(db, {
migrationsFolder: './drizzle/migrations',
});
console.log('Migrations complete.');
await pool.end();
}
// main.ts에서 호출
import { runMigrations } from './db/migrate';
async function bootstrap() {
await runMigrations(); // 앱 시작 전 마이그레이션
const app = await NestFactory.create(AppModule);
await app.listen(3000);
}
이 패턴은 NestJS + Drizzle ORM 통합에서 다룬 모듈 구조와 함께 사용하면 효과적입니다.
스키마 변경과 마이그레이션 전략
실무에서 자주 발생하는 스키마 변경 시나리오입니다:
1. 컬럼 추가 (안전)
// 스키마에 컬럼 추가
export const users = pgTable('users', {
// 기존 컬럼...
phoneNumber: varchar('phone_number', { length: 20 }), // nullable
});
// generate → 마이그레이션 생성
// ALTER TABLE "users" ADD COLUMN "phone_number" varchar(20);
2. 컬럼 NOT NULL 변경 (주의)
-- 자동 생성된 SQL 수정이 필요한 경우
-- 1단계: 기본값으로 기존 데이터 채우기
UPDATE "users" SET "phone_number" = 'unknown'
WHERE "phone_number" IS NULL;
-- 2단계: NOT NULL 제약 추가
ALTER TABLE "users" ALTER COLUMN "phone_number"
SET NOT NULL;
3. 컬럼 이름 변경 (위험)
-- Drizzle Kit은 컬럼 이름 변경을 DROP + ADD로 생성할 수 있음
-- strict: true 설정 시 확인 프롬프트가 표시됨
-- 수동으로 ALTER COLUMN RENAME 사용:
ALTER TABLE "users" RENAME COLUMN "name" TO "full_name";
시드 데이터 관리
테스트·개발 환경용 시드 스크립트입니다:
// src/db/seed.ts
import { drizzle } from 'drizzle-orm/node-postgres';
import { Pool } from 'pg';
import { users, orders } from './schema';
import { faker } from '@faker-js/faker';
async function seed() {
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const db = drizzle(pool);
console.log('Seeding...');
// 유저 100명 생성
const insertedUsers = await db.insert(users).values(
Array.from({ length: 100 }, () => ({
email: faker.internet.email(),
name: faker.person.fullName(),
role: faker.helpers.arrayElement(['user', 'admin', 'manager']),
}))
).returning({ id: users.id });
// 유저당 주문 5건씩
for (const user of insertedUsers) {
await db.insert(orders).values(
Array.from({ length: 5 }, () => ({
userId: user.id,
totalAmount: faker.number.int({ min: 1000, max: 500000 }),
status: faker.helpers.arrayElement(
['pending', 'confirmed', 'shipped', 'delivered']
),
}))
);
}
console.log('Seed complete: 100 users, 500 orders');
await pool.end();
}
seed().catch(console.error);
// package.json
{
"scripts": {
"db:seed": "tsx src/db/seed.ts"
}
}
CI/CD 파이프라인 통합
GitHub Actions에서 마이그레이션을 자동 검증하고 적용하는 파이프라인입니다:
# .github/workflows/migration.yml
name: Database Migration
on:
push:
branches: [main]
paths: ['drizzle/**', 'src/db/schema/**']
jobs:
check:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_DB: test_db
POSTGRES_PASSWORD: test
ports: ['5432:5432']
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20 }
- run: npm ci
- run: npx drizzle-kit check # 스키마↔마이그레이션 일관성 검증
env:
DATABASE_URL: postgres://postgres:test@localhost:5432/test_db
- run: npx drizzle-kit migrate # 테스트 DB에 마이그레이션 적용
env:
DATABASE_URL: postgres://postgres:test@localhost:5432/test_db
deploy:
needs: check
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20 }
- run: npm ci
- run: npx drizzle-kit migrate
env:
DATABASE_URL: ${{ secrets.PROD_DATABASE_URL }}
Drizzle Studio: 시각적 DB 관리
# 로컬에서 Drizzle Studio 실행
npx drizzle-kit studio
# → https://local.drizzle.studio 에서 DB 조회/수정 가능
Drizzle Studio는 브라우저에서 테이블 데이터를 조회하고 편집할 수 있는 GUI 도구입니다. 마이그레이션 결과를 즉시 확인할 때 유용합니다.
멀티 스키마·멀티 DB 마이그레이션
여러 데이터베이스를 사용하는 경우 config 파일을 분리합니다:
// drizzle-main.config.ts
export default defineConfig({
schema: './src/db/schema/main/*.ts',
out: './drizzle/main',
dialect: 'postgresql',
dbCredentials: { url: process.env.MAIN_DB_URL! },
});
// drizzle-analytics.config.ts
export default defineConfig({
schema: './src/db/schema/analytics/*.ts',
out: './drizzle/analytics',
dialect: 'postgresql',
dbCredentials: { url: process.env.ANALYTICS_DB_URL! },
});
# config별 마이그레이션 실행
npx drizzle-kit generate --config=drizzle-main.config.ts
npx drizzle-kit generate --config=drizzle-analytics.config.ts
멀티 DB 패턴은 Drizzle ORM 동적 쿼리·필터 심화에서 다룬 동적 쿼리와 조합하면 분석용 DB에서 유연한 필터링을 구현할 수 있습니다.
운영 베스트 프랙티스
- strict: true 필수: 위험한 변경(DROP COLUMN 등)에 확인 프롬프트를 표시합니다
- 마이그레이션 파일 커밋: 생성된 SQL 파일은 반드시 Git에 커밋하세요
- 절대 수동 DDL 금지: 프로덕션 DB에 직접 ALTER TABLE 하지 마세요
- 롤백 전략: 각 마이그레이션에 대응하는 롤백 SQL을 별도로 관리하세요
- 대용량 테이블 주의: ALTER TABLE은 테이블 락이 발생할 수 있으므로 점검 시간에 실행하세요
- check 명령 활용: CI에서
drizzle-kit check로 스키마와 마이그레이션의 일관성을 검증하세요
마무리
Drizzle Kit은 TypeScript 스키마 → 순수 SQL 마이그레이션이라는 실용적인 접근 방식을 제공합니다. generate로 SQL을 자동 생성하되 직접 수정할 수 있는 유연성, push로 로컬 개발을 빠르게, migrate로 프로덕션을 안전하게 관리할 수 있습니다. CI/CD 파이프라인에 check와 migrate를 통합하면 스키마 변경의 안전성을 자동으로 보장할 수 있습니다.