dd/DRIZZLE_GUIDE.md

10 KiB
Raw Permalink Blame History

Drizzle ORM 사용 가이드

Drizzle ORM으로 마이그레이션 완료

기존 순수 SQL 쿼리 방식에서 Drizzle ORM으로 완전히 전환되었습니다.

🎯 Drizzle ORM의 장점

  • 타입 안전성: TypeScript와 완벽한 통합
  • 자동 완성: IDE에서 테이블과 컬럼 자동 완성
  • 마이그레이션 관리: 스키마 변경을 자동으로 SQL로 생성
  • 쿼리 빌더: SQL 인젝션 방지 및 가독성 향상
  • Cloudflare D1 최적화: D1과 완벽하게 호환

📦 설치된 패키지

{
  "dependencies": {
    "drizzle-orm": "^0.44.7"
  },
  "devDependencies": {
    "drizzle-kit": "^0.31.7"
  }
}

📁 파일 구조

프로젝트/
├── drizzle.config.ts              # Drizzle 설정
├── drizzle/                       # 마이그레이션 파일
│   └── 0000_*.sql                # 생성된 SQL 마이그레이션
└── src/
    └── lib/
        └── server/
            ├── schema.ts          # Drizzle 스키마 정의
            └── db.ts              # 데이터베이스 함수 (Drizzle 사용)

🔧 스키마 정의 (schema.ts)

import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';
import { sql } from 'drizzle-orm';

export const users = sqliteTable('users', {
	id: integer('id').primaryKey({ autoIncrement: true }),
	email: text('email').notNull().unique(),
	passwordHash: text('password_hash').notNull(),
	nickname: text('nickname').notNull(),
	createdAt: integer('created_at', { mode: 'timestamp' })
		.notNull()
		.default(sql`(strftime('%s', 'now'))`)
});

export type User = typeof users.$inferSelect;
export type NewUser = typeof users.$inferInsert;

타입 추론

  • User: SELECT 쿼리 결과 타입 (모든 필드 포함)
  • NewUser: INSERT 시 사용하는 타입 (자동 생성 필드 제외)

💾 데이터베이스 함수 (db.ts)

기존 (순수 SQL)

// ❌ 타입 안전성 없음, 오타 발생 가능
const user = await db
  .prepare('SELECT id, email, password_hash, nickname FROM users WHERE email = ?')
  .bind(email)
  .first<User>();

Drizzle 사용 (현재)

// ✅ 완전한 타입 안전성
import { eq } from 'drizzle-orm';

const [user] = await drizzleDb
  .select()
  .from(users)
  .where(eq(users.email, email))
  .limit(1);

전체 함수 예시

import { drizzle } from 'drizzle-orm/d1';
import { eq } from 'drizzle-orm';
import { users, type User, type NewUser } from './schema';

export function getDb(d1: D1Database) {
	return drizzle(d1);
}

// CREATE
export async function createUser(
	db: D1Database,
	email: string,
	passwordHash: string,
	nickname: string
): Promise<User | null> {
	const drizzleDb = getDb(db);
	
	await drizzleDb.insert(users).values({
		email,
		passwordHash,
		nickname
	});
	
	const [user] = await drizzleDb
		.select()
		.from(users)
		.where(eq(users.email, email))
		.limit(1);
	
	return user || null;
}

// READ
export async function getUserByEmail(db: D1Database, email: string) {
	const drizzleDb = getDb(db);
	
	const [user] = await drizzleDb
		.select()
		.from(users)
		.where(eq(users.email, email))
		.limit(1);
	
	return user || null;
}

// UPDATE
export async function updateUserNickname(
	db: D1Database,
	userId: number,
	nickname: string
) {
	const drizzleDb = getDb(db);
	
	await drizzleDb
		.update(users)
		.set({ nickname })
		.where(eq(users.id, userId));
}

// DELETE
export async function deleteUser(db: D1Database, userId: number) {
	const drizzleDb = getDb(db);
	
	await drizzleDb
		.delete(users)
		.where(eq(users.id, userId));
}

📜 Package.json 스크립트

{
  "scripts": {
    "db:generate": "drizzle-kit generate",
    "db:push": "wrangler d1 execute auth-db --local --file=./drizzle/0000_*.sql",
    "db:push:remote": "wrangler d1 execute auth-db --remote --file=./drizzle/0000_*.sql",
    "db:studio": "drizzle-kit studio",
    "db:query": "wrangler d1 execute auth-db --local --command=\"SELECT * FROM users\"",
    "db:query:remote": "wrangler d1 execute auth-db --remote --command=\"SELECT * FROM users\""
  }
}

명령어 설명

pnpm db:generate

스키마 파일(schema.ts)에서 SQL 마이그레이션 파일을 자동 생성합니다.

pnpm db:generate
# 출력: drizzle/0000_omniscient_lady_mastermind.sql 생성

pnpm db:push

생성된 마이그레이션을 로컬 D1 데이터베이스에 적용합니다.

pnpm db:push
# 로컬 SQLite DB에 테이블 생성

pnpm db:push:remote

프로덕션 D1 데이터베이스에 마이그레이션을 적용합니다.

pnpm db:push:remote
# Cloudflare의 원격 D1에 테이블 생성

pnpm db:studio

Drizzle Studio를 실행하여 브라우저에서 데이터베이스를 시각적으로 관리합니다.

pnpm db:studio
# https://local.drizzle.studio 에서 DB 관리

pnpm db:query

로컬 데이터베이스에서 쿼리를 실행합니다.

pnpm db:query
# users 테이블의 모든 데이터 조회

