Communitygithub.com

prisma-patterns

Prisma ORM 패턴 for TypeScript 백엔드 — 스키마 디자인, 쿼리 최적화, 트랜잭션, 페이지네이션, 그리고 updateMany가 count 반환, $transaction 타임아웃, migrate dev가 DB 리셋, @updatedAt이 벌크 쓰기에서 무시됨, 서버리스 연결 고갈과 같은 중요한 함정을 다룹니다. Prisma 스키마나 쿼리 작성 시, 또는 트랜잭션, 마이그레이션, 서버리스 연결 제한 디버깅 시 사용하세요.

prisma-patterns란 무엇인가요?

이 스킬은 TypeScript 백엔드에서 Prisma ORM을 사용할 때 발생하는 생산 패턴과 잘 알려지지 않은 함정을 종합적으로 제공합니다. 스키마 디자인(ID 전략, 기본값 설정, include vs select), 쿼리 최적화(커서 페이지네이션, N+1 문제 해결), 트랜잭션(배열/인터랙티브 폼 선택), PrismaClient 싱글톤 관리, 서버리스 환경에서의 연결 풀 설정 등 핵심 개념을 설명합니다. 또한 updateMany가 레코드 대신 count를 반환하는 문제, $transaction 인터랙티브 폼의 5초 타임아웃, migrate dev가 데이터베이스를 리셋할 수 있는 위험, @updatedAt이 bulk update에서 동작하지 않는 문제, soft delete와 findUniqueOrThrow의 충돌 등 주요 안티패턴을 상세히 다룹니다. Claude Code, Cursor 등 AI 에이전트가 Prisma 관련 작업 시 이 스킬을 활성화하여 정확하고 안전한 코드를 생성할 수 있습니다.

지원 대상~Claude Code~Codex CLI✓Cursor
npx skills add https://github.com/affaan-m/ECC/tree/main/skills/prisma-patterns

즐겨 사용하는 AI에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

문서

Prisma 패턴

TypeScript 백엔드에서 Prisma ORM을 사용하기 위한 프로덕션 패턴과 잘 알려지지 않은 함정.

패턴을 적용하기 전에 버전을 확인하세요. Prisma API는 주요 릴리스에 걸쳐 변경되었습니다:

npx prisma --version

버전 간 주요 API 차이점:

  • relationJoins은 관계를 별도의 쿼리 대신 JOIN을 통해 로드할 수 있지만, 큰 1:N 관계나 깊은 include에서 행 폭발을 일으킬 수 있음 — 두 접근 방식을 벤치마킹하세요
  • omit 필드 수정자와 prisma.$extends 클라이언트 확장 API가 추가됨
  • 최신 설치: 패키지 이름이 @prisma/client 대신 prisma일 수 있음; PrismaClient는 드라이버 어댑터(예: @prisma/adapter-pg)가 필요할 수 있음; datasource.url이 schema.prisma 대신 prisma.config.ts에 있을 수 있음
  • CLI 명령어(migrate dev, migrate deploy, generate)는 버전에 관계없이 동일

활성화 시점

  • Prisma 스키마 모델과 관계를 설계하거나 수정할 때
  • 쿼리, 트랜잭션 또는 페이지네이션 로직을 작성할 때
  • updateMany, deleteMany 또는 벌크 작업을 사용할 때
  • 데이터베이스 마이그레이션을 실행하거나 계획할 때
  • 서버리스 환경(Vercel, Lambda, Cloudflare Workers)에 배포할 때
  • 소프트 삭제 또는 멀티테넌트 행 필터링을 구현할 때

핵심 개념

ID 전략

전략사용 시점피해야 할 시점
@default(cuid())기본 선택 — URL 안전, 정렬 가능, 충돌 없음외부 시스템에 순차적 ID 필요함
@default(uuid())Prisma 외부 시스템과의 상호 운용성 필요쓰기가 많은 테이블(랜덤 UUID는 B-트리 인덱스를 조각냄)
@default(autoincrement())내부 조인 테이블, 감사 로그공개용 ID(레코드 수 노출)

