SHIN STORYSHIN STORY
홈포스트C#TypeScriptNext.jsNode.js시리즈
</>SHIN STORY

sdf

탐색

  • 홈
  • 모든 포스트
  • 시리즈
  • 검색

카테고리

  • C#
  • TypeScript
  • Next.js
  • Node.js
  • 알고리즘
  • 개발 도구

© 2025 Shin Blog. All rights reserved.

GitHubRSS
목록으로
Next.js#TypeScript#Prisma#SQLite

3편. Prisma 스키마 설계와 마이그레이션

SHIN

2025년 3월 10일8분 읽기0
📚

Next.js App Router로 풀스택 블로그 만들기

9편
  1. 11편. 프로젝트 소개와 기술 스택 선정
  2. 22편. 프로젝트 초기 설정과 디렉토리 구조
  3. 3편. Prisma 스키마 설계와 마이그레이션현재
  4. 44편. NextAuth.js로 관리자 인증 구현하기
  5. 55편. 관리자 대시보드 — 포스트 CRUD 구현
  6. 66편. 블로그 프론트엔드와 마크다운 렌더링
  7. 77편. 검색·카테고리·태그·시리즈 기능 구현
  8. 98편. SEO 최적화 — 메타태그·sitemap·RSS
  9. 99편. Vercel 배포와 성능 최적화

3편. Prisma 스키마 설계와 마이그레이션

전체 스키마 설계

블로그에 필요한 모델과 관계를 먼저 도식화합니다.

CODE
User ──┐
       ├──< Post >──< PostTag >──< Tag
       │     │
       │     ├──< Comment (트리 구조)
       │     └──< SeriesPost >──< Series
       └──< Comment

prisma/schema.prisma

CODE
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "sqlite"
}

model User {
  id        String    @id @default(cuid())
  name      String?
  email     String    @unique
  password  String?
  role      String    @default("ADMIN")
  bio       String?
  github    String?
  twitter   String?
  image     String?
  createdAt DateTime  @default(now())
  updatedAt DateTime  @updatedAt

  posts    Post[]
  comments Comment[]
}

model Post {
  id          String    @id @default(cuid())
  title       String
  slug        String    @unique
  excerpt     String?
  content     String
  coverImage  String?
  status      String    @default("DRAFT")  // DRAFT | PUBLISHED | ARCHIVED
  featured    Boolean   @default(false)
  pinned      Boolean   @default(false)
  viewCount   Int       @default(0)
  likeCount   Int       @default(0)
  readingTime Int?
  publishedAt DateTime?
  createdAt   DateTime  @default(now())
  updatedAt   DateTime  @updatedAt

  authorId   String
  author     User      @relation(fields: [authorId], references: [id])
  categoryId String?
  category   Category? @relation(fields: [categoryId], references: [id])

  tags     PostTag[]
  comments Comment[]
  series   SeriesPost[]
}

다대다 관계 — 중간 테이블

Post ↔ Tag는 다대다 관계입니다. Prisma에서는 명시적 중간 테이블을 권장합니다.

CODE
model PostTag {
  postId String
  tagId  String

  post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
  tag  Tag  @relation(fields: [tagId], references: [id], onDelete: Cascade)

  @@id([postId, tagId])
}

명시적 중간 테이블을 쓰면 나중에 order 같은 컬럼을 추가하기 쉽습니다.

셀프 참조 — 대댓글

CODE
model Comment {
  id        String   @id @default(cuid())
  content   String
  status    String   @default("PENDING")  // PENDING | APPROVED | REJECTED

  postId   String
  post     Post     @relation(fields: [postId], references: [id], onDelete: Cascade)
  authorId String?
  author   User?    @relation(fields: [authorId], references: [id], onDelete: SetNull)

  // 셀프 참조로 대댓글 구현
  parentId String?
  parent   Comment?  @relation("CommentReplies", fields: [parentId], references: [id], onDelete: Cascade)
  replies  Comment[] @relation("CommentReplies")
}

마이그레이션

CODE
# 최초 마이그레이션
npx prisma migrate dev --name init

# 스키마 변경 후
npx prisma migrate dev --name add-series

# Prisma Client 재생성
npx prisma generate

Prisma 클라이언트 싱글톤

Next.js 개발 환경에서는 Hot Reload 때마다 새 연결이 생겨 리소스가 낭비됩니다.

CODE
// src/lib/prisma.ts
import { PrismaClient } from '@prisma/client'
import { PrismaBetterSqlite3 } from '@prisma/adapter-better-sqlite3'
import path from 'path'

const globalForPrisma = globalThis as unknown as { prisma: PrismaClient }

function createClient() {
  const url = `file:${path.join(process.cwd(), 'prisma/dev.db')}`
  const adapter = new PrismaBetterSqlite3({ url })
  return new PrismaClient({ adapter } as any)
}

export const prisma = globalForPrisma.prisma ?? createClient()

if (process.env.NODE_ENV !== 'production') {
  globalForPrisma.prisma = prisma
}

globalThis에 인스턴스를 캐싱해 Hot Reload에도 단일 연결을 유지합니다.

읽기 시간 자동 계산

포스트를 저장할 때 내용 길이를 기반으로 읽기 시간을 계산합니다.

CODE
export function calcReadingTime(content: string): number {
  const words = content.trim().split(/\s+/).length
  return Math.max(1, Math.round(words / 200)) // 분당 200단어
}

다음 편에서는 NextAuth.js v5를 사용해 관리자 인증 시스템을 구현합니다.

공유
S

SHIN

.NET 개발자입니다

GitHub

이전 포스트

2편. 프로젝트 초기 설정과 디렉토리 구조

다음 포스트

4편. NextAuth.js로 관리자 인증 구현하기

같은 카테고리 포스트

9편. Vercel 배포와 성능 최적화

2025년 4월 10일· 7분

8편. SEO 최적화 — 메타태그·sitemap·RSS

2025년 4월 5일· 2분

7편. 검색·카테고리·태그·시리즈 기능 구현

2025년 3월 30일· 8분

댓글