🎯 요약
Next.js와 Prisma ORM의 조합은 현대 웹 개발에서 가장 강력한 풀스택 솔루션 중 하나입니다. 타입 안전성, 자동완성, 마이그레이션 등 개발자 경험을 극대화하면서 성능까지 챙길 수 있어요. 특히 프론트엔드 웹 개발에서 백엔드 로직을 효율적으로 처리하고 싶은 개발자들에게 필수적인 기술 조합입니다.
📋 목차
- Next.js + Prisma 기본 개념
- Prisma 초기 설정과 스키마 설계
- 고급 스키마 패턴과 관계 설정
- Prisma Client 활용과 쿼리 최적화
- 실무 성능 최적화 전략
- 배포와 프로덕션 환경 설정
Next.js + Prisma 기본 개념
📍 Prisma ORM이란?
Prisma는 차세대 타입스크립트 ORM으로, 데이터베이스 스키마를 코드로 관리하고 타입 안전한 쿼리를 작성할 수 있게 해주는 도구입니다.
🚀 실무에서의 경험담
처음 Prisma를 도입하기 전에는 SQL 쿼리를 직접 작성하고 타입을 수동으로 관리해야 했습니다. 특히 복잡한 조인이나 관계형 데이터를 다룰 때 타입 에러가 자주 발생했죠.
Prisma를 도입한 후에는 스키마 변경 시 타입이 자동으로 업데이트되고, 쿼리 작성 시 자동완성까지 지원돼서 개발 속도가 2배 이상 빨라졌습니다. 런타임 에러도 거의 사라져서 코드 품질이 크게 향상되었어요.
Next.js + Prisma 초기 설정 5단계
Prisma ORM은 타입스크립트와 완벽하게 통합되는 차세대 데이터베이스 도구입니다. 스키마 정의부터 쿼리 작성까지 모든 과정에서 타입 안전성을 보장해줘요.
핵심 특징:
- 선언적 스키마 정의로 데이터 모델 관리
- 자동 타입 생성과 IntelliSense 지원
- 직관적인 쿼리 API와 관계형 데이터 처리
- 마이그레이션 시스템으로 스키마 버전 관리
Prisma ORM은 기존 SQL ORM들의 복잡함을 해결하고 개발자 경험을 혁신적으로 개선한 도구입니다. 특히 타입스크립트 생태계에서는 필수적인 선택이 되었죠.
💡 왜 Next.js와 Prisma를 함께 써야 할까?
실제로 제가 개발하면서 겪었던 상황을 예로 들어보겠습니다:
// 기존 방식 (문제점이 많음)
// pages/api/users.ts
import { NextApiRequest, NextApiResponse } from 'next';
import mysql from 'mysql2';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const connection = mysql.createConnection({
host: 'localhost',
user: 'root',
database: 'myapp'
});
// ❌ SQL 직접 작성으로 타입 안전성 부족
const users = await new Promise((resolve, reject) => {
connection.query('SELECT * FROM users WHERE active = 1', (err, results) => {
if (err) reject(err);
resolve(results);
});
});
// ❌ 타입 추론 불가, any 타입 사용
res.json(users);
}
Prisma를 써야 하는 5가지 이유
- 타입 안전성: 컴파일 타임에 모든 데이터베이스 에러 감지
- 개발자 경험: 자동완성과 IntelliSense로 생산성 극대화
- 마이그레이션: 스키마 변경사항을 안전하게 버전 관리
- 관계형 데이터: 복잡한 관계도 직관적인 API로 처리
- 성능 최적화: N+1 쿼리 문제 해결과 쿼리 최적화
기존 방식의 문제점:
- SQL 쿼리 작성 시 오타나 문법 오류 발생
- 타입 정의와 데이터베이스 스키마 불일치
- 스키마 변경 시 관련 코드 누락으로 런타임 에러
Prisma 초기 설정과 스키마 설계
Next.js 프로젝트 초기 설정
# Next.js 프로젝트 생성
npx create-next-app@latest nextjs-prisma --typescript --tailwind --eslint --app
# 프로젝트 디렉토리로 이동
cd nextjs-prisma
# Prisma 설치
npm install prisma @prisma/client
npm install -D prisma
# Prisma 초기화
npx prisma init
환경 변수 설정
# .env
# PostgreSQL 예시
DATABASE_URL="postgresql://username:password@localhost:5432/mydb?schema=public"
# MySQL 예시
DATABASE_URL="mysql://username:password@localhost:3306/mydb"
# SQLite 예시 (개발용)
DATABASE_URL="file:./dev.db"
기본 스키마 설계
// 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?
avatar String?
role Role @default(USER)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// 관계형 데이터
posts Post[]
comments Comment[]
likes Like[]
@@map("users")
}
model Post {
id String @id @default(cuid())
title String
content String?
slug String @unique
published Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// 관계 설정
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
authorId String
comments Comment[]
likes Like[]
tags TagOnPost[]
@@map("posts")
}
model Comment {
id String @id @default(cuid())
content String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
authorId String
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
postId String
@@map("comments")
}
model Tag {
id String @id @default(cuid())
name String @unique
color String @default("#3b82f6")
posts TagOnPost[]
@@map("tags")
}
model TagOnPost {
id String @id @default(cuid())
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
postId String
tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade)
tagId String
@@unique([postId, tagId])
@@map("tag_on_posts")
}
model Like {
id String @id @default(cuid())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId String
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
postId String
createdAt DateTime @default(now())
@@unique([userId, postId])
@@map("likes")
}
enum Role {
USER
ADMIN
MODERATOR
}
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;
export default prisma;
첫 마이그레이션 실행
# 데이터베이스 스키마 생성
npx prisma migrate dev --name init
# Prisma Client 생성
npx prisma generate
고급 스키마 패턴과 관계 설정
복합 키와 인덱스 최적화
// prisma/schema.prisma
model UserSession {
id String @id @default(cuid())
userId String
token String @unique
expiresAt DateTime
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
// 복합 인덱스로 쿼리 성능 최적화
@@index([userId, expiresAt])
@@index([token, expiresAt])
@@map("user_sessions")
}
model PostView {
id String @id @default(cuid())
postId String
userId String?
ip String
viewedAt DateTime @default(now())
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
// 중복 방지를 위한 복합 유니크
@@unique([postId, ip, userId])
@@index([postId, viewedAt])
@@map("post_views")
}
자기 참조와 계층 구조
model Category {
id String @id @default(cuid())
name String
slug String @unique
parentId String?
// 자기 참조 관계
parent Category? @relation("CategoryHierarchy", fields: [parentId], references: [id])
children Category[] @relation("CategoryHierarchy")
posts PostCategory[]
@@map("categories")
}
model PostCategory {
id String @id @default(cuid())
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
postId String
category Category @relation(fields: [categoryId], references: [id], onDelete: Cascade)
categoryId String
@@unique([postId, categoryId])
@@map("post_categories")
}
JSON 필드와 고급 데이터 타입
model UserSettings {
id String @id @default(cuid())
userId String @unique
// JSON 필드로 유연한 설정 저장
preferences Json @default("{}")
metadata Json?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@map("user_settings")
}
model Post {
id String @id @default(cuid())
title String
content String?
// SEO 메타데이터를 JSON으로 저장
seoData Json? @default("{}")
// 배열 타입 (PostgreSQL 전용)
tags String[]
@@map("posts")
}
Prisma Client 활용과 쿼리 최적화
기본 CRUD 작업
// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server';
import prisma from '@/lib/prisma';
export async function GET() {
try {
const users = await prisma.user.findMany({
select: {
id: true,
name: true,
email: true,
role: true,
createdAt: true,
_count: {
select: {
posts: true,
comments: true,
},
},
},
orderBy: {
createdAt: 'desc',
},
take: 20,
});
return NextResponse.json(users);
} catch (error) {
console.error('사용자 조회 실패:', error);
return NextResponse.json(
{ error: '사용자 데이터를 불러올 수 없습니다.' },
{ status: 500 }
);
}
}
export async function POST(request: NextRequest) {
try {
const { name, email, role } = await request.json();
// 입력 검증
if (!name || !email) {
return NextResponse.json(
{ error: '이름과 이메일은 필수 항목입니다.' },
{ status: 400 }
);
}
const user = await prisma.user.create({
data: {
name,
email,
role: role || 'USER',
},
select: {
id: true,
name: true,
email: true,
role: true,
createdAt: true,
},
});
return NextResponse.json(user, { status: 201 });
} catch (error) {
if (error.code === 'P2002') {
return NextResponse.json(
{ error: '이미 존재하는 이메일입니다.' },
{ status: 409 }
);
}
console.error('사용자 생성 실패:', error);
return NextResponse.json(
{ error: '사용자 생성에 실패했습니다.' },
{ status: 500 }
);
}
}
관계형 데이터 조회와 Include
// app/api/posts/[id]/route.ts
import { NextRequest, NextResponse } from 'next/server';
import prisma from '@/lib/prisma';
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params;
const post = await prisma.post.findUnique({
where: { id },
include: {
author: {
select: {
id: true,
name: true,
avatar: true,
},
},
comments: {
include: {
author: {
select: {
id: true,
name: true,
avatar: true,
},
},
},
orderBy: {
createdAt: 'asc',
},
},
tags: {
include: {
tag: {
select: {
id: true,
name: true,
color: true,
},
},
},
},
_count: {
select: {
likes: true,
comments: true,
},
},
},
});
if (!post) {
return NextResponse.json(
{ error: '포스트를 찾을 수 없습니다.' },
{ status: 404 }
);
}
return NextResponse.json(post);
} catch (error) {
console.error('포스트 조회 실패:', error);
return NextResponse.json(
{ error: '포스트 데이터를 불러올 수 없습니다.' },
{ status: 500 }
);
}
}
트랜잭션과 복합 작업
// app/api/posts/route.ts
import { NextRequest, NextResponse } from 'next/server';
import prisma from '@/lib/prisma';
export async function POST(request: NextRequest) {
try {
const { title, content, tags, authorId } = await request.json();
// 트랜잭션으로 포스트와 태그 관계 생성
const result = await prisma.$transaction(async (tx) => {
// 1. 포스트 생성
const post = await tx.post.create({
data: {
title,
content,
slug: title.toLowerCase().replace(/\s+/g, '-'),
authorId,
},
});
// 2. 태그 처리 (존재하지 않으면 생성)
if (tags && tags.length > 0) {
for (const tagName of tags) {
// upsert로 태그 생성 또는 조회
const tag = await tx.tag.upsert({
where: { name: tagName },
update: {},
create: { name: tagName },
});
// 포스트-태그 관계 생성
await tx.tagOnPost.create({
data: {
postId: post.id,
tagId: tag.id,
},
});
}
}
// 3. 완성된 포스트 반환 (관계 데이터 포함)
return await tx.post.findUnique({
where: { id: post.id },
include: {
author: {
select: {
id: true,
name: true,
},
},
tags: {
include: {
tag: true,
},
},
},
});
});
return NextResponse.json(result, { status: 201 });
} catch (error) {
console.error('포스트 생성 실패:', error);
return NextResponse.json(
{ error: '포스트 생성에 실패했습니다.' },
{ status: 500 }
);
}
}
고급 쿼리 패턴
// lib/queries/posts.ts
import prisma from '@/lib/prisma';
interface PostFilters {
authorId?: string;
tags?: string[];
published?: boolean;
search?: string;
skip?: number;
take?: number;
}
export async function getFilteredPosts({
authorId,
tags,
published = true,
search,
skip = 0,
take = 10,
}: PostFilters) {
const where: any = {
published,
};
// 작성자 필터
if (authorId) {
where.authorId = authorId;
}
// 태그 필터
if (tags && tags.length > 0) {
where.tags = {
some: {
tag: {
name: {
in: tags,
},
},
},
};
}
// 검색 필터
if (search) {
where.OR = [
{
title: {
contains: search,
mode: 'insensitive',
},
},
{
content: {
contains: search,
mode: 'insensitive',
},
},
];
}
const [posts, totalCount] = await prisma.$transaction([
prisma.post.findMany({
where,
include: {
author: {
select: {
id: true,
name: true,
avatar: true,
},
},
tags: {
include: {
tag: {
select: {
name: true,
color: true,
},
},
},
},
_count: {
select: {
likes: true,
comments: true,
},
},
},
orderBy: {
createdAt: 'desc',
},
skip,
take,
}),
prisma.post.count({ where }),
]);
return {
posts,
pagination: {
total: totalCount,
pages: Math.ceil(totalCount / take),
current: Math.floor(skip / take) + 1,
hasNext: skip + take < totalCount,
hasPrev: skip > 0,
},
};
}
// 인기 포스트 조회 (좋아요 순)
export async function getPopularPosts(limit: number = 5) {
return await prisma.post.findMany({
where: {
published: true,
},
include: {
author: {
select: {
id: true,
name: true,
avatar: true,
},
},
_count: {
select: {
likes: true,
comments: true,
},
},
},
orderBy: {
likes: {
_count: 'desc',
},
},
take: limit,
});
}
실무 성능 최적화 전략
쿼리 성능 최적화
// lib/performance/query-optimization.ts
import prisma from '@/lib/prisma';
// ❌ N+1 쿼리 문제 (비효율적)
export async function getBadPostsWithAuthors() {
const posts = await prisma.post.findMany();
// 각 포스트마다 개별 쿼리 실행 (N+1 문제!)
const postsWithAuthors = await Promise.all(
posts.map(async (post) => ({
...post,
author: await prisma.user.findUnique({
where: { id: post.authorId },
}),
}))
);
return postsWithAuthors;
}
// ✅ 최적화된 쿼리 (단일 쿼리)
export async function getOptimizedPostsWithAuthors() {
return await prisma.post.findMany({
include: {
author: {
select: {
id: true,
name: true,
avatar: true,
},
},
},
});
}
// 데이터 로더 패턴 (캐싱 활용)
export class PostDataLoader {
private cache = new Map<string, any>();
async getPostsByIds(ids: string[]) {
const uncachedIds = ids.filter(id => !this.cache.has(id));
if (uncachedIds.length > 0) {
const posts = await prisma.post.findMany({
where: {
id: {
in: uncachedIds,
},
},
include: {
author: {
select: {
id: true,
name: true,
avatar: true,
},
},
},
});
posts.forEach(post => {
this.cache.set(post.id, post);
});
}
return ids.map(id => this.cache.get(id)).filter(Boolean);
}
}
커넥션 풀과 메모리 최적화
// lib/prisma-optimized.ts
import { PrismaClient } from '@prisma/client';
declare global {
var __prisma: PrismaClient | undefined;
}
const prisma = globalThis.__prisma ??
new PrismaClient({
log: process.env.NODE_ENV === 'development'
? ['query', 'error', 'warn']
: ['error'],
datasources: {
db: {
url: process.env.DATABASE_URL,
},
},
});
if (process.env.NODE_ENV !== 'production') {
globalThis.__prisma = prisma;
}
// 연결 풀 최적화
export const optimizedPrisma = prisma.$extends({
query: {
$allModels: {
async $allOperations({ operation, model, args, query }) {
const start = Date.now();
const result = await query(args);
const end = Date.now();
// 느린 쿼리 로깅 (500ms 이상)
if (end - start > 500) {
console.warn(
`Slow query detected: ${model}.${operation} took ${end - start}ms`
);
}
return result;
},
},
},
});
export default prisma;
캐싱 전략
// lib/cache/post-cache.ts
import { unstable_cache } from 'next/cache';
import prisma from '@/lib/prisma';
// Next.js 캐시 활용
export const getCachedPosts = unstable_cache(
async (page: number = 1, limit: number = 10) => {
return await prisma.post.findMany({
where: {
published: true,
},
include: {
author: {
select: {
id: true,
name: true,
avatar: true,
},
},
_count: {
select: {
likes: true,
comments: true,
},
},
},
orderBy: {
createdAt: 'desc',
},
skip: (page - 1) * limit,
take: limit,
});
},
['posts-list'],
{
revalidate: 300, // 5분 캐시
tags: ['posts'],
}
);
// 인메모리 캐시 (Redis 대안)
class MemoryCache {
private cache = new Map<string, { data: any; expiry: number }>();
set(key: string, data: any, ttl: number = 300000) { // 5분 기본값
const expiry = Date.now() + ttl;
this.cache.set(key, { data, expiry });
}
get(key: string) {
const item = this.cache.get(key);
if (!item) return null;
if (Date.now() > item.expiry) {
this.cache.delete(key);
return null;
}
return item.data;
}
clear() {
this.cache.clear();
}
}
export const memoryCache = new MemoryCache();
export async function getCachedPostById(id: string) {
const cacheKey = `post:${id}`;
let post = memoryCache.get(cacheKey);
if (!post) {
post = await prisma.post.findUnique({
where: { id },
include: {
author: {
select: {
id: true,
name: true,
avatar: true,
},
},
comments: {
include: {
author: {
select: {
id: true,
name: true,
avatar: true,
},
},
},
orderBy: {
createdAt: 'asc',
},
},
},
});
if (post) {
memoryCache.set(cacheKey, post, 600000); // 10분 캐시
}
}
return post;
}
백그라운드 작업과 큐
// lib/jobs/background-tasks.ts
import prisma from '@/lib/prisma';
interface BackgroundJob {
id: string;
type: string;
payload: any;
status: 'pending' | 'processing' | 'completed' | 'failed';
createdAt: Date;
processedAt?: Date;
error?: string;
}
// 간단한 작업 큐 시스템
class SimpleJobQueue {
private processing = false;
async addJob(type: string, payload: any) {
return await prisma.backgroundJob.create({
data: {
type,
payload,
status: 'pending',
},
});
}
async processJobs() {
if (this.processing) return;
this.processing = true;
try {
const jobs = await prisma.backgroundJob.findMany({
where: {
status: 'pending',
},
orderBy: {
createdAt: 'asc',
},
take: 5, // 한 번에 5개씩 처리
});
for (const job of jobs) {
await this.processJob(job);
}
} catch (error) {
console.error('Job processing error:', error);
} finally {
this.processing = false;
}
}
private async processJob(job: any) {
try {
await prisma.backgroundJob.update({
where: { id: job.id },
data: {
status: 'processing',
processedAt: new Date(),
},
});
// 작업 타입별 처리
switch (job.type) {
case 'send_email':
await this.sendEmail(job.payload);
break;
case 'generate_thumbnail':
await this.generateThumbnail(job.payload);
break;
case 'update_search_index':
await this.updateSearchIndex(job.payload);
break;
default:
throw new Error(`Unknown job type: ${job.type}`);
}
await prisma.backgroundJob.update({
where: { id: job.id },
data: {
status: 'completed',
},
});
} catch (error) {
await prisma.backgroundJob.update({
where: { id: job.id },
data: {
status: 'failed',
error: error.message,
},
});
}
}
private async sendEmail(payload: any) {
// 이메일 전송 로직
console.log('Sending email:', payload);
}
private async generateThumbnail(payload: any) {
// 썸네일 생성 로직
console.log('Generating thumbnail:', payload);
}
private async updateSearchIndex(payload: any) {
// 검색 인덱스 업데이트 로직
console.log('Updating search index:', payload);
}
}
export const jobQueue = new SimpleJobQueue();
// 주기적 작업 실행
if (process.env.NODE_ENV === 'production') {
setInterval(() => {
jobQueue.processJobs();
}, 30000); // 30초마다 실행
}
배포와 프로덕션 환경 설정
Docker를 활용한 배포 설정
# Dockerfile
FROM node:18-alpine AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --only=production
FROM node:18-alpine AS builder
WORKDIR /app
COPY /app/node_modules ./node_modules
COPY . .
# Prisma 클라이언트 생성
RUN npx prisma generate
# Next.js 빌드
RUN npm run build
FROM node:18-alpine AS runner
WORKDIR /app
ENV NODE_ENV production
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY /app/public ./public
COPY /app/.next/standalone ./
COPY /app/.next/static ./.next/static
# Prisma 설정 복사
COPY /app/prisma ./prisma
COPY /app/node_modules/.prisma ./node_modules/.prisma
USER nextjs
EXPOSE 3000
ENV PORT 3000
# 마이그레이션 실행 후 서버 시작
CMD ["sh", "-c", "npx prisma migrate deploy && node server.js"]
환경별 설정 관리
// lib/config/database.ts
interface DatabaseConfig {
url: string;
maxConnections: number;
connectionTimeout: number;
logLevel: string[];
}
const configs: Record<string, DatabaseConfig> = {
development: {
url: process.env.DATABASE_URL!,
maxConnections: 10,
connectionTimeout: 10000,
logLevel: ['query', 'error', 'warn'],
},
test: {
url: process.env.TEST_DATABASE_URL!,
maxConnections: 5,
connectionTimeout: 5000,
logLevel: ['error'],
},
production: {
url: process.env.DATABASE_URL!,
maxConnections: 50,
connectionTimeout: 20000,
logLevel: ['error'],
},
};
export const databaseConfig = configs[process.env.NODE_ENV || 'development'];
모니터링과 로깅
// lib/monitoring/prisma-metrics.ts
import { PrismaClient } from '@prisma/client';
export const instrumentedPrisma = new PrismaClient({
log: [
{
emit: 'event',
level: 'query',
},
{
emit: 'event',
level: 'error',
},
{
emit: 'event',
level: 'info',
},
{
emit: 'event',
level: 'warn',
},
],
});
// 쿼리 성능 모니터링
instrumentedPrisma.$on('query', (e) => {
if (e.duration > 1000) { // 1초 이상 걸리는 쿼리
console.warn('Slow query detected:', {
query: e.query,
params: e.params,
duration: `${e.duration}ms`,
timestamp: e.timestamp,
});
}
});
// 에러 로깅
instrumentedPrisma.$on('error', (e) => {
console.error('Prisma error:', {
message: e.message,
target: e.target,
timestamp: e.timestamp,
});
});
// 연결 상태 모니터링
let connectionCount = 0;
instrumentedPrisma.$on('info', (e) => {
if (e.message.includes('connection')) {
connectionCount++;
console.info(`Database connections: ${connectionCount}`);
}
});
CI/CD 파이프라인 설정
# .github/workflows/deploy.yml
name: Deploy to Production
on:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:14
env:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: test
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Generate Prisma Client
run: npx prisma generate
- name: Run migrations
run: npx prisma migrate deploy
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test
- name: Run tests
run: npm test
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test
deploy:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v3
- name: Deploy to Vercel
uses: amondnet/vercel-action@v20
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.ORG_ID }}
vercel-project-id: ${{ secrets.PROJECT_ID }}
vercel-args: '--prod'
자주 묻는 질문 (FAQ)
Q1: Prisma와 다른 ORM(TypeORM, Sequelize)과의 차이점은 무엇인가요?
A: Prisma는 스키마 우선 접근법으로 타입 안전성이 뛰어나고, 자동 타입 생성과 마이그레이션 시스템이 더 직관적입니다. 특히 TypeScript 통합이 가장 우수해요.
Q2: N+1 쿼리 문제를 어떻게 해결하나요?
A: include
나 select
를 사용해서 관련 데이터를 한 번에 조회하거나, DataLoader 패턴을 활용하세요. 대부분의 경우 include
만으로도 충분히 해결됩니다.
Q3: Prisma Client를 여러 곳에서 사용할 때 연결 풀 문제가 있나요?
A: 글로벌 인스턴스를 사용하고, 개발 환경에서만 전역 변수에 저장하는 패턴을 권장합니다. 프로덕션에서는 연결 풀이 자동으로 관리되어요.
Q4: 복잡한 쿼리는 어떻게 처리하나요?
A: Prisma의 $queryRaw
나 $executeRaw
를 사용해서 직접 SQL을 작성할 수 있습니다. 하지만 대부분의 경우 Prisma의 쿼리 빌더만으로도 충분해요.
Q5: 테스트 환경에서 데이터베이스는 어떻게 관리하나요?
A: 별도의 테스트 데이터베이스를 사용하고, 각 테스트 후에 데이터를 정리하는 패턴을 권장합니다. prisma migrate reset
을 활용하면 편리해요.
❓ Next.js + Prisma 마스터 마무리
Next.js와 Prisma의 조합은 현대 웹 개발에서 가장 강력하고 생산성 높은 풀스택 솔루션입니다. 타입 안전성부터 성능 최적화까지 개발자가 원하는 모든 것을 제공해주죠.
특히 프론트엔드 웹 개발에서 백엔드 로직을 효율적으로 관리하고 싶은 개발자들에게는 필수적인 기술 스택이에요. 스키마 설계부터 배포까지 전 과정에서 개발자 경험을 극대화하면서도 높은 성능을 보장할 수 있습니다!
Next.js 고급 기법 더 배우고 싶다면 Next.js Middleware 활용법와 Next.js 배포 최적화 전략을 꼭 확인해보세요! 💪
🔗 Next.js + Prisma 심화 학습 시리즈
Prisma ORM 마스터가 되셨다면, 다른 고급 기능들도 함께 학습해보세요:
📚 다음 단계 학습 가이드
- Next.js 15 완전 정복 가이드: React 19와 Turbopack으로 극한 성능 최적화
- Next.js Middleware 활용법: 인증/권한/리다이렉트 로직 구현 마스터
- Next.js 배포와 최적화 전략: Vercel vs AWS 비교 분석과 성능 튜닝 실전
- Next.js API Routes 마스터: 실무에서 3년간 써본 완전 정복 가이드
- Next.js SEO 최적화 가이드: 메타데이터 관리와 사이트맵 자동화 전략