스키마 기본값

model User {
  id        String    @id @default(cuid())
  email     String    @unique  // @unique는 이미 인덱스를 생성 — @@index 불필요
  name      String
  role      Role      @default(USER)
  posts     Post[]
  createdAt DateTime  @default(now())
  updatedAt DateTime  @updatedAt
  deletedAt DateTime?

  @@index([createdAt])
  @@index([deletedAt, createdAt]) // 소프트 삭제 + 정렬 쿼리를 위한 복합 인덱스
}
  • WHERE 또는 ORDER BY에 사용되는 모든 외래 키와 열에 @@index를 추가하세요.
  • 소프트 삭제가 예상되는 요구사항이라면 미리 deletedAt DateTime?을 선언하세요 — 나중에 추가하려면 라이브 테이블에 마이그레이션이 필요합니다.
  • updatedAt @updatedAt는 update 및 upsert에서만 Prisma에 의해 자동으로 설정됩니다(벌크 업데이트 함정에 대한 안티패턴 참조).

include vs select

includeselect
반환모든 스칼라 필드 + 지정된 관계지정된 필드만
사용 시점대부분의 필드와 관계가 필요할 때핫 경로, 큰 테이블, 과다 페치 방지
성능넓은 테이블에서 과다 페치 가능최소 페이로드, 큰 데이터셋에서 더 빠름
Prisma 5 참고기본적으로 JOIN 사용(relationJoins)동일
// include — 모든 열 + 관계
const user = await prisma.user.findUnique({
  where: { id },
  include: { posts: { select: { id: true, title: true } } },
});

// select — 명시적 허용 목록
const user = await prisma.user.findUnique({
  where: { id },
  select: { id: true, email: true, name: true },
});

API 응답에서 Prisma 엔티티를 직접 반환하지 마세요 — 노출되는 필드를 제어하기 위해 응답 DTO에 매핑하세요:

// 나쁨: passwordHash, deletedAt, 내부 필드가 유출됨
return await prisma.user.findUniqueOrThrow({ where: { id } });

// 좋음: 명시적 DTO 매핑
const user = await prisma.user.findUniqueOrThrow({ where: { id } });
return { id: user.id, name: user.name, email: user.email };

트랜잭션 형식 선택

상황사용
독립적인 작업, 상호 의존성 없음배열 형식
이후 단계가 이전 결과에 의존인터랙티브 형식
외부 호출(이메일, HTTP) 포함트랜잭션 외부에서 완전히 처리
// 배열 형식 — 한 번의 왕복으로 일괄 처리
const [user, post] = await prisma.$transaction([
  prisma.user.update({ where: { id }, data: { name } }),
  prisma.post.create({ data: { title, authorId: id } }),
]);

// 인터랙티브 형식 — tx 클라이언트만 사용, 외부 prisma 클라이언트는 절대 사용하지 않음
const post = await prisma.$transaction(async (tx) => {
  const user = await tx.user.findUniqueOrThrow({ where: { id } });
  if (user.role !== 'ADMIN') throw new Error('Forbidden');
  return tx.post.create({ data: { title, authorId: user.id } });
});

PrismaClient 싱글톤

각 PrismaClient 인스턴스는 자체 연결 풀을 엽니다. 한 번만 인스턴스화하세요.

// lib/prisma.ts

// 옵션 A — 어댑터 기반 초기화 (최신 Prisma 설치에 필요)
import { PrismaClient } from '@prisma/client'; // 또는 설정에 맞는 생성된 클라이언트 경로
import { PrismaPg } from '@prisma/adapter-pg';

function createPrismaClient() {
  const adapter = new PrismaPg({
    connectionString: process.env.DATABASE_URL!,
  });
  return new PrismaClient({
    adapter,
    log: process.env.NODE_ENV === 'development' ? ['query', 'error'] : ['error'],
  });
}

const globalForPrisma = globalThis as unknown as { prisma?: PrismaClient };

export const prisma = globalForPrisma.prisma ?? createPrismaClient();

if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;

