Communitygithub.com

prisma-patterns

TypeScript 백엔드용 Prisma ORM 패턴 — 스키마 설계, 쿼리 최적화, 트랜잭션, 페이지네이션, 그리고 updateMany가 count를 반환하는 문제, $transaction 타임아웃, migrate dev의 DB 리셋, @updatedAt이 벌크 쓰기에서 누락되는 현상, 서버리스 연결 고갈과 같은 중요한 함정들을 다룹니다. Prisma 스키마나 쿼리를 작성하거나 트랜잭션, 마이그레이션, 서버리스 연결 제한을 디버깅할 때 사용하세요.

prisma-patterns란 무엇인가요?

이 스킬은 TypeScript 백엔드에서 Prisma ORM을 사용할 때 발생하는 생산 패턴과 숨겨진 함정들을 집중적으로 다룹니다. ID 전략, include와 select 비교, 커서 기반 페이지네이션, 소프트 삭제, 서버리스 환경에서의 연결 풀 관리 등 핵심 개념을 명확히 제시하며, 실제 코드 예제와 함께 구체적인 구현 방법을 보여줍니다. 특히 updateMany가 레코드가 아닌 count를 반환하는 문제, $transaction 대화형 폼의 5초 타임아웃, migrate dev가 데이터베이스를 초기화할 위험, @updatedAt이 벌크 쓰기에서 동작하지 않는 점, findUniqueOrThrow가 소프트 삭제된 레코드를 누설하는 현상 등 개발자들이 자주 실수하는 안티 패턴을 경고합니다. 이 스킬은 Prisma 버전별 API 차이점을 확인하도록 안내하며, `npx prisma --version` 명령어로 현재 버전을 체크한 후 패턴을 적용하도록 권장합니다. 지원 플랫폼은 Vercel, AWS Lambda, Cloudflare Workers 등 모든 서버리스 환경이며, Next.js나 Express와 같은 Node.js 백엔드 프레임워크와 함께 사용할 수 있습니다.

지원 대상~Claude Code~Codex CLI✓Cursor
npx skills add https://github.com/affaan-m/everything-claude-code/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을 추가하거나 열 이름을 한 번의 마이그레이션으로 변경하면 테이블이 잠기거나 데이터가 삭제됩니다. 확장-축소(expand-and-contract)를 사용하세요:

# 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 without where는 모든 행을 삭제함

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

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

모범 사례

규칙이유
CI/CD에서는 migrate deploy, 로컬에서만 migrate devmigrate dev는 드리프트 시 DB를 초기화할 수 있음
엔터티를 응답 DTO에 매핑내부 필드 누출 방지
항상 limit + cursor 기반 페이지네이션 사용오프셋 기반은 대규모 데이터셋에서 느리고 불안정함
findUniqueOrThrow 대신 findFirstOrThrow(소프트 삭제 시)findUniqueOrThrow는 고유 제약 조건 where가 필요하며 deletedAt과 호환되지 않음
트랜잭션 내부에서 외부 호출(이메일, HTTP) 금지5초 기본 타임아웃으로 인해 트랜잭션이 중단됨
트랜잭션 배열 형태가 가능하면 선호대화형 형태보다 성능이 좋고 명시적임
서버리스의 경우 모든 인스턴스에서 connection_limit=1연결 고갈 방지, 각 함수 호출은 자체 풀을 가짐
마이그레이션 파일을 절대 편집하지 말고 새 마이그레이션을 생성Prisma 체크섬이 적용된 마이그레이션을 무효화함

추가 자료

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/claude-api

Anthropic Claude API patterns for Python and TypeScript. Covers Messages API, streaming, tool use, vision, extended thinking, batches, prompt caching, and Claude Agent SDK. Use when building applications with the Claude API or Anthropic SDKs.

affaan-m/everything-claude-code

End-to-end marketing campaign planning and execution. Covers audience research, positioning, campaign angle definition, landing page copy, email sequences, social posts, ad copy, short-form video scripts, and content calendars. Use as the orchestration layer for multi-channel product launches. Use when planning or executing a multi-channel product launch, or producing landing page, email, social, or ad copy.

affaan-m/everything-claude-code

Development conventions and patterns for everything-claude-code. JavaScript project with conventional commits.

affaan-m/everything-claude-code-conventions

Development conventions and patterns for everything-claude-code. JavaScript project with conventional commits.

affaan-m/frontend-design

Create distinctive, production-grade frontend interfaces with high design quality. Use when the user asks to build web components, pages, or applications and the visual direction matters as much as the code quality.

affaan-m/gget

gget CLI and Python workflow for quick genomic database queries, sequence lookup, BLAST-style searches, enrichment checks, and reproducible bioinformatics evidence logs.

affaan-m/literature-review

Systematic literature-review workflow for academic, biomedical, technical, and scientific topics, including search planning, source screening, synthesis, citation checks, and evidence logging.

affaan-m/motion-ui

Production-ready UI motion system for React/Next.js. Use when implementing animations, transitions, or motion patterns.

affaan-m/project-guidelines-example

Example project-specific skill template based on a real production application.

affaan-m/pubmed-database

Direct PubMed and NCBI E-utilities search workflows for biomedical literature, MeSH queries, PMID lookup, citation retrieval, and API-backed literature monitoring.

affaan-m/scholar-evaluation

Structured scholarly-work evaluation for papers, proposals, literature reviews, methods sections, evidence quality, citation support, and research-writing feedback.

affaan-m/uspto-database

USPTO patent and trademark data workflow for official record lookup, PatentSearch queries, TSDR checks, assignment data, and reproducible IP research logs.

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.

관련 스킬