Prisma ORM 완벽 가이드 — TypeScript와 함께하는 현대적 데이터베이스

PrismaORMTypeScriptPostgreSQL데이터베이스백엔드

Prisma는 Node.js/TypeScript 생태계에서 가장 인기 있는 ORM입니다. 타입 안전성, 직관적인 쿼리 API, 강력한 마이그레이션 도구를 제공합니다.


Prisma vs 다른 ORM

항목PrismaTypeORMDrizzle
타입 안전성⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
학습 곡선낮음높음중간
쿼리 스타일객체 기반메서드 체이닝SQL 스타일
마이그레이션내장내장별도 도구
번들 크기중간작음
Edge 지원Prisma Accelerate

설치 및 초기 설정

1. 패키지 설치

npm install prisma --save-dev
npm install @prisma/client

# 초기화
npx prisma init

2. 생성된 구조

my-app/
├── prisma/
│   └── schema.prisma    # 스키마 정의
├── .env                  # DATABASE_URL
└── package.json

3. 데이터베이스 연결

# .env
DATABASE_URL="postgresql://user:password@localhost:5432/mydb?schema=public"

# 다른 데이터베이스
# MySQL: mysql://user:password@localhost:3306/mydb
# SQLite: file:./dev.db
# MongoDB: mongodb+srv://user:password@cluster.mongodb.net/mydb

스키마 설계

기본 모델

// prisma/schema.prisma
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model User {
  id        String   @id @default(cuid())
  email     String   @unique
  name      String?
  role      Role     @default(USER)
  posts     Post[]
  profile   Profile?
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  @@index([email])
}

model Post {
  id        String   @id @default(cuid())
  title     String
  content   String?
  published Boolean  @default(false)
  author    User     @relation(fields: [authorId], references: [id])
  authorId  String
  tags      Tag[]
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  @@index([authorId])
  @@index([published, createdAt])
}

model Profile {
  id     String  @id @default(cuid())
  bio    String?
  avatar String?
  user   User    @relation(fields: [userId], references: [id], onDelete: Cascade)
  userId String  @unique
}

model Tag {
  id    String @id @default(cuid())
  name  String @unique
  posts Post[]
}

enum Role {
  USER
  ADMIN
  MODERATOR
}

관계 유형


마이그레이션

개발 환경

# 마이그레이션 생성 및 적용
npx prisma migrate dev --name init

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

프로덕션 환경

# 마이그레이션만 적용 (생성 없이)
npx prisma migrate deploy

# CI/CD에서
npx prisma migrate deploy && node dist/index.js

프로토타이핑 (빠른 테스트)

# 마이그레이션 없이 DB 동기화 (개발용)
npx prisma db push

# DB 초기화
npx prisma migrate reset

CRUD 작업

Prisma Client 설정

// lib/prisma.ts
import { PrismaClient } from '@prisma/client'

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

export const prisma =
  globalForPrisma.prisma ??
  new PrismaClient({
    log: process.env.NODE_ENV === 'development'
      ? ['query', 'error', 'warn']
      : ['error'],
  })

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

Create

// 단일 생성
const user = await prisma.user.create({
  data: {
    email: 'user@example.com',
    name: '홍길동',
    profile: {
      create: { bio: '개발자입니다' }, // 중첩 생성
    },
  },
  include: {
    profile: true,
  },
})

// 다중 생성
const users = await prisma.user.createMany({
  data: [
    { email: 'a@example.com', name: 'A' },
    { email: 'b@example.com', name: 'B' },
  ],
  skipDuplicates: true,
})

Read

// 단일 조회
const user = await prisma.user.findUnique({
  where: { email: 'user@example.com' },
})

// 첫 번째 일치 항목
const post = await prisma.post.findFirst({
  where: { published: true },
  orderBy: { createdAt: 'desc' },
})

// 목록 조회
const posts = await prisma.post.findMany({
  where: {
    published: true,
    author: {
      role: 'ADMIN',
    },
  },
  include: {
    author: {
      select: { name: true, email: true },
    },
    tags: true,
  },
  orderBy: { createdAt: 'desc' },
  take: 10,
  skip: 0,
})

// 개수
const count = await prisma.post.count({
  where: { published: true },
})

Update

// 단일 업데이트
const user = await prisma.user.update({
  where: { id: 'user-id' },
  data: {
    name: '새 이름',
    profile: {
      update: { bio: '새로운 소개' },
    },
  },
})

// 다중 업데이트
const result = await prisma.post.updateMany({
  where: { authorId: 'user-id' },
  data: { published: false },
})

// Upsert (있으면 업데이트, 없으면 생성)
const user = await prisma.user.upsert({
  where: { email: 'user@example.com' },
  update: { name: '업데이트 이름' },
  create: {
    email: 'user@example.com',
    name: '새 사용자',
  },
})

Delete

// 단일 삭제
await prisma.user.delete({
  where: { id: 'user-id' },
})

// 다중 삭제
await prisma.post.deleteMany({
  where: { published: false },
})

// 관계 포함 삭제 (Cascade 설정 필요)
await prisma.user.delete({
  where: { id: 'user-id' },
  // Profile도 함께 삭제됨 (onDelete: Cascade)
})

