Prisma 완벽 가이드: 차세대 TypeScript ORM
이 글의 핵심
Prisma는 타입 안전하고 직관적인 차세대 ORM으로 Prisma Schema로 DB를 정의하고 자동 생성된 타입 안전 클라이언트로 쿼리합니다. 마이그레이션, Prisma Studio GUI, 강력한 관계 쿼리로 생산성이 높습니다.
Prisma란?
Prisma는 차세대 TypeScript ORM(Object-Relational Mapping)으로, 타입 안전성과 개발자 경험을 극대화한 데이터베이스 툴킷입니다.
핵심 구성 요소
-
Prisma Schema
- 선언적 데이터 모델
- 단일 진실 공급원
- 마이그레이션 생성
-
Prisma Client
- 자동 생성 쿼리 빌더
- 완벽한 타입 안전성
- 자동완성 지원
-
Prisma Migrate
- 스키마 마이그레이션
- 버전 관리
- 롤백 지원
-
Prisma Studio
- GUI 데이터 브라우저
- 데이터 편집
- 시각적 탐색
TypeORM vs Sequelize vs Prisma 비교
| 항목 | TypeORM | Sequelize | Prisma |
|---|---|---|---|
| 언어 | TypeScript | JavaScript/TS | TypeScript |
| 스키마 정의 | 데코레이터 클래스 | 모델 클래스 | Schema 파일 |
| 타입 안전성 | 좋음 | 보통 | 매우 좋음 |
| 마이그레이션 | CLI | CLI | Migrate CLI |
| Raw SQL | 지원 | 지원 | 지원 |
| 관계 쿼리 | Eager/Lazy | Include | Include |
| N+1 해결 | 수동 | 수동 | 자동 |
| GUI | ❌ | ❌ | Prisma Studio |
| 학습 곡선 | 중간 | 중간 | 낮음 |
프로젝트 설정
Prisma를 시작하려면 먼저 프로젝트에 필요한 패키지를 설치해야 합니다. Prisma는 개발 의존성(devDependencies)으로 설치하고, Prisma Client는 런타임에 필요하므로 일반 의존성(dependencies)으로 설치합니다.
설치 후 npx prisma init 명령어를 실행하면 prisma 폴더와 schema.prisma 파일이 자동으로 생성됩니다. 이 파일이 데이터베이스 스키마를 정의하는 중심 파일입니다. 데이터베이스 종류에 따라 적절한 provider를 선택할 수 있습니다.
# Prisma 설치
npm install -D prisma
npm install @prisma/client
# Prisma 초기화
npx prisma init
# PostgreSQL 사용 시
npx prisma init --datasource-provider postgresql
# MySQL 사용 시
npx prisma init --datasource-provider mysql
초기화 후 .env 파일에 DATABASE_URL을 설정해야 합니다. 이 URL은 데이터베이스 연결 정보를 담고 있으며, 환경에 따라 다른 값을 사용할 수 있습니다.
디렉터리 구조
my-project/
├── prisma/
│ ├── schema.prisma # 스키마 정의
│ └── migrations/ # 마이그레이션 파일
├── src/
│ └── index.ts
├── .env # DATABASE_URL
└── package.json
Prisma Schema
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
role Role @default(USER)
posts Post[]
profile Profile?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([email])
@@map("users") // 테이블명 지정
}
model Profile {
id Int @id @default(autoincrement())
bio String?
userId Int @unique
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
authorId Int
author User @relation(fields: [authorId], references: [id])
tags Tag[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([authorId])
@@index([published])
}
model Tag {
id Int @id @default(autoincrement())
name String @unique
posts Post[]
}
enum Role {
USER
ADMIN
MODERATOR
}
마이그레이션
# 마이그레이션 생성 및 적용
npx prisma migrate dev --name init
# 프로덕션 적용
npx prisma migrate deploy
# 마이그레이션 상태 확인
npx prisma migrate status
# 마이그레이션 리셋 (개발 전용)
npx prisma migrate reset
# Prisma Client 재생성
npx prisma generate
Prisma Client 사용
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
// Create
const user = await prisma.user.create({
data: {
email: '[email protected]',
name: '홍길동',
posts: {
create: [
{ title: '첫 번째 글' },
{ title: '두 번째 글' }
]
}
}
});
// Read
const users = await prisma.user.findMany();
const user = await prisma.user.findUnique({
where: { email: '[email protected]' }
});
// Include 관계
const userWithPosts = await prisma.user.findUnique({
where: { id: 1 },
include: {
posts: true,
profile: true
}
});
// Update
const user = await prisma.user.update({
where: { id: 1 },
data: { name: '김철수' }
});
// Delete
await prisma.user.delete({
where: { id: 1 }
});
고급 쿼리
필터링
// AND 조건
const users = await prisma.user.findMany({
where: {
AND: [
{ age: { gte: 20 } },
{ role: 'USER' }
]
}
});
// OR 조건
const users = await prisma.user.findMany({
where: {
OR: [
{ email: { contains: 'gmail.com' } },
{ email: { contains: 'naver.com' } }
]
}
});
// NOT 조건
const users = await prisma.user.findMany({
where: {
NOT: { role: 'ADMIN' }
}
});
관계 필터링
// 포스트가 있는 사용자만
const users = await prisma.user.findMany({
where: {
posts: {
some: {
published: true
}
}
}
});
// 특정 태그가 있는 포스트
const posts = await prisma.post.findMany({
where: {
tags: {
some: {
name: 'typescript'
}
}
},
include: {
author: true,
tags: true
}
});
집계
// 카운트
const userCount = await prisma.user.count();
const activeUsers = await prisma.user.count({
where: { isActive: true }
});
// 그룹화 집계
const stats = await prisma.user.groupBy({
by: ['role'],
_count: { id: true },
_avg: { age: true }
});
트랜잭션
// 연속 트랜잭션
const [user, post] = await prisma.$transaction([
prisma.user.create({ data: { email: '[email protected]' } }),
prisma.post.create({ data: { title: 'Hello' } })
]);
// 대화형 트랜잭션
await prisma.$transaction(async (tx) => {
const user = await tx.user.create({
data: { email: '[email protected]' }
});
await tx.post.create({
data: {
title: 'Hello',
authorId: user.id
}
});
// 에러 발생 시 자동 롤백
});
성능 최적화
Prisma는 기본적으로 성능이 우수하지만, 대규모 애플리케이션에서는 추가 최적화가 필요합니다. 가장 중요한 것은 필요한 데이터만 조회하는 것입니다.
Select를 사용하면 필요한 필드만 조회해 네트워크 전송량과 메모리 사용량을 줄일 수 있습니다. 특히 BLOB 필드나 긴 텍스트 필드가 있는 경우 큰 차이가 납니다.
// Select로 필요한 필드만
const users = await prisma.user.findMany({
select: {
id: true,
name: true,
email: true
}
});
// 페이지네이션
const users = await prisma.user.findMany({
skip: 10,
take: 10
});
// 커서 기반 페이지네이션
const users = await prisma.user.findMany({
take: 10,
cursor: {
id: lastUserId
},
skip: 1
});
페이지네이션은 두 가지 방식이 있습니다. Offset 기반(skip/take)은 구현이 간단하지만 데이터가 많으면 느려집니다. 커서 기반은 일관된 성능을 제공하지만 이전 페이지로 이동하기 어렵습니다.
실전 사례: 블로그 시스템 구축
실무에서 Prisma를 사용해 블로그 시스템을 구축하는 과정을 살펴보겠습니다. 사용자, 포스트, 댓글, 태그의 관계를 효율적으로 모델링하고 쿼리하는 방법을 다룹니다.
N+1 문제 자동 해결
전통적인 ORM에서는 N+1 문제가 자주 발생합니다. 예를 들어 10개의 포스트를 조회하고 각 포스트의 작성자를 조회하면 총 11번의 쿼리가 실행됩니다(1번: 포스트 목록, 10번: 각 작성자). Prisma는 include를 사용하면 자동으로 JOIN이나 IN 쿼리로 최적화합니다.
// Prisma는 자동으로 최적화된 쿼리 생성
const posts = await prisma.post.findMany({
include: {
author: {
select: {
name: true,
email: true
}
},
tags: true
}
});
이 쿼리는 내부적으로 효율적인 JOIN 또는 2번의 쿼리(포스트, 태그)로 실행됩니다. 개발자가 직접 최적화를 신경 쓸 필요가 없습니다.
복잡한 필터링 구현
실무에서는 여러 조건을 조합한 복잡한 검색 기능이 자주 필요합니다. Prisma의 조합 연산자(AND, OR, NOT)를 사용하면 복잡한 쿼리도 타입 안전하게 작성할 수 있습니다.
// 게시된 포스트 중 특정 태그가 있거나 제목에 키워드가 포함된 포스트
const posts = await prisma.post.findMany({
where: {
AND: [
{ published: true },
{
OR: [
{
tags: {
some: {
name: { in: ['typescript', 'prisma'] }
}
}
},
{
title: {
contains: '튜토리얼',
mode: 'insensitive' // 대소문자 무시
}
}
]
}
]
},
include: {
author: true,
tags: true,
_count: {
select: { comments: true }
}
},
orderBy: {
createdAt: 'desc'
}
});
이런 복잡한 쿼리도 타입 안전성이 보장되어 오타나 잘못된 필드명 사용을 컴파일 타임에 잡을 수 있습니다.
트랜잭션으로 데이터 일관성 보장
포스트를 삭제할 때 관련된 댓글과 태그 관계도 함께 삭제해야 합니다. 트랜잭션을 사용하면 모든 작업이 성공하거나 모두 실패하도록 보장할 수 있습니다.
async function deletePostWithRelations(postId: number) {
return await prisma.$transaction(async (tx) => {
// 댓글 삭제
await tx.comment.deleteMany({
where: { postId }
});
// 포스트 삭제 (태그 관계는 onDelete: Cascade로 자동 삭제)
await tx.post.delete({
where: { id: postId }
});
// 사용되지 않는 태그 정리
await tx.tag.deleteMany({
where: {
posts: {
none: {}
}
}
});
});
}
트랜잭션 중 에러가 발생하면 모든 변경사항이 자동으로 롤백됩니다.
주의사항과 팁
1. 연결 풀 관리
서버리스 환경(Lambda, Vercel)에서는 연결 풀이 금방 고갈될 수 있습니다. Prisma Data Proxy나 PgBouncer 같은 연결 풀러를 사용하세요.
2. 마이그레이션 전략
개발 환경에서는 prisma migrate dev를 사용하고, 프로덕션에서는 prisma migrate deploy를 사용하세요. 절대 프로덕션에서 migrate reset을 실행하면 안 됩니다.
3. 타입 생성 자동화
스키마를 변경한 후 반드시 npx prisma generate를 실행해 타입을 재생성해야 합니다. CI/CD 파이프라인에 이 단계를 포함하세요.
Prisma는 현대적이고 타입 안전한 ORM입니다. 직관적인 API와 강력한 타입 시스템으로 데이터베이스 작업을 즐겁게 만듭니다. N+1 문제 자동 해결, 트랜잭션 지원, Prisma Studio GUI 등 생산성을 높이는 기능이 풍부합니다.
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. Prisma를 활용한 타입 안전 데이터베이스 접근 완벽 가이드. TypeORM/Sequelize 대비 장점, 설치부터 스키마 정의, 마이그레이션, Prisma Client, 관계 쿼리, 트랜잭션, 성능 최적화까지 실… 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.
이 글에서 다루는 키워드 (관련 검색어)
Prisma, ORM, TypeScript, Database, PostgreSQL, MySQL, Node.js 등으로 검색하시면 이 글이 도움이 됩니다.