Node
Nest.js
Prisma

NestJS와 Prisma 통합 가이드

결론

NestJS와 Prisma를 통합하는 핵심 방법은 PrismaService를 생성하여 NestJS의 의존성 주입 시스템에 등록하는 것입니다. PrismaClient를 확장한 서비스를 만들어 모듈 초기화 시 데이터베이스 연결을 자동으로 관리하고, 이를 애플리케이션 전체에서 주입받아 사용합니다.

Prisma 공식 문서에서 "Prisma integrates seamlessly with the NestJS framework. Prisma is used inside NestJS services via dependency injection" 이라고 명시되어 있습니다.

LogRocket 가이드에서 "The PrismaService extends PrismaClient and implements OnModuleInit to establish the database connection when the module is initialized" 이라고 설명하고 있습니다.

설치 및 초기 설정

1. 패키지 설치

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

2. Prisma 초기화

npx prisma init

이 명령어는 prisma 디렉토리와 .env 파일을 생성합니다 Prisma NestJS REST API 가이드 (opens in a new tab)

Prisma 공식 블로그에서 "Running npx prisma init creates a new prisma directory with a schema.prisma file and a .env file in the root of the project" 이라고 명시되어 있습니다.

3. 데이터베이스 연결 설정

.env 파일에 데이터베이스 URL을 설정합니다:

DATABASE_URL="postgresql://user:password@localhost:5432/mydb?schema=public"

4. 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?
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

5. 마이그레이션 실행

npx prisma migrate dev --name init

이 명령어는 데이터베이스 스키마를 생성하고 Prisma Client를 자동으로 생성합니다.

PrismaService 구현

PrismaService 생성

src/prisma/prisma.service.ts 파일을 생성합니다:

import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
 
@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
  async onModuleInit() {
    await this.$connect();
  }
 
  async onModuleDestroy() {
    await this.$disconnect();
  }
}
  • onModuleInit(): 모듈 초기화 시 데이터베이스에 연결합니다
  • onModuleDestroy(): 애플리케이션 종료 시 연결을 정리합니다

NestJS Prisma 통합 가이드에서 "The PrismaService is responsible for instantiating a PrismaClient instance and connecting to your database. The $connect() call happens in the onModuleInit hook" 이라고 설명하고 있습니다.

PrismaModule 생성

src/prisma/prisma.module.ts 파일을 생성합니다:

import { Module, Global } from '@nestjs/common';
import { PrismaService } from './prisma.service';
 
@Global()
@Module({
  providers: [PrismaService],
  exports: [PrismaService],
})
export class PrismaModule {}
  • @Global() 데코레이터를 사용하면 다른 모듈에서 PrismaModule을 import하지 않아도 PrismaService를 주입받을 수 있습니다
  • exports에 PrismaService를 추가하여 다른 모듈에서 사용 가능하게 합니다

AppModule에 등록

import { Module } from '@nestjs/common';
import { PrismaModule } from './prisma/prisma.module';
 
@Module({
  imports: [PrismaModule],
})
export class AppModule {}

실제 사용 예시

Service에서 PrismaService 주입

NestJS의 의존성 주입을 통해 PrismaService를 사용합니다:

import { Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { User, Prisma } from '@prisma/client';
 
@Injectable()
export class UserService {
  constructor(private prisma: PrismaService) {}
 
  async findAll(): Promise<User[]> {
    return this.prisma.user.findMany();
  }
 
  async findOne(id: number): Promise<User | null> {
    return this.prisma.user.findUnique({
      where: { id },
    });
  }
 
  async create(data: Prisma.UserCreateInput): Promise<User> {
    return this.prisma.user.create({
      data,
    });
  }
 
  async update(id: number, data: Prisma.UserUpdateInput): Promise<User> {
    return this.prisma.user.update({
      where: { id },
      data,
    });
  }
 
  async delete(id: number): Promise<User> {
    return this.prisma.user.delete({
      where: { id },
    });
  }
}
  • Prisma는 자동으로 타입 안전한 쿼리 인터페이스를 생성합니다
  • Prisma.UserCreateInput, Prisma.UserUpdateInput 등의 타입을 활용하여 컴파일 타임 안정성을 확보합니다

Controller에서 사용

import { Controller, Get, Post, Body, Param, Put, Delete } from '@nestjs/common';
import { UserService } from './user.service';
import { User, Prisma } from '@prisma/client';
 
@Controller('users')
export class UserController {
  constructor(private readonly userService: UserService) {}
 
  @Get()
  findAll(): Promise<User[]> {
    return this.userService.findAll();
  }
 
  @Get(':id')
  findOne(@Param('id') id: string): Promise<User | null> {
    return this.userService.findOne(Number(id));
  }
 
  @Post()
  create(@Body() createUserDto: Prisma.UserCreateInput): Promise<User> {
    return this.userService.create(createUserDto);
  }
 
  @Put(':id')
  update(
    @Param('id') id: string,
    @Body() updateUserDto: Prisma.UserUpdateInput,
  ): Promise<User> {
    return this.userService.update(Number(id), updateUserDto);
  }
 
  @Delete(':id')
  delete(@Param('id') id: string): Promise<User> {
    return this.userService.delete(Number(id));
  }
}

고급 기능

트랜잭션

Prisma는 세 가지 트랜잭션 방식을 제공합니다 NestJS Prisma Transactions 가이드 (opens in a new tab)

1. 중첩된 쓰기 (Nested Writes)

관련된 여러 레코드를 한 번에 생성합니다:

async createUserWithProfile(data: CreateUserDto) {
  return this.prisma.user.create({
    data: {
      email: data.email,
      name: data.name,
      profile: {
        create: {
          bio: data.bio,
          avatar: data.avatar,
        },
      },
    },
    include: { profile: true },
  });
}

Wanago 가이드에서 "If any of the operations fail, Prisma automatically rolls back the entire transaction" 이라고 명시되어 있습니다.

2. 순차 작업 (Sequential Operations)

여러 독립적인 작업을 하나의 트랜잭션으로 묶습니다:

async deleteUserAndPosts(userId: number) {
  return this.prisma.$transaction([
    this.prisma.post.deleteMany({
      where: { authorId: userId },
    }),
    this.prisma.user.delete({
      where: { id: userId },
    }),
  ]);
}
  • 모든 작업이 성공하거나 전체가 롤백됩니다
  • 배열의 순서대로 실행됩니다
3. 대화형 트랜잭션 (Interactive Transactions)

중간 결과에 따라 조건부 로직을 구현합니다:

async transferFunds(fromId: number, toId: number, amount: number) {
  return this.prisma.$transaction(async (tx) => {
    // 출금
    const sender = await tx.account.update({
      where: { id: fromId },
      data: { balance: { decrement: amount } },
    });
 
    // 잔액 확인
    if (sender.balance < 0) {
      throw new Error('잔액 부족');
    }
 
    // 입금
    await tx.account.update({
      where: { id: toId },
      data: { balance: { increment: amount } },
    });
 
    return { success: true };
  });
}

Wanago 가이드에서 "Interactive transactions allow us to pass a function to the $transaction method. Its argument is an instance of a Prisma client. Each use of this client is encapsulated in a transaction" 이라고 설명하고 있습니다.

에러 핸들링

Prisma의 에러를 NestJS의 HTTP 예외로 변환합니다:

import { Injectable, NotFoundException, ConflictException } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { Prisma } from '@prisma/client';
 
@Injectable()
export class UserService {
  constructor(private prisma: PrismaService) {}
 
  async findOne(id: number) {
    const user = await this.prisma.user.findUnique({
      where: { id },
    });
 
    if (!user) {
      throw new NotFoundException(`User with ID ${id} not found`);
    }
 
    return user;
  }
 
  async create(data: Prisma.UserCreateInput) {
    try {
      return await this.prisma.user.create({ data });
    } catch (error) {
      if (error instanceof Prisma.PrismaClientKnownRequestError) {
        // 유니크 제약 조건 위반
        if (error.code === 'P2002') {
          throw new ConflictException('Email already exists');
        }
      }
      throw error;
    }
  }
}

관계 데이터 조회

Prisma의 includeselect를 활용하여 관계 데이터를 효율적으로 조회합니다:

// 사용자와 게시글을 함께 조회
async findUserWithPosts(userId: number) {
  return this.prisma.user.findUnique({
    where: { id: userId },
    include: {
      posts: true,
    },
  });
}
 
// 특정 필드만 선택
async findUserProfile(userId: number) {
  return this.prisma.user.findUnique({
    where: { id: userId },
    select: {
      id: true,
      email: true,
      name: true,
      posts: {
        select: {
          id: true,
          title: true,
          published: true,
        },
      },
    },
  });
}

동적 쿼리 조건 구성

검색 필터나 옵션 파라미터처럼 런타임에 조건이 결정되는 경우, where 절을 동적으로 구성해야 합니다. Prisma는 두 가지 주요 패턴을 제공합니다 Prisma 공식 문서 - Filtering and Sorting (opens in a new tab)

Prisma 공식 문서에서 "Prisma Client supports filtering with a wide range of operators that you can use to control the records returned by your query. Operators include OR, NOT, contains, startsWith, and many more" 이라고 명시되어 있습니다.

1. Spread Operator 패턴 (권장)

가장 일반적이고 타입 안전한 방법입니다 How to do conditional 'where' statements in Prisma (opens in a new tab)

Brockherion 블로그에서 "The solution is to conditionally add your where statement to your query by taking advantage of the spread operator and conditional statements" 이라고 명시되어 있습니다.

async searchUsers(filters?: { email?: string; name?: string; isActive?: boolean }) {
  return this.prisma.user.findMany({
    where: {
      ...(filters?.email ? { email: { contains: filters.email } } : {}),
      ...(filters?.name ? { name: { contains: filters.name } } : {}),
      ...(filters?.isActive !== undefined ? { isActive: filters.isActive } : {}),
    },
  });
}
  • 조건이 없으면 빈 객체 {}가 spread되어 무시됩니다
  • TypeScript의 타입 체크가 정상적으로 작동합니다
  • 코드가 간결하고 읽기 쉽습니다
2. 객체 직접 조작 패턴

복잡한 조건 조합이 필요한 경우 where 객체를 직접 구성합니다 Stack Overflow - Conditional where clause (opens in a new tab)

Stack Overflow에서 "You can build a custom query based on conditions by creating a filter object and only adding properties when they exist" 이라고 설명하고 있습니다.

async searchPosts(query?: string, authorId?: number, published?: boolean) {
  const whereInput: Prisma.PostWhereInput = {};
 
  if (query) {
    whereInput.title = { contains: query };
  }
 
  if (authorId) {
    whereInput.authorId = authorId;
  }
 
  if (published !== undefined) {
    whereInput.published = published;
  }
 
  return this.prisma.post.findMany({
    where: whereInput,
  });
}
  • Prisma.PostWhereInput 타입을 명시하여 타입 안전성을 확보합니다
  • 조건부 로직이 명확하게 분리되어 가독성이 높습니다
  • 복잡한 비즈니스 로직을 처리하기 적합합니다
3. OR 조건 동적 구성

여러 필드 중 하나라도 일치하는 경우를 찾을 때 OR 배열을 사용합니다 Prisma 공식 문서 - Filtering and Sorting (opens in a new tab)

Prisma 공식 문서에서 "Use the OR operator to return records that match one or more conditions" 이라고 명시되어 있습니다.

async searchByMultipleFields(query: string) {
  const whereInput: Prisma.UserWhereInput = {
    OR: [
      { email: { contains: query } },
      { name: { contains: query } },
      { phone: { contains: query } },
    ],
  };
 
  return this.prisma.user.findMany({
    where: whereInput,
  });
}
4. 복잡한 조건 조합 (AND + OR)

실무에서는 여러 조건을 중첩하여 사용해야 하는 경우가 많습니다 DEV Community - Dynamic Filtering (opens in a new tab)

DEV Community에서 "Dynamic filtering can be implemented by manipulating Prisma's findMany query to be more flexible, building filter queries using objects with AND arrays" 이라고 명시되어 있습니다.

async advancedSearch(params: {
  query?: string;
  category?: string;
  minPrice?: number;
  maxPrice?: number;
}) {
  const whereInput: Prisma.ProductWhereInput = {};
 
  // AND 조건 구성
  const andConditions: Prisma.ProductWhereInput[] = [];
 
  // 검색어가 있으면 이름 또는 설명에서 검색 (OR)
  if (params.query) {
    andConditions.push({
      OR: [
        { name: { contains: params.query } },
        { description: { contains: params.query } },
      ],
    });
  }
 
  // 카테고리 필터
  if (params.category) {
    andConditions.push({
      category: { equals: params.category },
    });
  }
 
  // 가격 범위 필터
  if (params.minPrice !== undefined || params.maxPrice !== undefined) {
    andConditions.push({
      price: {
        ...(params.minPrice !== undefined ? { gte: params.minPrice } : {}),
        ...(params.maxPrice !== undefined ? { lte: params.maxPrice } : {}),
      },
    });
  }
 
  // AND 조건이 있으면 추가
  if (andConditions.length > 0) {
    whereInput.AND = andConditions;
  }
 
  return this.prisma.product.findMany({
    where: whereInput,
  });
}
5. 관계 데이터에서 동적 조건 사용

중첩된 관계에서도 동적 조건을 적용할 수 있습니다:

async findUsersWithMatchingPosts(postQuery?: string, userQuery?: string) {
  const whereInput: Prisma.UserWhereInput = {};
 
  // 사용자 이름 조건
  if (userQuery) {
    whereInput.name = { contains: userQuery };
  }
 
  // 게시글 조건 (관계 필터링)
  if (postQuery) {
    whereInput.posts = {
      some: {
        OR: [
          { title: { contains: postQuery } },
          { content: { contains: postQuery } },
        ],
      },
    };
  }
 
  return this.prisma.user.findMany({
    where: whereInput,
    include: {
      posts: true,
    },
  });
}
  • some: 관계된 레코드 중 하나라도 조건을 만족하면 포함
  • every: 모든 관계된 레코드가 조건을 만족해야 포함
  • none: 조건을 만족하는 관계된 레코드가 없어야 포함
베스트 프랙티스
  1. 타입 안전성 유지: 항상 Prisma.ModelWhereInput 타입을 명시합니다
// 좋은 예
const whereInput: Prisma.UserWhereInput = {};
 
// 나쁜 예
const whereInput: any = {};
  1. 빈 조건 처리: 모든 필터가 비어있을 때의 동작을 고려합니다
async searchUsers(filters?: SearchFilters) {
  // 필터가 없으면 모든 사용자 반환
  if (!filters || Object.keys(filters).length === 0) {
    return this.prisma.user.findMany();
  }
 
  // ... 필터링 로직
}
  1. 재사용 가능한 필터 함수: 공통 필터 로직을 함수로 분리합니다
private buildUserSearchFilter(query?: string): Prisma.UserWhereInput {
  if (!query) return {};
 
  return {
    OR: [
      { email: { contains: query, mode: 'insensitive' } },
      { name: { contains: query, mode: 'insensitive' } },
    ],
  };
}
 
async searchUsers(query?: string) {
  return this.prisma.user.findMany({
    where: this.buildUserSearchFilter(query),
  });
}
  1. 대소문자 구분 없는 검색: mode: 'insensitive' 옵션을 활용합니다 (PostgreSQL, MongoDB 지원)
where: {
  email: {
    contains: query,
    mode: 'insensitive', // 대소문자 구분 없이 검색
  },
}

모범 사례

1. DTO 사용

Prisma의 자동 생성 타입 대신 명시적인 DTO를 사용하여 API 계층을 명확하게 분리합니다:

// create-user.dto.ts
export class CreateUserDto {
  email: string;
  name?: string;
}
 
// user.service.ts
async create(createUserDto: CreateUserDto): Promise<User> {
  return this.prisma.user.create({
    data: createUserDto,
  });
}

2. 환경별 설정 분리

개발, 테스트, 프로덕션 환경별로 데이터베이스 연결을 분리합니다:

// .env.development
DATABASE_URL="postgresql://dev:dev@localhost:5432/mydb_dev"
 
// .env.production
DATABASE_URL="postgresql://prod:prod@prod-host:5432/mydb_prod"
 
// .env.test
DATABASE_URL="postgresql://test:test@localhost:5432/mydb_test"

3. 시딩을 통한 개발 데이터 관리

prisma/seed.ts 파일을 통해 개발 환경의 초기 데이터를 관리합니다 Prisma NestJS REST API 가이드 (opens in a new tab):

import { PrismaClient } from '@prisma/client';
 
const prisma = new PrismaClient();
 
async function main() {
  const user = await prisma.user.upsert({
    where: { email: 'test@example.com' },
    update: {},
    create: {
      email: 'test@example.com',
      name: 'Test User',
    },
  });
 
  console.log({ user });
}
 
main()
  .catch((e) => {
    console.error(e);
    process.exit(1);
  })
  .finally(async () => {
    await prisma.$disconnect();
  });

package.json에 seed 스크립트를 추가합니다:

{
  "prisma": {
    "seed": "ts-node prisma/seed.ts"
  }
}

실행:

npx prisma db seed

4. Swagger 문서화

NestJS의 Swagger 통합을 활용하여 API를 자동으로 문서화합니다:

import { ApiTags, ApiOkResponse } from '@nestjs/swagger';
 
@ApiTags('users')
@Controller('users')
export class UserController {
  @Get()
  @ApiOkResponse({ type: [User] })
  findAll(): Promise<User[]> {
    return this.userService.findAll();
  }
}

Prisma 공식 블로그에서 "SwaggerModule automatically generates API documentation from your controllers and DTOs" 이라고 명시되어 있습니다.

요약

NestJS와 Prisma의 조합은 타입 안전성과 생산성을 극대화하는 강력한 백엔드 스택입니다. PrismaService를 중심으로 의존성 주입을 활용하면 깔끔하고 유지보수 가능한 코드를 작성할 수 있습니다.

핵심 포인트:

  • PrismaService를 전역 모듈로 등록하여 애플리케이션 전체에서 사용
  • 트랜잭션을 활용하여 데이터 무결성 보장
  • DTO와 타입을 활용하여 타입 안전성 확보
  • 환경별 설정 분리와 시딩으로 개발 효율성 향상

Prisma Migration Guide

결론

Prisma Migrate는 데이터베이스 스키마를 버전 관리하고 안전하게 변경사항을 적용할 수 있는 선언적 마이그레이션 도구입니다. Prisma 공식 문서 (opens in a new tab)에 따르면, Prisma Schema 파일의 변경사항을 기반으로 자동으로 SQL 마이그레이션 파일을 생성하고 적용합니다.

Prisma 공식 문서에서 "Prisma Migrate는 개발 환경에서 스키마 마이그레이션을 시작하는 방법을 제공하며, 마이그레이션 히스토리를 migrations/ 폴더에 생성한다"고 명시되어 있습니다.

2025년 1월 9일에 출시된 Prisma ORM 6.2.0 (opens in a new tab)은 가장 많이 요청된 기능인 omit API를 정식(GA)으로 제공합니다.

기본 워크플로우

1. 새 프로젝트에서 시작하기

새로운 프로젝트에서 Prisma Migrate를 사용하는 워크플로우는 세 단계로 구성됩니다. Prisma 공식 문서 (opens in a new tab)

1단계: Prisma Schema 정의

model User {
  id    Int    @id @default(autoincrement())
  email String @unique
  name  String?
}
 
model Post {
  id        Int      @id @default(autoincrement())
  title     String
  content   String?
  published Boolean  @default(false)
  authorId  Int
  author    User     @relation(fields: [authorId], references: [id])
}

2단계: 초기 마이그레이션 생성

prisma migrate dev --name init

Prisma 공식 문서에서 "prisma migrate dev --name init 명령으로 초기 마이그레이션을 생성하며, 결과적으로 migrations/ 폴더에 마이그레이션 히스토리가 생성된다"고 명시되어 있습니다.

3단계: 스키마 수정 후 마이그레이션

스키마를 수정한 후 다시 마이그레이션을 실행합니다.

prisma migrate dev --name added_job_title

2. 기존 프로젝트에 추가하기

이미 운영 중인 데이터베이스가 있는 경우, 기존 데이터를 유지하면서 Prisma Migrate를 도입할 수 있습니다. Prisma 공식 문서 (opens in a new tab)

1단계: 데이터베이스 스키마 가져오기(Introspection)

prisma db pull

2단계: 베이스라인 마이그레이션 생성

prisma migrate diff \
  --from-empty \
  --to-schema-datamodel prisma/schema.prisma \
  --script > migrations/0_init/migration.sql

Prisma 공식 문서에서 "0_ 접두사는 렉시코그래픽 순서 적용을 위해 중요하다"고 명시되어 있습니다.

3단계: 마이그레이션을 적용된 것으로 표시

prisma migrate resolve --applied 0_init

주요 명령어

명령어목적설명
prisma migrate dev개발 환경 마이그레이션스키마 변경사항을 감지하고 마이그레이션 파일을 생성한 후 데이터베이스에 적용
prisma db pull스키마 동기화기존 데이터베이스 구조를 Prisma Schema로 가져오기
prisma migrate diff마이그레이션 SQL 생성두 스키마 상태 간의 차이를 SQL로 생성
prisma migrate resolve마이그레이션 상태 관리마이그레이션의 적용 상태를 수동으로 표시

Prisma 공식 문서 (opens in a new tab)에서 확인할 수 있습니다.

베스트 프랙티스 (2025년 최신)

1. 비파괴적 변경(Non-Breaking Changes)

새로운 필드를 추가할 때는 반드시 기본값을 설정하여 기존 데이터를 보호해야 합니다. Wasp 블로그 - 2025년 4월 2일 (opens in a new tab)

model User {
  id       Int     @id @default(autoincrement())
  email    String  @unique
  name     String?
  jobTitle String? @default("미지정")  // 기본값 설정으로 기존 데이터 보호
}

Wasp 블로그에서 "기본값 설정이 필수: 기존 데이터에 새 필드값을 제공하고, 선택적 필드(@default)로 표시하여 기존 행 영향을 최소화해야 한다"고 명시되어 있습니다.

2. Expand & Contract 패턴

파괴적 변경이 필요한 경우, 4단계로 나누어 안전하게 진행합니다. Wasp 블로그 - 2025년 4월 2일 (opens in a new tab)

단계 1: Expand - 새 필드 추가

model User {
  id         Int     @id @default(autoincrement())
  name       String?  // 기존 필드
  firstName  String?  // 새 필드
  lastName   String?  // 새 필드
}

애플리케이션 코드를 수정하여 기존 필드와 새 필드 모두에 데이터를 기록합니다.

단계 2: 데이터 마이그레이션

기존 name 데이터를 firstNamelastName으로 분리하는 스크립트를 실행합니다.

단계 3: 읽기 로직 변경

애플리케이션이 새 필드에서만 데이터를 읽도록 코드를 수정합니다.

단계 4: Contract - 구 필드 제거

model User {
  id        Int     @id @default(autoincrement())
  firstName String?
  lastName  String?
  // name 필드 제거
}

Wasp 블로그에서 "복잡한 변경사항은 여러 배포로 분리하여 '사용자의 다운타임 없이' 안전한 스키마 진화를 달성할 수 있다"고 명시되어 있습니다.

3. 필수 안전 수칙

Wasp 블로그 - 2025년 4월 2일 (opens in a new tab)에서 제시하는 필수 베스트 프랙티스:

  • 마이그레이션을 통한 스키마 변경: 로컬/프로덕션 환경의 일관성 보장
  • 합리적인 기본값 설정: 새 필드 추가 시 기존 데이터 보호
  • 다단계 접근: 복잡한 변경사항은 여러 배포로 분리
  • 데이터베이스 백업: 마이그레이션 전 필수 수행

Wasp 블로그에서 "데이터베이스 마이그레이션은 전기를 끄지 않은 상태에서 천장에 새로운 램프를 달아야 하는 작업과 같다"고 비유했습니다.

Prisma 6 업그레이드 시 고려사항

2025년 기준 최신 버전인 Prisma ORM 6을 사용하는 경우 다음 사항을 확인해야 합니다. Prisma 6 업그레이드 가이드 (opens in a new tab)

최소 요구사항

  • Node.js 버전
    • Node.js 18: 18.18.0 이상
    • Node.js 20: 20.9.0 이상
    • Node.js 22: 22.11.0 이상
  • TypeScript: 5.1.0 이상

주요 Breaking Changes

1. PostgreSQL m-n 관계 변경

Prisma 6 업그레이드 가이드에서 "Unique index가 primary key로 변경되어 관계 테이블 구조가 수정되므로, 업그레이드 직후 마이그레이션을 실행할 것을 권장한다"고 명시되어 있습니다.

업그레이드 후 반드시 마이그레이션을 실행하세요:

npm install @prisma/client@6
npm install -D prisma@6
prisma migrate dev

2. Buffer에서 Uint8Array로 전환

Bytes 필드에서 Node.js 특화 API가 표준 JavaScript API로 변경되었습니다.

3. 예약어 추가

모델명으로 async, await, using을 사용할 수 없게 되었습니다.

주의사항

1. 마이그레이션 폴더 관리

Prisma 공식 문서 (opens in a new tab)에 따르면:

"마이그레이션 폴더를 소스 컨트롤에 커밋하는 것이 필수"라고 명시되어 있습니다.

git add migrations/
git commit -m "Add initial migration"

2. 프로덕션 배포

개발 환경에서 사용하는 prisma migrate dev 명령은 프로덕션에서 사용하면 안 됩니다. 프로덕션 배포에 대한 자세한 내용은 Prisma 공식 문서의 배포 가이드 (opens in a new tab)를 참고하세요.

3. 미지원 기능 처리

Prisma가 아직 지원하지 않는 데이터베이스 기능(부분 인덱스 등)은 생성된 SQL 파일을 수동으로 수정해야 합니다. Prisma 공식 문서 (opens in a new tab)

참고 자료

Prisma Multi-File Schema로 도메인별 스키마 작성하기

결론

Prisma 공식 블로그에서 "The multi-file schema feature went into General Availability in Prisma ORM v6.7.0 in June 2025. You no longer need to include it in the previewFeatures" 라고 명시되어 있습니다.

Prisma 공식 블로그에서 "It helps if your schema has large models or complex relations, as putting related models into a separate file can help make your schema easier to understand and navigate. When you have a large team with many developers committing, separating a large schema into several files can help reduce merge conflicts" 라고 명시되어 있습니다.

기본 설정

버전 요구사항

  • Prisma 5.15.0 이상: preview feature로 사용 가능 (previewFeatures에 prismaSchemaFolder 추가 필요)
  • Prisma 6.7.0 이상: 정식 기능으로 사용 가능 (preview feature 설정 불필요)

폴더 구조 설정

package.json에 스키마 폴더 경로를 지정합니다 GitHub Discussions (opens in a new tab)

GitHub Discussions에서 "The schema folder path should be a directory, not individual files. package.json's 'schema': './prisma/schema' (folder) is correct, individual file paths will cause errors" 라고 명시되어 있습니다.

{
  "prisma": {
    "schema": "./prisma/schema"
  }
}

주의: 개별 파일 경로가 아닌 디렉토리 경로를 지정해야 합니다.

디렉토리 구조

prisma/
├── schema/
│   ├── base.prisma       ## datasource, generator 정의
│   ├── user.prisma       ## User 도메인 모델
│   ├── post.prisma       ## Post 도메인 모델
│   └── comment.prisma    ## Comment 도메인 모델
└── migrations/           ## 마이그레이션 파일

중요: prisma/schema.prisma 파일이 존재하면 안 됩니다 GitHub Discussions (opens in a new tab)

GitHub Discussions에서 "All split schemas must be in the prisma/schema folder and the prisma/schema.prisma file must not exist" 라고 명시되어 있습니다.

Generator 설정

Prisma 6.7.0 이상을 사용하는 경우:

// prisma/schema/base.prisma
generator client {
  provider = "prisma-client-js"
}
 
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

Prisma 5.15.0 ~ 6.6.x를 사용하는 경우:

generator client {
  provider        = "prisma-client-js"
  previewFeatures = ["prismaSchemaFolder"]
}

도메인별 스키마 작성 방법

기본 원칙

  1. 관련 모델을 동일 파일에 그룹화: User와 관련된 모델들은 user.prisma에 작성합니다 Prisma 공식 블로그 (opens in a new tab)

Prisma 공식 블로그에서 "Group related models in the same file (e.g., user-related models in user.prisma)" 라고 명시되어 있습니다.

  1. 단일 파일에 설정 집중: datasourcegenerator 블록은 하나의 파일(예: base.prisma)에만 정의합니다

  2. 명확한 네이밍: 파일명은 도메인을 명확하게 표현해야 합니다

파일 분리 전략

관계가 있는 모델들도 서로 다른 파일에 정의할 수 있습니다. Prisma는 모든 파일을 자동으로 병합하므로 별도의 import가 필요하지 않습니다 Prisma 공식 블로그 (opens in a new tab)

Prisma 공식 블로그에서 "Prisma handles the combining of files, allowing you to define a model in one file and then use it in other schema files without the need for importing" 라고 명시되어 있습니다.

코드 예시

prisma/schema/base.prisma

generator client {
  provider = "prisma-client-js"
}
 
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

prisma/schema/user.prisma

model User {
  id        Int      @id @default(autoincrement())
  email     String   @unique
  name      String
  posts     Post[]
  comments  Comment[]
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

prisma/schema/post.prisma

model Post {
  id        Int       @id @default(autoincrement())
  title     String
  content   String
  published Boolean   @default(false)
  authorId  Int
  author    User      @relation(fields: [authorId], references: [id])
  comments  Comment[]
  createdAt DateTime  @default(now())
  updatedAt DateTime  @updatedAt
}

prisma/schema/comment.prisma

model Comment {
  id        Int      @id @default(autoincrement())
  content   String
  postId    Int
  post      Post     @relation(fields: [postId], references: [id])
  authorId  Int
  author    User     @relation(fields: [authorId], references: [id])
  createdAt DateTime @default(now())
}

CLI 명령어 지원

모든 Prisma CLI 명령어가 multi-file schema를 지원합니다 Prisma 공식 블로그 (opens in a new tab)

Prisma 공식 블로그에서 "When running prisma generate all schema files are combined, and other Prisma CLI commands, such as validate and format have also been updated to work with multi-file Prisma Schemas" 라고 명시되어 있습니다.

## 모든 스키마 파일을 병합하여 클라이언트 생성
npx prisma generate
 
## 모든 스키마 파일 포맷팅
npx prisma format
 
## 스키마 검증
npx prisma validate
 
## 마이그레이션 생성
npx prisma migrate dev

주의사항

VSCode 확장 기능 제한

VSCode Prisma 확장 기능은 여러 파일에 걸친 필드 자동 완성이 제한적입니다 GitHub Discussions (opens in a new tab)

GitHub Discussions에서 "VSCode extension doesn't automatically add fields across multiple files. This can be resolved with the prisma format command" 라고 명시되어 있습니다.

해결 방법: npx prisma format 명령어를 사용하여 수동으로 포맷팅합니다.

마이그레이션 경로

스키마 폴더 경로는 마이그레이션 디렉토리도 포함해야 합니다. 기존 마이그레이션이 있다면 충돌이 발생할 수 있습니다 GitHub Discussions (opens in a new tab)

GitHub Discussions에서 "The specified schema folder path must also contain the migrations directory. If there are existing migrations, conflicts may occur" 라고 명시되어 있습니다.

db pull 제한

prisma db pull 명령어는 자동으로 스키마를 여러 파일로 분할하지 않습니다 GitHub Discussions (opens in a new tab)

GitHub Discussions에서 "db pull doesn't support automatic splitting, so manual scripts are required" 라고 명시되어 있습니다.

해결 방법: 수동으로 스크립트를 작성하여 분할해야 합니다.

환경별 파일 제외 불가

특정 .prisma 파일을 프로덕션 환경에서만 제외하는 기능은 지원되지 않습니다. 심링크나 빌드 스크립트를 사용해야 합니다 GitHub Discussions (opens in a new tab)

GitHub Discussions에서 "There's no way to exclude specific .prisma files only in production. You'll need to use symlinks or scripts" 라고 명시되어 있습니다.

베스트 프랙티스

  1. 도메인 중심 분리: 비즈니스 도메인에 따라 파일을 분리합니다

    • 예: user.prisma, order.prisma, product.prisma
  2. 1,000줄 이상의 스키마에 적용: 작은 프로젝트에서는 단일 파일이 더 관리하기 쉽습니다

  3. 관계 모델 그룹화: 강하게 연결된 모델들은 같은 파일에 배치합니다

    • 예: UserUserProfileuser.prisma에 함께 정의
  4. 일관된 네이밍: 명확하고 일관된 파일명 규칙을 사용합니다

    • 좋은 예: user.prisma, post.prisma
    • 나쁜 예: models1.prisma, schema2.prisma
  5. 정기적인 포맷팅: prisma format을 정기적으로 실행하여 일관성을 유지합니다

실제 사용 사례

1,291줄의 단일 스키마를 28개 파일로 분할하여 성공적으로 사용한 사례가 보고되었습니다 GitHub Discussions (opens in a new tab)

GitHub Discussions에서 "Users have effectively split a 1,291-line schema into 28 files. Reported 'Works great' evaluation along with improved code organization" 이라고 명시되어 있습니다.

이는 대규모 프로젝트에서 multi-file schema가 효과적으로 작동함을 보여줍니다.