고급 쿼리

필터링

const posts = await prisma.post.findMany({
  where: {
    // AND 조건
    AND: [
      { published: true },
      { authorId: 'user-id' },
    ],

    // OR 조건
    OR: [
      { title: { contains: '검색어' } },
      { content: { contains: '검색어' } },
    ],

    // NOT 조건
    NOT: { authorId: 'blocked-user' },

    // 비교 연산
    createdAt: { gte: new Date('2026-01-01') },

    // 포함 여부
    tags: {
      some: { name: 'TypeScript' },
    },
  },
})

집계 함수

// 집계
const stats = await prisma.post.aggregate({
  _count: { id: true },
  _avg: { viewCount: true },
  _max: { viewCount: true },
  where: { published: true },
})

// 그룹화
const byAuthor = await prisma.post.groupBy({
  by: ['authorId'],
  _count: { id: true },
  having: {
    id: { _count: { gt: 5 } },
  },
})

Raw SQL

// 읽기 쿼리
const users = await prisma.$queryRaw`
  SELECT * FROM "User"
  WHERE email LIKE ${`%@example.com`}
`

// 쓰기 쿼리
await prisma.$executeRaw`
  UPDATE "Post"
  SET "viewCount" = "viewCount" + 1
  WHERE id = ${postId}
`

트랜잭션

// 자동 트랜잭션 ($transaction 배열)
const [user, post] = await prisma.$transaction([
  prisma.user.create({ data: { email: 'new@example.com' } }),
  prisma.post.create({ data: { title: '첫 글', authorId: 'temp' } }),
])

// 인터랙티브 트랜잭션
const result = await prisma.$transaction(async (tx) => {
  const user = await tx.user.create({
    data: { email: 'user@example.com' },
  })

  const post = await tx.post.create({
    data: {
      title: '첫 글',
      authorId: user.id,
    },
  })

  // 조건부 롤백
  if (someCondition) {
    throw new Error('롤백')
  }

  return { user, post }
})

성능 최적화

N+1 문제 해결

// ❌ N+1 문제 발생
const users = await prisma.user.findMany()
for (const user of users) {
  const posts = await prisma.post.findMany({
    where: { authorId: user.id },
  })
}

// ✅ include로 해결
const users = await prisma.user.findMany({
  include: { posts: true },
})

// ✅ select로 필요한 필드만
const users = await prisma.user.findMany({
  select: {
    id: true,
    name: true,
    posts: {
      select: { title: true },
      take: 5,
    },
  },
})

인덱스 설정

model Post {
  id        String @id @default(cuid())
  title     String
  authorId  String
  published Boolean
  createdAt DateTime @default(now())

  // 단일 인덱스
  @@index([authorId])

  // 복합 인덱스
  @@index([published, createdAt])

  // 유니크 인덱스
  @@unique([authorId, title])
}

페이지네이션

// Offset 페이지네이션
const page = 1
const perPage = 10

const posts = await prisma.post.findMany({
  skip: (page - 1) * perPage,
  take: perPage,
  orderBy: { createdAt: 'desc' },
})

// Cursor 페이지네이션 (대용량에 효율적)
const posts = await prisma.post.findMany({
  take: 10,
  cursor: { id: lastPostId },
  skip: 1, // cursor 다음부터
  orderBy: { createdAt: 'desc' },
})

Prisma Studio

# GUI 데이터 브라우저 실행
npx prisma studio
  • 브라우저에서 데이터 조회/수정
  • 관계 시각화
  • 필터링 및 정렬

Next.js 통합

Server Actions

// app/actions/user.ts
'use server'

import { prisma } from '@/lib/prisma'
import { revalidatePath } from 'next/cache'

export async function createUser(formData: FormData) {
  const user = await prisma.user.create({
    data: {
      email: formData.get('email') as string,
      name: formData.get('name') as string,
    },
  })

  revalidatePath('/users')
  return user
}

API Routes

// app/api/posts/route.ts
import { prisma } from '@/lib/prisma'
import { NextResponse } from 'next/server'

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url)
  const page = parseInt(searchParams.get('page') || '1')

  const posts = await prisma.post.findMany({
    where: { published: true },
    take: 10,
    skip: (page - 1) * 10,
    orderBy: { createdAt: 'desc' },
  })

  return NextResponse.json(posts)
}

마치며

Prisma 선택 이유:

  1. 타입 안전: 컴파일 타임에 쿼리 오류 발견
  2. DX: 자동완성, 마이그레이션, Studio
  3. 유지보수: 스키마 중심의 명확한 구조
  4. 생태계: Next.js, NestJS 등과 완벽 호환

단점도 고려하세요:

  • 번들 크기가 큼 (Edge에서 제한)
  • 복잡한 쿼리는 Raw SQL 필요
  • 일부 DB 기능 미지원

대부분의 풀스택 프로젝트에서 Prisma는 최선의 선택입니다.

궁금한 점이 있으신가요?

협업·의뢰는 아래로, 가벼운 소통은 인스타그램 @bluefox._.hi도 환영이에요.