428 lines
10 KiB
Markdown
428 lines
10 KiB
Markdown
# 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이 함께 작동하는 것을 확인할 수 있습니다!
|
||
|