prisma generate 누락 — 빌드는 되는데 런타임 에러가 나는 이유
📚 NestJS 실전 트러블슈팅 시리즈 (12편)
NestJS + Prisma 프로젝트에서 schema.prisma에 새 필드를 추가한 뒤 prisma generate를 빠뜨리면, TypeScript 빌드는 as any 캐스팅 덕에 통과하지만 런타임에서 Prisma Client가 새 필드를 모른다. pnpm build와 prisma generate가 별개 명령인 구조적 함정, prebuild 훅으로 자동화하는 해결법, Docker와 CI에서 놓치지 않는 예방 전략을 실전 코드와 함께 정리한다.
💡 Tip. 바쁜 현대인들을 위한 본문 요약
pnpm build(=nest build)는 TypeScript 컴파일만 수행한다 — Prisma Client 재생성은 포함되지 않는다schema.prisma에 새 필드를 추가해도prisma generate를 실행하지 않으면 Prisma Client에 반영되지 않는다as any캐스팅으로 타입 에러를 우회하면 빌드는 통과하지만 런타임에서 undefined가 된다package.json의prebuild훅에prisma generate를 넣어 빌드할 때 자동으로 실행되게 하는 것이 정답이다- Docker와 CI 파이프라인에서도 별도로
prisma generate스텝을 명시해야 안전하다
🔍 증상 — 빌드 성공, 하지만 런타임에서 undefined

PM이 schema.prisma에 snapshotData Json? 필드를 추가하고 머지했다.
BE 개발자(내 역할)가 해당 필드를 사용하는 서비스 코드를 작성한 뒤 pnpm build를 돌렸다.
$ pnpm build
> @alp/[email protected] build /app/apps/api
> nest build
✔ Build succeeded
빌드 통과. 문제없어 보인다.
하지만 실제로 서버를 실행하면?
// student-report.application.service.ts
const report = await this.prisma.studentReport.create({
data: {
token,
studentId,
type: dto.type || 'CURRENT',
periodDays,
expiresAt,
createdByUserId,
snapshotData: fullSnapshot as any, // ← 여기
},
});
이 코드에서 snapshotData에 데이터를 넣었는데, 조회 시 undefined가 나왔다.
더 정확히는 — Prisma Client가 snapshotData라는 필드 자체를 모르는 상태였다.
왜?
prisma generate를 실행하지 않았기 때문이다.
🎯 원인 — pnpm build ≠ prisma generate

이 문제의 근본 원인은 pnpm build와 prisma generate가 완전히 별개의 명령이라는 점이다.
실제 빌드 스크립트 구조
프로젝트의 package.json을 보자.
// apps/api/package.json
{
"scripts": {
"build": "nest build", // ← TypeScript → JavaScript 컴파일만
"start:dev": "dotenv -e .env -- nest start --watch"
}
}
루트의 package.json은 Turborepo를 사용한다.
// package.json (root)
{
"scripts": {
"build": "turbo run build" // ← 각 워크스페이스의 build를 병렬 실행
}
}
turbo run build는 각 패키지의 build 스크립트를 실행할 뿐이다.
nest build는 TypeScript를 JavaScript로 컴파일만 한다.
Prisma Client를 재생성하는 건 아무도 하지 않는다.
전체 흐름 — pnpm build vs prisma generate