// 옵션 B — 직접 초기화 (이전 설치, 어댑터 불필요)
// import { PrismaClient } from '@prisma/client';
// export const prisma = globalForPrisma.prisma ?? new PrismaClient({ ... });

Prisma 설치가 PrismaClient 생성자에 adapter 인수를 요구하는 경우 옵션 A를 사용하세요. new PrismaClient()가 인수 없이 작동하는 경우 옵션 B를 사용하세요. 컴파일러가 올바른 것을 알려줄 것입니다.

globalThis 패턴은 핫 리로드(Next.js, nodemon, ts-node-dev) 중 중복 인스턴스를 방지합니다.

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 } });
}

// 좋음: 단일 쿼리
const users = await prisma.user.findMany({ include: { posts: true } });

Prisma 5+ relationJoins를 사용하면 include 형식이 단일 JOIN을 사용합니다. 큰 1:N 집합에서는 결과 집합 크기가 증가할 수 있습니다 — 관계가 부모당 많은 행을 반환할 수 있는 경우 두 접근 방식을 벤치마킹하세요.

코드 예제

커서 페이지네이션 (피드 및 대규모 데이터셋에 선호)

async function getPosts(cursor?: string, limit = 20) {
  const items = await prisma.post.findMany({
    where: { published: true },
    orderBy: [
      { createdAt: 'desc' },
      { id: 'desc' }, // 보조 정렬은 중복 타임스탬프에서 불안정한 페이지네이션 방지
    ],
    take: limit + 1,
    ...(cursor && { cursor: { id: cursor }, skip: 1 }),
  });

  const hasNextPage = items.length > limit;
  if (hasNextPage) items.pop();

  return { items, nextCursor: hasNextPage ? items[items.length - 1].id : null };
}

limit + 1을 가져오고 pop — 추가 count 쿼리 없이 hasNextPage를 감지하는 표준 방법입니다. 여러 행이 동일한 타임스탬프를 공유할 때 불안정한 페이지네이션을 방지하기 위해 항상 고유 필드(예: id)를 보조 orderBy로 포함하세요. 사용자가 임의 페이지로 이동해야 하는 경우(관리자 테이블)에만 오프셋 페이지네이션을 사용하세요.

소프트 삭제

// 항상 명시적으로 필터링 — 미들웨어에 의존하지 마세요 (동작을 숨기고 디버그가 어려움)
const activeUsers = await prisma.user.findMany({ where: { deletedAt: null } });

await prisma.user.update({ where: { id }, data: { deletedAt: new Date() } });
await prisma.user.update({ where: { id }, data: { deletedAt: null } }); // 복원

오류 처리

import { Prisma } from '@prisma/client'; // 또는 설정에 맞는 생성된 클라이언트 경로

try {
  await prisma.user.create({ data: { email } });
} catch (e) {
  if (e instanceof Prisma.PrismaClientKnownRequestError) {
    if (e.code === 'P2002') throw new ConflictError('Email already exists');
    if (e.code === 'P2025') throw new NotFoundError('Record not found');
    if (e.code === 'P2003') throw new BadRequestError('Referenced record does not exist');
  }
  throw e;
}

일반 코드: P2002 고유성 위반 · P2025 찾을 수 없음 · P2003 외래 키 위반.

서비스 경계에서 포착하여 도메인 오류로 변환하세요. API 소비자에게 원시 Prisma 메시지를 노출하지 마세요.

연결 풀 — 서버리스

연결 매개변수를 DATABASE_URL에 직접 포함시키세요 — 문자열 연결은 URL에 이미 쿼리 매개변수(예: ?schema=public)가 있으면 깨집니다:

# .env — 권장: URL에 매개변수 포함
DATABASE_URL="postgresql://user:pass@host/db?connection_limit=1&pool_timeout=20"

# 외부 풀러(PgBouncer, Supabase pooler) 사용 시
DATABASE_URL="postgresql://user:pass@host/db?pgbouncer=true&connection_limit=1"
// Vercel, AWS Lambda 및 유사한 서버리스 런타임:
// 인스턴스당 풀을 1로 제한; connection_limit 및 pool_timeout은 DATABASE_URL을 통해 제어

