데이터베이스
Neon (서버리스 PostgreSQL) + Drizzle ORM 을 사용합니다.
DB 관련 코드는 packages/db에 집중되어 있으며, apps에서 이 패키지를 import해 사용합니다.
Git 브랜치 = Neon 브랜치
Neon은 Git처럼 DB를 브랜치로 관리합니다. 이 프로젝트에서는 Git 브랜치와 Neon 브랜치를 1:1로 매핑해 사용합니다.
main ← production DB
└─ develop ← staging DB
└─ feature/my-work ← 개발 중 개인 DB (develop의 데이터를 복사)브랜치를 만들면 parent 브랜치의 스키마와 데이터를 그대로 복사해서 시작합니다. 개발 중 스키마를 마음껏 수정해도 develop/main에 영향을 주지 않습니다.
개발 플로우
1. Git 브랜치 생성 후 Neon 브랜치 자동 생성
Git 브랜치를 만든 뒤 루트에서 다음 명령을 실행합니다.
yarn db:create-branch이 명령 하나로:
- 현재 Git 브랜치 이름과 동일한 Neon 브랜치 생성
- parent 브랜치 자동 감지 (
epic/**형태의 브랜치가 있으면 그걸, 없으면develop) - compute endpoint 자동 생성
.env.shared의DATABASE_URL을 새 브랜치의 URL로 자동 업데이트
이미 해당 이름의 Neon 브랜치가 존재하면 아무것도 하지 않고 종료됩니다.
2. 스키마 수정
packages/db/schema/에서 Drizzle 스키마를 수정합니다.
// packages/db/schema/myTable.ts
import {pgTable, text, timestamp} from 'drizzle-orm/pg-core';
export const myTable = pgTable('my_table', {
id: text('id').primaryKey(),
name: text('name').notNull(),
createdAt: timestamp('created_at').notNull().defaultNow(),
});3. 개발 중 스키마 반영 (push)
마이그레이션 파일 없이 현재 DB 브랜치에 스키마 변경을 즉시 반영합니다. 개발 초기에 스키마를 빠르게 탐색할 때 유용합니다.
yarn db:push
push는 개발 브랜치에서만 사용합니다. 마이그레이션 히스토리가 남지 않으므로 PR 전에는 반드시db:generate로 마이그레이션 파일을 생성해야 합니다.
4. 마이그레이션 파일 생성
PR 전에 반드시 마이그레이션 파일을 생성합니다.
yarn db:generate내부적으로 현재 Neon 브랜치의 parent 브랜치 DB와 현재 스키마를 비교해 SQL을 생성합니다. (parent 기준으로 diff를 뽑기 때문에 develop → feature에서 변경한 내용만 정확히 담깁니다.)
생성된 파일은 packages/db/drizzle/migrations/에 저장됩니다. SQL 내용을 검토하고, rename 등 파괴적인 변경이 있으면 수동으로 수정합니다.
5. PR 생성
스키마가 변경됐는데 마이그레이션 파일이 없으면 CI가 실패합니다.
마이그레이션 파일이 포함된 PR에는 자동으로 db-migration 라벨이 추가됩니다.
6. 배포 시 자동 마이그레이션
Vercel에서 빌드될 때 ENABLE_MIGRATION=true 환경변수가 설정되어 있으면 빌드 전에 자동으로 마이그레이션이 실행됩니다.
// apps/app/package.json
{
"prebuild": "yarn syncpack && yarn db:migrate:conditional"
}해당 환경변수는 자동으로 설정되므로 개발자가 직접 마이그레이션을 실행할 필요가 없습니다.
DB 사용
일반 쿼리
packages/db에서 db를 import합니다.
import {db} from '@howmuchhome-web/db';
import {myTable} from '@howmuchhome-web/db/schema';
import {eq} from 'drizzle-orm';
const result = await db.select().from(myTable).where(eq(myTable.id, 'some-id'));트랜잭션
트랜잭션이 필요한 경우 transactionDb를 사용합니다.
import {transactionDb} from '@howmuchhome-web/db';
await transactionDb.transaction(async tx => {
await tx.update(myTable).set({isLatest: false}).where(...);
await tx.insert(myTable).values({...});
});
db는 Neon HTTP 모드로 동작하며 트랜잭션을 지원하지 않습니다. 트랜잭션이 필요한 경우 반드시transactionDb를 사용합니다.
Raw SQL 지양
가능한 한 drizzle-orm이 제공하는 쿼리 빌더와 헬퍼를 사용하고, sql 템플릿 리터럴(raw SQL)은 지양합니다.
raw SQL은 타입 안전성이 떨어지고, Drizzle이 제공하는 방언(dialect) 간 호환성·파라미터 바인딩 보호를 우회하게 되어 오류와 보안 이슈를 일으키기 쉽습니다.
// ❌ 지양 — raw SQL로 집계
import {sql} from 'drizzle-orm';
const [row] = await db
.select({count: sql<number>`count(*)::int`})
.from(myTable);// ✅ 권장 — drizzle-orm 헬퍼 사용
import {count} from 'drizzle-orm';
const [row] = await db.select({count: count()}).from(myTable);자주 쓰는 헬퍼: count, sum, avg, min, max, eq, and, or, inArray, isNull, isNotNull, desc, asc 등.
Drizzle 쿼리 빌더로 표현하기 어려운 특수한 경우(예: Postgres 고유 함수, 복잡한 윈도우 함수 등)에 한해 sql 사용을 허용합니다. 이 경우에도 값은 반드시 아래와 같이 파라미터로 바인딩하고, PR 설명 및 주석에 사유를 남겨 주세요.
import {sql} from 'drizzle-orm';
await db.execute(sql`SELECT * FROM my_table WHERE id = ${id}`);스키마 작성
스키마는 packages/db/schema/에 파일 단위로 작성합니다.
import {
boolean,
pgEnum,
pgTable,
text,
timestamp,
uniqueIndex,
} from 'drizzle-orm/pg-core';
export const platformEnum = pgEnum('app_platform', ['IOS', 'ANDROID']);
export const appVersionTable = pgTable(
'app-version',
{
id: text('id').primaryKey(),
platform: platformEnum('platform').notNull(),
version: text('version').notNull(),
isLatest: boolean('is_latest').notNull().default(false),
createdAt: timestamp('created_at').notNull().defaultNow(),
},
table => [
// isLatest = true인 레코드는 플랫폼당 하나만 허용
uniqueIndex('unique_latest_per_platform')
.on(table.platform)
.where(eq(table.isLatest, true)),
],
);새 스키마 파일을 만든 뒤 packages/db/schema/index.ts에 export를 추가합니다.
서비스 레이어
DB 쿼리 로직은 packages/db/services/에 작성합니다.
Next.js 서버 컴포넌트나 서버 액션에서 직접 import해 사용합니다.
// packages/db/services/appVersion.ts
'use server';
import {db} from '../db';
import {appVersionTable} from '../schema';
export async function getLatestAppVersion() {
return db
.select()
.from(appVersionTable)
.where(eq(appVersionTable.isLatest, true));
}명령어 레퍼런스
루트에서 실행합니다.
| 명령어 | 설명 |
|---|---|
yarn db:create-branch | 현재 Git 브랜치에 대응하는 Neon 브랜치 생성 및 .env.shared 업데이트 |
yarn db:push | 마이그레이션 없이 현재 브랜치 DB에 스키마 즉시 반영 (개발 중 탐색용) |
yarn db:generate | parent 브랜치 기준으로 마이그레이션 SQL 파일 생성 |
yarn db:migrate | 현재 DB에 미실행 마이그레이션 적용 |
yarn db:studio | Drizzle Studio 실행 (DB 브라우저 GUI) |
환경변수
.env.shared에서 관리합니다.
| 변수 | 설명 |
|---|---|
DATABASE_URL | 현재 브랜치의 DB 연결 URL (db:create-branch가 자동 업데이트) |
NEON_API_KEY | Neon API 인증 키 (브랜치 생성·조회에 사용) |
NEON_PROJECT_ID | Neon 프로젝트 ID |
ENABLE_MIGRATION 환경변수는 Vercel 프로젝트 설정에서 관리합니다.