🚀 개발 워크플로우

1. 스키마 변경

새로운 테이블이나 컬럼을 추가할 때:

// src/lib/server/schema.ts
export const users = sqliteTable('users', {
	id: integer('id').primaryKey({ autoIncrement: true }),
	email: text('email').notNull().unique(),
	passwordHash: text('password_hash').notNull(),
	nickname: text('nickname').notNull(),
	createdAt: integer('created_at', { mode: 'timestamp' })
		.notNull()
		.default(sql`(strftime('%s', 'now'))`),
	// 새로운 컬럼 추가
	avatar: text('avatar')
});

2. 마이그레이션 생성

pnpm db:generate

이 명령어는 스키마 변경사항을 분석하여 SQL 마이그레이션 파일을 생성합니다:

-- drizzle/0001_new_migration.sql
ALTER TABLE users ADD COLUMN avatar text;

3. 로컬 DB에 적용

# 파일명을 실제 생성된 파일명으로 변경
pnpm wrangler d1 execute auth-db --local --file=./drizzle/0001_new_migration.sql

4. 코드에서 사용

// 타입이 자동으로 업데이트됨
const [user] = await drizzleDb
	.select()
	.from(users)
	.where(eq(users.email, email))
	.limit(1);

// user.avatar는 타입 체크됨!
console.log(user.avatar);

🔍 Drizzle Studio 사용법

Drizzle Studio는 웹 기반 데이터베이스 관리 도구입니다.

pnpm db:studio

실행 후 브라우저가 자동으로 열리며 다음 기능을 사용할 수 있습니다:

  • 📊 테이블 데이터 조회 및 편집
  • 새 레코드 추가
  • 🗑️ 레코드 삭제
  • 🔍 검색 및 필터링
  • 📈 관계 시각화

🎯 고급 쿼리 예제

여러 조건으로 검색

import { and, eq, like } from 'drizzle-orm';

const users = await drizzleDb
	.select()
	.from(users)
	.where(
		and(
			like(users.nickname, '%테스트%'),
			eq(users.email, 'test@example.com')
		)
	);

정렬 및 페이지네이션

import { desc } from 'drizzle-orm';

const users = await drizzleDb
	.select()
	.from(users)
	.orderBy(desc(users.createdAt))
	.limit(10)
	.offset(0);

JOIN 쿼리 (추후 테이블 추가 시)

// 예: posts 테이블이 있다면
const postsWithUsers = await drizzleDb
	.select()
	.from(posts)
	.leftJoin(users, eq(posts.userId, users.id));

집계 함수

import { count } from 'drizzle-orm';

const [result] = await drizzleDb
	.select({ count: count() })
	.from(users);

console.log(`총 사용자 수: ${result.count}`);

🔐 타입 안전성의 이점

Before (순수 SQL)

// ❌ 오타 발생 가능
const user = await db.prepare('SELECT * FROM usres WHERE email = ?');
//                                              ^^^^^ 오타!

// ❌ 컬럼명 오타
const user = await db.prepare('SELECT emial FROM users');
//                                      ^^^^^ 오타!

// ❌ 런타임에서만 에러 발견

After (Drizzle)

// ✅ 컴파일 타임에 에러 발견
const user = await drizzleDb
	.select()
	.from(usres)  // ← IDE에서 빨간 줄, 컴파일 에러
	.where(eq(users.emial, email));  // ← 컴파일 에러

// ✅ 자동 완성 지원
const user = await drizzleDb
	.select()
	.from(users)
	.where(eq(users. /* ← IDE가 email, nickname 등 제안 */

📝 마이그레이션 히스토리 관리

Drizzle은 마이그레이션 파일을 순차적으로 관리합니다:

drizzle/
├── 0000_omniscient_lady_mastermind.sql  # 초기 스키마
├── 0001_add_avatar_column.sql           # 아바타 컬럼 추가
├── 0002_add_posts_table.sql             # 게시글 테이블 추가
└── meta/
    └── _journal.json                     # 마이그레이션 히스토리

각 마이그레이션은 독립적으로 적용 가능하며, Drizzle이 자동으로 순서를 관리합니다.

🆚 순수 SQL vs Drizzle 비교

기능 순수 SQL Drizzle ORM
타입 안전성 런타임에만 확인 컴파일 타임 확인
자동 완성 없음 전체 지원
SQL 인젝션 ⚠️ 수동 방어 필요 자동 방지
마이그레이션 📝 수동 작성 🤖 자동 생성
쿼리 작성 문자열 기반 타입 안전 빌더
리팩토링 어려움 쉬움
학습 곡선 낮음 중간
성능 빠름 빠름 (동일)

🎓 추가 학습 자료

다음 단계

Drizzle ORM을 활용하여 다음 기능들을 쉽게 추가할 수 있습니다:

  1. 게시글 테이블 - 사용자와 1:N 관계
  2. 댓글 시스템 - 게시글과 1:N 관계
  3. 좋아요 기능 - M:N 관계
  4. 팔로우 시스템 - 자기 참조 관계
  5. 프로필 설정 - 1:1 관계

모든 관계가 타입 안전하게 관리됩니다!

🔥 현재 프로젝트 상태

Drizzle ORM 완전 통합 완료 타입 안전한 쿼리 함수 구현 마이그레이션 시스템 구축 로컬/프로덕션 환경 분리 빌드 성공 확인

이제 pnpm cf:dev로 서버를 실행하고 JWT 인증과 Drizzle ORM이 함께 작동하는 것을 확인할 수 있습니다!