// 어댑터 기반 설정 (Prisma 설치에 어댑터가 필요한 경우):
import { PrismaClient } from '@prisma/client';
import { PrismaPg } from '@prisma/adapter-pg';

const prisma = new PrismaClient({
  adapter: new PrismaPg({ connectionString: process.env.DATABASE_URL }),
});

// 직접 설정 (Prisma 설치에 어댑터가 필요하지 않은 경우):
// const prisma = new PrismaClient();

안티패턴

updateMany는 레코드가 아닌 count를 반환

// 나쁨: 결과는 { count: 2 } — users[0]은 undefined
const users = await prisma.user.updateMany({ where: { role: 'GUEST' }, data: { role: 'USER' } });

// 좋음: 먼저 ID를 캡처한 다음 업데이트, 마지막으로 영향을 받은 행만 가져오기
const targets = await prisma.user.findMany({
  where: { role: 'GUEST' },
  select: { id: true },
});
const ids = targets.map((u) => u.id);
await prisma.user.updateMany({ where: { id: { in: ids } }, data: { role: 'USER' } });
const updated = await prisma.user.findMany({ where: { id: { in: ids } } });

deleteMany에도 동일하게 적용 — { count: n }를 반환하며, 삭제된 행을 반환하지 않습니다.

$transaction 인터랙티브 형식이 5초 후 타임아웃

// 나쁨: 트랜잭션 내부의 외부 호출이 기본 5초 초과 → "Transaction already closed"
await prisma.$transaction(async (tx) => {
  const user = await tx.user.findUniqueOrThrow({ where: { id } });
  await sendWelcomeEmail(user.email); // 외부 호출
  await tx.user.update({ where: { id }, data: { emailSent: true } });
});

// 좋음: 외부 호출은 트랜잭션 외부에서
const user = await prisma.user.findUniqueOrThrow({ where: { id } });
await sendWelcomeEmail(user.email);
await prisma.user.update({ where: { id }, data: { emailSent: true } });

// 벌크 처리가 실제로 필요한 경우에만 타임아웃 증가
await prisma.$transaction(async (tx) => { ... }, { timeout: 30_000 });

migrate dev가 데이터베이스를 리셋할 수 있음

migrate dev는 스키마 드리프트를 감지하고 DB를 리셋하라는 메시지를 표시하여 모든 데이터를 삭제할 수 있습니다.

# 절대 공유 개발, 스테이징 또는 프로덕션에서 사용하지 마세요
npx prisma migrate dev --name add_column

# 로컬 단독 개발 이외의 모든 곳에서 안전
npx prisma migrate deploy

# 적용하지 않고 드리프트 확인
npx prisma migrate diff \
  --from-migrations ./prisma/migrations \
  --to-schema-datamodel ./prisma/schema.prisma \
  --shadow-database-url "$SHADOW_DATABASE_URL"

마이그레이션 파일을 수동으로 편집하면 향후 배포가 중단됨

Prisma는 모든 마이그레이션 파일을 체크섬합니다. 적용 후 편집하면 원본이 이미 실행된 모든 환경에서 P3006 checksum mismatch가 발생합니다. 대신 새 마이그레이션을 생성하세요.

스키마 변경 사항을 깨뜨리려면 다단계 마이그레이션이 필요함

기존 열에 NOT NULL을 추가하거나 열 이름을 한 번의 마이그레이션으로 변경하면 테이블이 잠기거나 데이터가 손실됩니다. 확장-축소 패턴을 사용하세요:

# 1단계: 로컬에서 마이그레이션 생성, 배포
npx prisma migrate dev --name add_new_column   # 로컬 전용
npx prisma migrate deploy                       # 스테이징 / 프로덕션
// 2단계: 데이터 백필 (스크립트 또는 마이그레이션 작업에서 실행, 쉘에서는 실행하지 마세요)
await prisma.user.updateMany({ data: { newColumn: derivedValue } });
# 3단계: 로컬에서 NOT NULL 제약 조건 마이그레이션 생성, 배포
npx prisma migrate dev --name make_new_column_required  # 로컬 전용
npx prisma migrate deploy                               # 스테이징 / 프로덕션

