Prisma ORM을 타입스크립트 프로젝트에 적용하는 단계별 실전 튜토리얼. PostgreSQL 연결부터 schema.prisma 스키마 작성, 마이그레이션, CRUD 쿼리, 관계형 데이터 조회까지 코드 중심으로 안내합니다. 서버리스 환경 연결 풀 관리, Next.js App Router API 라우트 통합, 자주 발생하는 에러 해결법도 포함합니다. 드리즐(Drizzle) ORM과의 선택 기준도 정리했습니다. 백엔드, 풀스택 개발자가 Prisma를 빠르게 도입하도록 실습 코드 중심으로 구성했습니다.
슈퍼베이스(Supabase)를 주로 쓰다가 기존 PostgreSQL 서버를 직접 제어해야 하는 프로젝트에서 처음 Prisma를 도입했습니다. 공식 문서는 충실하지만 막상 Next.js App Router와 연결할 때 서버리스 연결 풀이나 마이그레이션 배포 순서에서 시행착오를 겪었습니다. 이 튜토리얼은 그 경험을 바탕으로, 환경 설정부터 스키마 설계·마이그레이션·CRUD·관계형 쿼리·Next.js 라우트 연동까지 코드 중심으로 단계별 정리했습니다.
Prisma는 타입스크립트(TypeScript) 생태계에서 가장 많이 쓰이는 ORM 중 하나입니다. 스키마 파일 한 장으로 데이터베이스 구조를 선언하고 자동 완성되는 타입 안전 클라이언트를 생성해주는 방식이 풀스택 프로젝트에 잘 어울립니다. 드리즐(Drizzle) ORM과 비교했을 때 마이그레이션 UX가 더 성숙해 팀 협업 환경에 유리합니다.
Prisma란 무엇인가 — 타입 안전 ORM의 핵심 구조
Prisma는 Node.js·타입스크립트 환경을 위한 오픈소스 ORM입니다. 데이터베이스 작업을 타입 안전하게 처리할 수 있어 풀스택 개발자들 사이에서 빠르게 퍼졌습니다. 세 가지 핵심 모듈로 구성됩니다.
Prisma Client — 자동 생성되는 타입 안전 데이터베이스 클라이언트. 스키마에서 타입이 직접 추론되어 IDE 자동 완성이 완벽하게 지원됩니다.
Prisma Migrate — schema.prisma의 변경사항을 감지해 SQL 마이그레이션 파일을 자동 생성·적용합니다. 팀 전체가 동일한 스키마를 유지할 수 있어 협업에 특히 유용합니다.
Prisma Studio — 로컬 브라우저에서 데이터를 직접 조회·수정할 수 있는 GUI 도구입니다. 별도 DB 클라이언트 없이 데이터를 확인하고 싶을 때 편리합니다.
세 모듈이 함께 작동하여 스키마 설계부터 실제 쿼리 실행까지 일관된 개발 경험을 제공합니다. 데이터베이스 구조가 바뀌면 스키마 파일 하나를 수정하고 마이그레이션 명령어 하나만 실행하면 됩니다. 타입스크립트 컴파일러와 연동되어 런타임 오류를 사전에 잡아준다는 점도 큰 강점입니다.
드리즐 ORM과 비교하면, Prisma는 마이그레이션 히스토리 관리와 팀 협업에 특화되어 있고, 드리즐은 SQL에 가까운 문법이나 번들 크기 최소화가 필요한 엣지 환경에 유리합니다. PostgreSQL·MySQL·SQLite·MongoDB를 공식 지원하며, 슈퍼베이스나 PlanetScale과도 바로 연결됩니다. 국내 1인 개발자들 사이에서는 슈퍼베이스와 함께 쓰는 조합이 특히 인기입니다.
개발 환경 설정 — Node.js·PostgreSQL·Prisma 초기화
이 튜토리얼은 Node.js 20 LTS 이상을 전제합니다. 로컬 PostgreSQL이 없다면 도커(Docker) 한 줄로 띄울 수 있습니다.
Prisma 스키마 파일은 데이터베이스 테이블 구조를 선언적으로 작성합니다. 블로그 예제를 기준으로 User와 Post 모델을 정의해보겠습니다.
prisma/schema.prisma — User, Post 모델 정의
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?
posts Post[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id])
authorId Int
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
핵심 어노테이션 정리입니다.
어노테이션
설명
@id
기본 키 지정
@default(autoincrement())
자동 증가 정수 ID
@unique
유니크 제약 조건
@relation
외래 키 관계 선언
@updatedAt
업데이트 시 자동 타임스탬프 갱신
?(물음표)
nullable(선택) 필드
스키마를 수정한 뒤에는 마이그레이션을 실행해야 실제 데이터베이스에 반영됩니다. 마이그레이션은 실제 변경 내역을 파일로 남기기 때문에 나중에 어떤 시점에 어떤 테이블이 추가·수정됐는지 추적할 수 있습니다. 이 파일들이 깃 저장소에 포함되어 있어야 팀원 모두가 동일한 데이터베이스 구조를 유지할 수 있습니다.
모델 이름은 단수·파스칼케이스(첫 글자 대문자)를 권장합니다. Prisma가 자동으로 복수형 테이블 이름을 매핑하지만, 명시적으로 지정하려면 @@map 어노테이션을 사용하면 됩니다.
첫 마이그레이션 실행 — 스키마를 데이터베이스에 반영하기
Prisma Migrate는 schema.prisma의 변경사항을 감지해 SQL 파일을 자동 생성하고 데이터베이스에 적용합니다. 개발 환경에서는 migrate dev 명령을 사용합니다.
개발 환경 마이그레이션 실행
# 마이그레이션 파일 생성 및 데이터베이스 적용
npx prisma migrate dev --name init
# 예상 출력:
# Applying migration 20260708000000_init
# Your database is now in sync with your schema.
# Generated Prisma Client
# 마이그레이션 상태 확인
npx prisma migrate status
# Prisma Studio 실행 (브라우저 GUI — localhost:5555)
npx prisma studio
migrate dev vs migrate deploy migrate dev는 개발 전용입니다. 프로덕션에서는 반드시 npx prisma migrate deploy를 사용하세요. migrate dev는 스키마 변경 감지 중 기존 데이터를 삭제할 수 있습니다.
마이그레이션이 완료되면 prisma/migrations/ 폴더에 타임스탬프가 붙은 SQL 파일이 생성됩니다. 이 파일을 깃(Git)에 커밋하면 팀 전체가 동일한 스키마를 유지할 수 있습니다.
npx prisma studio 실행 시 localhost:5555에서 데이터를 직접 조회·수정할 수 있습니다
Prisma Client CRUD 구현 — 생성·조회·수정·삭제
마이그레이션 후 Prisma Client가 자동 생성됩니다. src/index.ts 파일을 만들어 기본 CRUD를 실습합니다.
src/index.ts — 기본 CRUD 실습
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
async function main() {
// 1. 사용자 생성
const user = await prisma.user.create({
data: {
email: 'dev@example.com',
name: '김개발',
},
})
console.log('생성된 사용자:', user)
// 2. 포스트 생성 (관계 연결)
const post = await prisma.post.create({
data: {
title: 'Prisma 입문',
content: 'Prisma로 데이터베이스를 다루는 방법',
published: true,
author: { connect: { id: user.id } },
},
})
console.log('생성된 포스트:', post)
// 3. 사용자 + 관련 포스트 조회
const userWithPosts = await prisma.user.findUnique({
where: { email: 'dev@example.com' },
include: { posts: true },
})
console.log('사용자+포스트:', JSON.stringify(userWithPosts, null, 2))
// 4. 포스트 수정
const updated = await prisma.post.update({
where: { id: post.id },
data: { title: 'Prisma 입문 (개정판)' },
})
console.log('수정:', updated.title)
// 5. 포스트 삭제
await prisma.post.delete({ where: { id: post.id } })
console.log('삭제 완료')
}
main()
.catch(console.error)
.finally(() => prisma.$disconnect())
실행 방법은 다음과 같습니다.
npx ts-node src/index.ts
스키마가 변경될 때마다 Prisma Client 재생성이 필요합니다. npx prisma migrate dev 실행 시 자동으로 재생성되며, 마이그레이션 없이 클라이언트만 갱신하려면 npx prisma generate를 실행합니다.
처음 실습할 때 가장 많이 막히는 지점이 타입 오류입니다. 예를 들어 prisma.user.create에 전달하는 데이터 객체의 필드 이름이나 타입이 스키마와 다르면 타입스크립트 컴파일 단계에서 바로 오류가 납니다. 이게 불편하게 느껴질 수 있지만, 실제 데이터베이스에 잘못된 값을 넣기 전에 미리 잡아준다는 점에서 장점입니다. 자동 완성이 잘 동작하려면 편집기가 타입스크립트 언어 서버와 연결되어 있어야 합니다.
관계형 쿼리 — include, select, 중첩 생성 패턴
Prisma의 강점은 관계형 쿼리를 타입 안전하게 작성할 수 있다는 점입니다. include로 연관 데이터를 가져오거나, select로 필요한 필드만 추출할 수 있습니다.
관계형 쿼리 패턴 — include, select, 중첩 생성, 페이지네이션
// 발행된 포스트가 있는 사용자 목록
const activeUsers = await prisma.user.findMany({
where: {
posts: { some: { published: true } },
},
include: {
posts: {
where: { published: true },
orderBy: { createdAt: 'desc' },
take: 5,
},
},
})
// 필요한 필드만 select (포스트 수 집계 포함)
const emailList = await prisma.user.findMany({
select: {
id: true,
email: true,
name: true,
_count: { select: { posts: true } },
},
})
// 중첩 생성 — 사용자 + 포스트 한 번에
const newUser = await prisma.user.create({
data: {
email: 'newuser@example.com',
name: '이신규',
posts: {
create: [
{ title: '첫 번째 글', published: true },
{ title: '두 번째 글 (초안)', published: false },
],
},
},
include: { posts: true },
})
// 오프셋 기반 페이지네이션 (페이지 3, 페이지당 10개)
const page3 = await prisma.post.findMany({
where: { published: true },
skip: 20,
take: 10,
orderBy: { createdAt: 'desc' },
})
무한 스크롤에는 커서 기반 페이지네이션이 더 적합합니다. 오프셋 방식보다 대용량 데이터에서 성능이 좋습니다.
// 커서 기반 (마지막으로 본 항목 ID 기준)
const nextPage = await prisma.post.findMany({
take: 10,
skip: 1, // 커서 자체는 결과에서 제외
cursor: { id: lastSeenId },
orderBy: { id: "asc" },
})
Next.js App Router 연동 — 싱글톤 패턴과 API 라우트
Next.js App Router에서 Prisma를 사용할 때는 서버리스 환경의 연결 풀 관리에 주의해야 합니다. 개발 환경에서 핫 리로드가 발생할 때마다 새 PrismaClient 인스턴스가 생성되지 않도록 싱글톤 패턴을 적용합니다.
lib/prisma.ts — 싱글톤 Prisma Client (Next.js 필수 패턴)
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
}
이 패턴은 개발 환경에서 핫 리로드가 발생해도 Prisma Client 인스턴스가 하나만 유지되도록 보장합니다. 프로덕션 환경은 모듈 캐시가 유지되므로 globalThis 없이도 싱글톤이 되지만, 양쪽 환경 모두 적용해도 문제없습니다.
Next.js App Router에서 lib/prisma.ts 싱글톤을 통해 PostgreSQL에 연결하는 구조
프로덕션 배포 시 주의사항 — 연결 풀과 마이그레이션 순서
버셀(Vercel)이나 AWS Lambda 같은 서버리스 환경에 배포할 때 주의할 사항입니다. 로컬 개발과 달리 서버리스는 요청이 들어올 때마다 새 실행 환경이 생겨날 수 있어, 데이터베이스 연결 수가 예상보다 빠르게 늘어납니다. 이 문제를 미리 대비하지 않으면 데이터베이스가 연결 한도를 초과해 응답 오류가 발생합니다.
연결 풀 관리: 슈퍼베이스 사용 시 트랜잭션 모드 URL에 ?pgbouncer=true&connection_limit=1을 추가하거나, Neon Serverless Postgres처럼 서버리스 최적화 서비스를 고려하세요. 직접 운영하는 PostgreSQL 서버라면 PgBouncer를 앞에 두는 것을 권장합니다.
마이그레이션 타이밍: 배포 파이프라인에서 prisma migrate deploy를 빌드 완료 후, 서비스 재시작 전에 실행해야 합니다. 서비스가 뜨기 전에 스키마가 적용되어 있어야 새 코드와 데이터베이스 구조가 맞습니다.
postinstall 훅: package.json의 postinstall에 prisma generate를 추가하면 배포 서버에서 패키지 설치 후 자동으로 클라이언트가 생성됩니다. 이 단계를 빠뜨리면 배포 환경에서 타입을 찾지 못해 오류가 납니다.
팀 규모와 SQL 숙련도에 따라 다릅니다. Prisma는 마이그레이션 히스토리 관리, IDE 자동 완성, Prisma Studio(GUI) 같은 도구가 잘 갖춰져 있어 팀 협업과 빠른 프로토타입에 적합합니다. 드리즐은 SQL에 가까운 문법이나 번들 크기 최소화가 필요한 서버리스 엣지 환경에 유리합니다. 처음 ORM을 도입한다면 Prisma의 진입 장벽이 더 낮습니다.
버셀(Vercel) 배포 시 연결 풀 문제를 어떻게 해결하나요?
버셀은 서버리스 방식이라 요청마다 새 연결이 생길 수 있습니다. 슈퍼베이스 백엔드 사용 시 연결 URL에 ?pgbouncer=true&connection_limit=1을 추가하면 PgBouncer가 연결을 풀링합니다. 직접 PostgreSQL을 운영한다면 PgBouncer를 앞에 두거나, Neon Serverless Postgres처럼 서버리스 최적화된 서비스를 고려하세요.
기존 데이터베이스에 Prisma를 연결하려면 어떻게 하나요?
이미 운영 중인 데이터베이스에 연결할 때는 npx prisma db pull 명령을 사용합니다. 기존 테이블 구조를 분석해 schema.prisma를 자동 생성합니다. 이후 npx prisma generate로 클라이언트를 만들면 됩니다. 이후 스키마 변경은 Prisma 방식으로 관리해야 마이그레이션 히스토리가 유지됩니다.
스키마를 수정했는데 타입에 반영이 안 됩니다. 어떻게 해야 하나요?
schema.prisma를 변경하면 반드시 npx prisma generate를 실행해 Prisma Client를 재생성해야 합니다. prisma migrate dev 실행 시 자동으로 포함되지만, 마이그레이션 없이 스키마만 수정한 경우 직접 실행해야 합니다. VS Code에 Prisma 공식 익스텐션을 설치하면 스키마 파일 편집 시 문법 강조와 자동 완성도 지원됩니다.
Prisma에서 복잡한 집계 쿼리는 어떻게 처리하나요?
Prisma Client는 groupBy, count, sum, avg, min, max 집계를 지원합니다. Prisma가 지원하지 않는 복잡한 SQL은 $queryRaw로 직접 실행할 수 있습니다. 이때 사용자 입력은 반드시 파라미터로 바인딩해야 SQL 인젝션을 방지할 수 있습니다. 자세한 문법은 Raw SQL 공식 문서를 참고하세요.
Prisma를 사용하면 번들 크기가 커진다는 데 실제로 문제가 될까요?
Prisma Client는 Node.js 서버 환경 위주로 설계되어 번들 크기보다 타입 안전성과 기능에 집중되어 있습니다. 버셀 서버리스 함수 배포 시 전체 클라이언트가 포함되어 콜드 스타트에 영향을 줄 수 있습니다. Cloudflare Workers처럼 엣지 런타임에서 번들 크기가 매우 중요한 경우 드리즐이 적합합니다. 일반 Node.js 서버나 버셀 표준 함수 환경에서는 실무에서 크게 문제가 되지 않습니다.