왼쪽 경로(pnpm build)는 TypeScript 컴파일만 수행하고, 오른쪽 경로(prisma generate)는 Prisma Client를 재생성한다.
두 경로를 모두 거쳐야 “타입 안전 + 런타임 정상”을 달성할 수 있다.
Prisma Client 생성이란?
prisma generate는 schema.prisma 파일을 읽어서 타입이 포함된 Prisma Client 코드를 node_modules/.prisma/client/에 생성한다.
schema.prisma (선언)
↓ prisma generate
node_modules/.prisma/client/ (실행 코드 + 타입)
↓ import
서비스 코드에서 사용
이 생성 과정을 거치지 않으면:
- TypeScript 타입 정의에 새 필드가 없다
- 런타임 Prisma Client도 새 필드를 인식하지 못한다
as any가 감춘 진짜 문제
정상적이라면 TypeScript 컴파일러가 에러를 잡아준다.
// ❌ prisma generate 안 했으면 → 컴파일 에러
snapshotData: fullSnapshot,
// → Property 'snapshotData' does not exist on type 'StudentReportCreateInput'
하지만 as any 캐스팅이 이 안전장치를 무력화했다.
// ✅ 빌드는 통과하지만... 런타임에서 문제
snapshotData: fullSnapshot as any,
TypeScript는 as any를 보고 타입 검사를 포기한다.
컴파일러 입장에서는 “개발자가 알아서 하겠지”인 셈이다.
결과적으로:
schema.prisma에snapshotData Json?추가됨prisma generate미실행 → Prisma Client에 필드 미반영- 서비스 코드에서
as any로 타입 에러 우회 pnpm build=nest build→ TypeScript 빌드 성공- 런타임에서 Prisma Client가
snapshotData를 무시 → DB에 저장은 되지만 조회 시 select에서 빠짐
🔧 해결 — prebuild 훅으로 자동화
즉시 해결: 수동 실행
# 스키마 변경이 포함된 머지 직후 필수 순서
git merge dev
cd apps/api && npx prisma generate # ← 이 한 줄이 빠져서 사고 남
pnpm build
하지만 사람이 기억해서 매번 실행하는 건 언젠가 또 빠뜨린다.
근본 해결: prebuild 훅
npm/pnpm의 lifecycle scripts를 활용한다.
prebuild라는 이름의 스크립트는 build 실행 직전에 자동으로 실행된다.
// ❌ Before: prisma generate가 빌드에 포함되지 않음
{
"scripts": {
"build": "nest build"
}
}
// ✅ After: prebuild 훅으로 자동 실행
{
"scripts": {
"prebuild": "npx prisma generate",
"build": "nest build"
}
}
이제 pnpm build를 실행하면:
prebuild→npx prisma generate자동 실행build→nest build실행
스키마가 변경되지 않았어도 prisma generate는 **멱등(idempotent)**하다.
이미 최신 상태면 빠르게 끝나므로 성능 걱정도 없다.
$ pnpm build
> @alp/[email protected] prebuild
> npx prisma generate
✔ Generated Prisma Client (v5.x.x) to ./node_modules/@prisma/client in 312ms
> @alp/[email protected] build
> nest build
✔ Build succeeded
Docker에서도 별도 명시
Dockerfile에서는 lifecycle script가 동작하지 않을 수 있다.
명시적으로 prisma generate 스텝을 추가해야 안전하다.
# ❌ Before: prisma generate 없이 바로 빌드
COPY apps/api ./apps/api
RUN pnpm run build
# ✅ After: generate를 먼저 실행
COPY apps/api ./apps/api
# Generate Prisma Client (prebuild 훅과 별개로 명시)
WORKDIR /app/apps/api
RUN npx prisma generate
# Build the application
RUN pnpm run build
실제 프로젝트의 Dockerfile도 이 패턴을 따른다.
# Stage 1: Builder
FROM node:20-slim AS builder
RUN corepack enable && corepack prepare pnpm@latest --activate
RUN apt-get update && apt-get install -y openssl && rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY package.json pnpm-workspace.yaml pnpm-lock.yaml ./
COPY turbo.json tsconfig.base.json ./
COPY apps/api/package.json ./apps/api/
COPY packages/ ./packages/
RUN pnpm install --frozen-lockfile
COPY apps/api ./apps/api
# ✅ Prisma Client 생성을 명시적으로 실행
WORKDIR /app/apps/api
RUN npx prisma generate
# 빌드
RUN pnpm run build
Docker 멀티스테이지 빌드에서 주의할 점이 하나 더 있다.
pnpm deploy --prod로 프로덕션 의존성만 추출하면 .prisma/client가 빠질 수 있다.
# 프로덕션 의존성 추출
RUN pnpm --filter @alp/api deploy --prod --legacy /app/deploy
# ✅ 빌드 스테이지에서 생성된 Prisma Client를 수동 복사
RUN mkdir -p /app/deploy/node_modules/.prisma && \
cp -r /app/node_modules/.pnpm/@prisma+client@*/node_modules/.prisma/client \
/app/deploy/node_modules/.prisma/
이 복사 스텝이 없으면 프로덕션 이미지에서 PrismaClientInitializationError가 발생한다.
🛡️ 예방 — 다시는 빠뜨리지 않으려면