@updatedAt이 updateMany에서 작동하지 않음

@updatedAt는 update 및 upsert에서만 자동으로 설정됩니다. 벌크 쓰기는 이를 만료된 상태로 둡니다.

// 나쁨: updatedAt이 이전 값으로 유지됨
await prisma.post.updateMany({ where: { authorId }, data: { published: true } });

// 좋음
await prisma.post.updateMany({
  where: { authorId },
  data: { published: true, updatedAt: new Date() },
});

소프트 삭제 + findUniqueOrThrow가 삭제된 레코드를 유출함

findUniqueOrThrow는 DB에 행이 존재하지 않을 때만 P2025를 발생시킵니다. 소프트 삭제된 행은 여전히 존재하며 오류 없이 반환됩니다.

findUniqueOrThrow는 where에 고유 제약 조건 필드가 필요합니다 — id 옆에 deletedAt: null을 추가하면 { id, deletedAt }이 복합 고유 제약 조건이 아니므로 타입이 깨집니다. 대신 findFirstOrThrow를 사용하세요.

// 나쁨: 소프트 삭제된 사용자 반환
const user = await prisma.user.findUniqueOrThrow({ where: { id } });

// 나쁨: Prisma 타입 오류 — { id, deletedAt }은 고유 제약 조건이 아님
const user = await prisma.user.findUniqueOrThrow({ where: { id, deletedAt: null } });

// 좋음: findFirstOrThrow는 임의의 where 조건 지원
const user = await prisma.user.findFirstOrThrow({ where: { id, deletedAt: null } });

deleteMany에 where가 없으면 모든 행이 삭제됨

// 나쁨: 조용히 테이블을 비움
await prisma.post.deleteMany();

// 좋음
await prisma.post.deleteMany({ where: { authorId: userId } });

모범 사례

규칙이유
CI/CD에서 migrate deploy, 로컬에서만 migrate devmigrate dev는 드리프트 시 DB를 리셋할 수 있음
엔터티를 응답 DTO에 매핑내부 필드 유출 방지
고유 필드를 보조 orderBy로 포함중복 타임스탬프에서 안정적인 페이지네이션 보장
소프트 삭제 시 findUniqueOrThrow 대신 findFirstOrThrow 사용삭제된 레코드가 쿼리를 통해 유출되는 것을 방지
연결 문자열 외부에서 connection_limit 및 pool_timeout 직접 설정URL에 ?가 이미 있으면 문자열 연결이 깨짐
Prisma 오류를 서비스 경계에서 도메인 오류로 변환API 소비자에게 내부 세부 사항 숨김
include가 관계를 반환해야 하는 경우 열을 명시적으로 선택(중첩 select 사용)과다 페치 방지
벌크 업데이트 전에 항상 ID를 먼저 가져오기updateMany가 레코드를 반환하지 않기 때문

관련 리소스

Individual skills in this repo

This repo contains 20 individual skills — each has its own dedicated page.

accessibility

Design, implement, and audit inclusive digital products using WCAG 2.2 Level AA. Use when building or auditing UI that must meet WCAG 2.2 Level AA, or when reviewing a change for keyboard, contrast, or screen-reader support.

affaan-m/content-engine

Create platform-native content systems for X, LinkedIn, TikTok, YouTube, newsletters, and repurposed multi-platform campaigns. Use when the user wants social posts, threads, scripts, content calendars, or one source asset adapted cleanly across platforms.

affaan-m/ECC

Multi-platform content distribution across X, LinkedIn, Threads, and Bluesky. Adapts content per platform using content-engine patterns. Never posts identical content cross-platform. Use when the user wants to distribute content across social platforms.

affaan-m/fal-ai-media

Unified media generation via fal.ai MCP — image, video, and audio. Covers text-to-image (Nano Banana), text/image-to-video (Seedance, Kling, Veo 3), text-to-speech (CSM-1B), and video-to-audio (ThinkSound). Use when the user wants to generate images, videos, or audio with AI.

