Skip to Content
데이터베이스

데이터베이스

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.sharedDATABASE_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:generateparent 브랜치 기준으로 마이그레이션 SQL 파일 생성
yarn db:migrate현재 DB에 미실행 마이그레이션 적용
yarn db:studioDrizzle Studio 실행 (DB 브라우저 GUI)

환경변수

.env.shared에서 관리합니다.

변수설명
DATABASE_URL현재 브랜치의 DB 연결 URL (db:create-branch가 자동 업데이트)
NEON_API_KEYNeon API 인증 키 (브랜치 생성·조회에 사용)
NEON_PROJECT_IDNeon 프로젝트 ID

ENABLE_MIGRATION 환경변수는 Vercel 프로젝트 설정에서 관리합니다.

Last updated on