dd/DRIZZLE_GUIDE.md

428 lines
10 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Drizzle ORM 사용 가이드
## ✅ Drizzle ORM으로 마이그레이션 완료
기존 순수 SQL 쿼리 방식에서 Drizzle ORM으로 완전히 전환되었습니다.
## 🎯 Drizzle ORM의 장점
- **타입 안전성**: TypeScript와 완벽한 통합
- **자동 완성**: IDE에서 테이블과 컬럼 자동 완성
- **마이그레이션 관리**: 스키마 변경을 자동으로 SQL로 생성
- **쿼리 빌더**: SQL 인젝션 방지 및 가독성 향상
- **Cloudflare D1 최적화**: D1과 완벽하게 호환
## 📦 설치된 패키지
```json
{
"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)
```typescript
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)
```typescript
// ❌ 타입 안전성 없음, 오타 발생 가능
const user = await db
.prepare('SELECT id, email, password_hash, nickname FROM users WHERE email = ?')
.bind(email)
.first<User>();
```
### Drizzle 사용 (현재)
```typescript
// ✅ 완전한 타입 안전성
import { eq } from 'drizzle-orm';
const [user] = await drizzleDb
.select()
.from(users)
.where(eq(users.email, email))
.limit(1);
```
### 전체 함수 예시
```typescript
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 스크립트
```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 마이그레이션 파일을 자동 생성합니다.
```bash
pnpm db:generate
# 출력: drizzle/0000_omniscient_lady_mastermind.sql 생성
```
#### `pnpm db:push`
생성된 마이그레이션을 로컬 D1 데이터베이스에 적용합니다.
```bash
pnpm db:push
# 로컬 SQLite DB에 테이블 생성
```
#### `pnpm db:push:remote`
프로덕션 D1 데이터베이스에 마이그레이션을 적용합니다.
```bash
pnpm db:push:remote
# Cloudflare의 원격 D1에 테이블 생성
```
#### `pnpm db:studio`
Drizzle Studio를 실행하여 브라우저에서 데이터베이스를 시각적으로 관리합니다.
```bash
pnpm db:studio
# https://local.drizzle.studio 에서 DB 관리
```
#### `pnpm db:query`
로컬 데이터베이스에서 쿼리를 실행합니다.
```bash
pnpm db:query
# users 테이블의 모든 데이터 조회
```
## 🚀 개발 워크플로우
### 1. 스키마 변경
새로운 테이블이나 컬럼을 추가할 때:
```typescript
// 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. 마이그레이션 생성
```bash
pnpm db:generate
```
이 명령어는 스키마 변경사항을 분석하여 SQL 마이그레이션 파일을 생성합니다:
```sql
-- drizzle/0001_new_migration.sql
ALTER TABLE users ADD COLUMN avatar text;
```
### 3. 로컬 DB에 적용
```bash
# 파일명을 실제 생성된 파일명으로 변경
pnpm wrangler d1 execute auth-db --local --file=./drizzle/0001_new_migration.sql
```
### 4. 코드에서 사용
```typescript
// 타입이 자동으로 업데이트됨
const [user] = await drizzleDb
.select()
.from(users)
.where(eq(users.email, email))
.limit(1);
// user.avatar는 타입 체크됨!
console.log(user.avatar);
```
## 🔍 Drizzle Studio 사용법
Drizzle Studio는 웹 기반 데이터베이스 관리 도구입니다.
```bash
pnpm db:studio
```
실행 후 브라우저가 자동으로 열리며 다음 기능을 사용할 수 있습니다:
- 📊 테이블 데이터 조회 및 편집
- 새 레코드 추가
- 🗑️ 레코드 삭제
- 🔍 검색 및 필터링
- 📈 관계 시각화
## 🎯 고급 쿼리 예제
### 여러 조건으로 검색
```typescript
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')
)
);
```
### 정렬 및 페이지네이션
```typescript
import { desc } from 'drizzle-orm';
const users = await drizzleDb
.select()
.from(users)
.orderBy(desc(users.createdAt))
.limit(10)
.offset(0);
```
### JOIN 쿼리 (추후 테이블 추가 시)
```typescript
// 예: posts 테이블이 있다면
const postsWithUsers = await drizzleDb
.select()
.from(posts)
.leftJoin(users, eq(posts.userId, users.id));
```
### 집계 함수
```typescript
import { count } from 'drizzle-orm';
const [result] = await drizzleDb
.select({ count: count() })
.from(users);
console.log(`총 사용자 수: ${result.count}`);
```
## 🔐 타입 안전성의 이점
### Before (순수 SQL)
```typescript
// ❌ 오타 발생 가능
const user = await db.prepare('SELECT * FROM usres WHERE email = ?');
// ^^^^^ 오타!
// ❌ 컬럼명 오타
const user = await db.prepare('SELECT emial FROM users');
// ^^^^^ 오타!
// ❌ 런타임에서만 에러 발견
```
### After (Drizzle)
```typescript
// ✅ 컴파일 타임에 에러 발견
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 공식 문서](https://orm.drizzle.team/)
- [Drizzle with Cloudflare D1](https://orm.drizzle.team/docs/get-started-sqlite#cloudflare-d1)
- [Drizzle Studio 가이드](https://orm.drizzle.team/drizzle-studio/overview)
## ✨ 다음 단계
Drizzle ORM을 활용하여 다음 기능들을 쉽게 추가할 수 있습니다:
1. **게시글 테이블** - 사용자와 1:N 관계
2. **댓글 시스템** - 게시글과 1:N 관계
3. **좋아요 기능** - M:N 관계
4. **팔로우 시스템** - 자기 참조 관계
5. **프로필 설정** - 1:1 관계
모든 관계가 타입 안전하게 관리됩니다!
## 🔥 현재 프로젝트 상태
✅ Drizzle ORM 완전 통합 완료
✅ 타입 안전한 쿼리 함수 구현
✅ 마이그레이션 시스템 구축
✅ 로컬/프로덕션 환경 분리
✅ 빌드 성공 확인
이제 `pnpm cf:dev`로 서버를 실행하고 JWT 인증과 Drizzle ORM이 함께 작동하는 것을 확인할 수 있습니다!