affaan-m/manim-video

日本語翻訳:このファイルは manim-video 用の日本語翻訳が必要です

affaan-m/remotion-video-creation

Remotion のベストプラクティス - React で動画を作成する。3D、アニメーション、音声、字幕、チャート、トランジションなどをカバーするドメイン固有の29のルール。

affaan-m/video-editing

AI-assisted video editing workflows for cutting, structuring, and augmenting real footage. Covers the full pipeline from raw capture through FFmpeg, Remotion, ElevenLabs, fal.ai, and final polish in Descript or CapCut. Use when the user wants to edit video, cut footage, create vlogs, or build video content.

agent-architecture-audit

Full-stack diagnostic for agent and LLM applications. Audits the 12-layer agent stack for wrapper regression, memory pollution, tool discipline failures, hidden repair loops, and rendering corruption. Produces severity-ranked findings with code-first fixes. Essential for developers building agent applications, autonomous loops, or any LLM-powered feature. Use when an agent or LLM feature misbehaves and the failing layer is unknown, or before shipping an agent stack.

agent-eval

Head-to-head comparison of coding agents (Claude Code, Aider, Codex, etc.) on custom tasks with pass rate, cost, time, and consistency metrics. Use when choosing between coding agents, or when a change to an agent setup needs measured pass rate, cost, and time rather than an impression.

agent-harness-construction

Design and optimize AI agent action spaces, tool definitions, and observation formatting for higher completion rates. Use when defining or revising an agent

agentic-engineering

Operate as an agentic engineer using eval-first execution, decomposition, and cost-aware model routing. Use when planning or executing engineering work that agents will carry out end to end.

agentic-os

Build persistent multi-agent operating systems on Claude Code. Covers kernel architecture, specialist agents, slash commands, file-based memory, scheduled automation, and state management without external databases. Use when building a persistent multi-agent system on Claude Code with its own memory, commands, and scheduling.

agent-introspection-debugging

Structured self-debugging workflow for AI agent failures using capture, diagnosis, contained recovery, and introspection reports. Use when an agent run fails and you need a reproducible diagnosis instead of a retry.

agent-payment-x402

Add x402 payment execution to AI agents with per-task budgets, spending controls, and non-custodial wallets. Supports Base through agentwallet-sdk and X Layer through OKX Payments / OKX Agent Payments Protocol. Use when an agent must pay for something itself and needs per-task budgets, spending controls, and a non-custodial wallet.

agent-self-evaluation

Use after completing any non-trivial task. The agent self-rates its output on 5 axes — accuracy, completeness, clarity, actionability, conciseness — with concrete evidence per criterion. Produces a structured 1-5 scorecard with specific improvement suggestions.

agent-sort

Build an evidence-backed ECC install plan for a specific repo by sorting skills, commands, rules, hooks, and extras into DAILY vs LIBRARY buckets using parallel repo-aware review passes. Use when ECC should be trimmed to what a project actually needs instead of loading the full bundle.

ai-first-engineering

Engineering operating model for teams where AI agents generate a large share of implementation output. Use when setting team process, review gates, or ownership rules for a codebase largely written by agents.

ai-regression-testing

Regression testing strategies for AI-assisted development. Sandbox-mode API testing without database dependencies, automated bug-check workflows, and patterns to catch AI blind spots where the same model writes and reviews code. Use when adding regression coverage to AI-assisted code, or when the same model both wrote and reviewed a change.

android-clean-architecture

Clean Architecture patterns for Android and Kotlin Multiplatform projects — module structure, dependency rules, UseCases, Repositories, and data layer patterns. Use when structuring modules, layers, or data flow in an Android or KMP project.

angular-developer

Generates Angular code and provides architectural guidance. Trigger when creating projects, components, or services, or for best practices on reactivity (signals, linkedSignal, resource), forms, dependency injection, routing, SSR, accessibility (ARIA), animations, styling (component styles, Tailwind CSS), testing, or CLI tooling.

관련 스킬