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

발행 읽는 시간 약 2 분

  • Prisma
  • ORM
  • TypeScript
  • PostgreSQL
  • 데이터베이스
  • 백엔드

글 소개

Prisma로 타입 안전한 데이터베이스 작업을 구현하는 방법. 스키마 설계, 마이그레이션, 쿼리 최적화, 관계 설정까지 모든 것을 다룹니다.

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


Prisma vs 다른 ORM#

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

설치 및 초기 설정#

1. 패키지 설치#

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

# 초기화
npx prisma init

2. 생성된 구조#

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

3. 데이터베이스 연결#

Dotenv
# .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
// 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
}

관계 유형#

Diagram · Mermaid

마이그레이션#

개발 환경#

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

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

프로덕션 환경#

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

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

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

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

# DB 초기화
npx prisma migrate reset

CRUD 작업#

Prisma Client 설정#

TypeScript
// 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#

TypeScript
// 단일 생성
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#

TypeScript
// 단일 조회
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#

TypeScript
// 단일 업데이트
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#

TypeScript
// 단일 삭제
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)
})

고급 쿼리#

필터링#

TypeScript
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' },
    },
  },
})

집계 함수#

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#

TypeScript
// 읽기 쿼리
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}
`

트랜잭션#

TypeScript
// 자동 트랜잭션 ($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 문제 해결#

TypeScript
// ❌ 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,
    },
  },
})

인덱스 설정#

Prisma
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])
}

페이지네이션#

TypeScript
// 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#

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

Next.js 통합#

Server Actions#

TypeScript
// 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#

TypeScript
// 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는 최선의 선택입니다.

Follow · Ask · Work

이 글이 도움이 됐다면

짧은 작업 기록과 데모는 인스타그램에 올립니다. 만들고 싶은 것이 있다면 프로젝트 의뢰나 협업 제안도 편하게 보내 주세요.

글쓴이

파란여우

직접 만든 AI 앱·제품의 개발·배포를 공개하는 실전형 테크 크리에이터

파란여우 소개 Instagram @bluefox._.hi

시리즈 · 개발 노트

시리즈 전체 보기

이어서 보기

만들고 싶은 것이 있다면 프로젝트 의뢰로 알려 주세요.