1. 머지 후 체크리스트
# 스키마 변경 포함 여부 확인
git diff dev --name-only | grep schema.prisma
# 변경 있으면 → generate 필수
cd apps/api && npx prisma generate
이 확인을 자동화할 수도 있다.
#!/bin/bash
# scripts/post-merge.sh
if git diff HEAD@{1} --name-only | grep -q "schema.prisma"; then
echo "⚠️ schema.prisma 변경 감지 → prisma generate 실행"
cd apps/api && npx prisma generate
fi
Git의 post-merge 훅으로 등록하면 머지할 때마다 자동 실행된다.
# .git/hooks/post-merge에 위 스크립트 연결
cp scripts/post-merge.sh .git/hooks/post-merge
chmod +x .git/hooks/post-merge
2. as any 최소화 원칙
as any는 TypeScript의 타입 안전성을 완전히 무력화한다.
// ❌ 안전장치 해제
snapshotData: fullSnapshot as any,
// ✅ 타입을 명시하면 generate 누락 시 컴파일 에러로 잡힌다
snapshotData: fullSnapshot satisfies Prisma.InputJsonValue,
as any 대신 satisfies나 명시적 타입 단언을 사용하면, prisma generate를 빠뜨렸을 때 컴파일 에러가 나서 즉시 알 수 있다.
프로젝트 전체에서 as any 사용 현황을 확인해보면:
$ grep -rn "as any" apps/api/src/ | wc -l
15
15곳이나 있었다. 각각이 잠재적 타입 안전성 구멍이다.
3. CI 파이프라인에 검증 스텝 추가
# .github/workflows/ci.yml
jobs:
build:
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v2
- run: pnpm install --frozen-lockfile
# ✅ Prisma Client 생성
- name: Generate Prisma Client
run: cd apps/api && npx prisma generate
# ✅ 타입 체크 (as any 없이)
- name: Type Check
run: pnpm tsc --noEmit
- name: Build
run: pnpm build
4. Turborepo에서 generate를 dependsOn으로 관리
모노레포라면 Turborepo의 태스크 의존성으로 관리할 수도 있다.
// turbo.json
{
"tasks": {
"db:generate": {
"cache": false
},
"build": {
"dependsOn": ["db:generate", "^build"],
"outputs": ["dist/**"]
}
}
}
// apps/api/package.json
{
"scripts": {
"db:generate": "prisma generate",
"build": "nest build"
}
}
이렇게 하면 turbo run build 실행 시 db:generate가 자동으로 먼저 실행된다.
📌 정리
| 항목 | 내용 |
|---|---|
| 증상 | pnpm build 성공하지만 런타임에서 새 필드가 undefined |
| 원인 | nest build는 TypeScript 컴파일만 수행, prisma generate는 별도 |
| 숨은 범인 | as any 캐스팅이 타입 에러를 은폐 |
| 해결 | prebuild 훅에 npx prisma generate 추가 |
| Docker | 명시적 RUN npx prisma generate + .prisma/client 수동 복사 |
| 예방 | post-merge 훅, as any 최소화, CI에 generate 스텝, Turborepo dependsOn |
핵심 교훈
pnpm build통과 ≠ Prisma Client 최신. 스키마를 바꿨으면prisma generate는 별도로 실행해야 한다.as any는 이런 문제를 감추기만 할 뿐, 해결하지 않는다.
📚 NestJS 실전 트러블슈팅 시리즈 (12편)
- 1. NestJS + Prisma에서 N+1 쿼리 문제 해결하기
- 2. NestJS CORS 삽질 총정리 — PATCH만 안 되는 이유
- 3. Prisma 마이그레이션 실수 방지 — 컬럼 누락 해결기
- 4. NestJS DTO 클래스 필수인 이유 — interface로 만들면 터지는 두 가지
- 5. NestJS FK 제약 위반 디버깅 — Level ID 검증으로 500 에러 잡기
- 6. Prisma enum vs 도메인 타입 캐스팅 함정 — TypeScript 타입 불일치 해결기
- 7. Seed 데이터 FK 삭제 순서 삽질 — Prisma deleteMany가 터지는 이유
- 8. NestJS DI 에러 디버깅 — Nest can't resolve dependencies 3가지 원인과 서버 기동 테스트
- 9. Docker 빌드에서 pnpm 모노레포 삽질 — 데코레이터 에러 3132개의 정체
- 10. NestJS 재귀 호출 무한루프 — API 504 타임아웃의 숨겨진 원인 찾기
- 11. Soft Delete 필터가 빠진 곳 찾기 — 삭제한 데이터가 되살아나는 미스터리
- 12. prisma generate 누락 — 빌드는 되는데 런타임 에러가 나는 이유