Prisma는 Node.js/TypeScript 생태계에서 가장 인기 있는 ORM입니다. 타입 안전성, 직관적인 쿼리 API, 강력한 마이그레이션 도구를 제공합니다.
Prisma vs 다른 ORM
| 항목 | Prisma | TypeORM | Drizzle |
|---|---|---|---|
| 타입 안전성 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| 학습 곡선 | 낮음 | 높음 | 중간 |
| 쿼리 스타일 | 객체 기반 | 메서드 체이닝 | 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 선택 이유:
- 타입 안전: 컴파일 타임에 쿼리 오류 발견
- DX: 자동완성, 마이그레이션, Studio
- 유지보수: 스키마 중심의 명확한 구조
- 생태계: Next.js, NestJS 등과 완벽 호환
단점도 고려하세요:
- 번들 크기가 큼 (Edge에서 제한)
- 복잡한 쿼리는 Raw SQL 필요
- 일부 DB 기능 미지원
대부분의 풀스택 프로젝트에서 Prisma는 최선의 선택입니다.