Drizzle Kit Migration 운영

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를 통합하면 스키마 변경의 안전성을 자동으로 보장할 수 있습니다.

위로 스크롤
WordPress Appliance - Powered by TurnKey Linux