prisma generate 를 어디서 돌릴까: postinstall 과 도커 빌드 캐시
새 맥에 프로젝트들을 하나씩 세우는 중이었습니다. 클론하고, 의존성을 깔고, 개발 서버를 띄우면 이 화면이 떴습니다.
Module not found: Can't resolve '../generated/prisma/client'
1 | import { PrismaPg } from "@prisma/adapter-pg";
> 2 | import { PrismaClient } from "../generated/prisma/client";같은 오류를 하루에 세 번 봤습니다. 리포마다 원인이 조금씩 달랐고, 고치려다 배포를 한 번 깨먹었습니다.
리포마다 갈린 답
Prisma 는 스키마를 읽어 타입이 붙은 클라이언트 코드를 생성합니다. 그 결과물을 리포에 커밋할지 말지가 갈립니다. 제 리포 네 개가 두 진영으로 나뉘어 있었습니다.
| 생성 코드 | 클론 직후 | |
|---|---|---|
| 대시보드 | 커밋함 | 바로 뜸 |
| 나머지 셋 | .gitignore |
Module not found |
무시하는 쪽이 셋인데 postinstall 은 아무 데도 없었습니다. 그러니 클론한 사람은 npx prisma generate 를 손으로 쳐야 하고, 그걸 아는 방법은 오류를 한 번 만나는 것뿐이었습니다.
커밋하는 쪽에 있던 다른 문제
바로 뜨는 쪽이 나아 보였는데, 그 리포에서 습관적으로 npx prisma generate 를 돌렸더니 파일 21개가 바뀌었습니다. 대부분이 삭제였습니다.
스키마에서 걷어낸 모델들의 생성 파일이 그대로 남아 있었던 것입니다. 두 달 전에 문서 기능을 별도 서비스로 떼어내면서 스키마에서는 지웠는데, 생성 코드는 다시 만들지 않아 옛 모델이 남았습니다.
이 상태로도 빌드는 됩니다. 앱이 그 모델을 안 쓰니 아무 일도 일어나지 않습니다. 어긋났다는 사실이 어디에도 드러나지 않고, 누군가 generate 를 돌리는 날 21개 파일 diff 로 나타납니다.
생성 코드를 커밋하는 방식은 스키마를 고칠 때마다 generate 하고 함께 커밋한다 는 규율을 요구합니다. 규율이 지켜지지 않으면 정본이 둘이 되고, 둘 중 어느 쪽이 맞는지는 아무도 안 알려 줍니다.
한 줄이면 될 줄 알았던 postinstall
무시하는 쪽 리포에 이 한 줄을 넣었습니다.
"postinstall": "prisma generate"로컬에서 확인했습니다. src/generated 를 통째로 지우고 npm install 만 돌리자 다시 만들어졌습니다. 62ms 걸렸습니다. 커밋하고 푸시했습니다.
2분 뒤 배포가 실패했습니다.
> app@0.1.0 postinstall
> prisma generate
Error: Could not find Prisma Schema
prisma/schema.prisma: file not found스키마가 없는 도커 deps 단계
Dockerfile 을 열어 보니 이유가 한눈에 보였습니다.
FROM node:22-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json .npmrc ./
RUN npm ci # ← 여기서 postinstall 이 돈다
FROM node:22-alpine AS builder
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npx prisma generate # ← 원래는 여기서 생성했다의존성 설치 단계는 소스 전체를 복사하지 않습니다. package.json 과 잠금 파일만 복사하는데, 그래야 소스가 바뀌어도 그 레이어가 캐시로 재사용되기 때문입니다. 도커 이미지를 빠르게 만드는 가장 기본적인 방법입니다.
그 시점에는 prisma/schema.prisma 가 없습니다. 그런데 제가 npm ci 에 "스키마를 읽어라" 는 일을 붙여 버렸습니다.
로컬에서는 스키마가 늘 거기 있습니다. 그래서 이 실패는 도커에서만 났고, 저는 로컬만 확인하고 푸시했습니다.
고치는 방법과 그 대가
deps 단계에 스키마를 함께 복사하면 됩니다.
COPY package.json package-lock.json .npmrc ./
COPY prisma ./prisma
RUN npm ci다음 배포는 통과했습니다. 다만 공짜가 아닙니다. 스키마를 고칠 때마다 이 레이어의 캐시가 무효화됩니다. 마이그레이션을 만들 때마다 CI 가 npm ci 를 처음부터 다시 돌립니다.
바꿔 말하면 이 선택은 "설치가 곧 생성" 이라는 약속을 사기 위해 캐시 적중률을 조금 파는 것입니다. 저는 샀습니다. 새 기계에서 오류를 만나고 원인을 찾는 시간이 CI 20초보다 비싸다고 봤습니다.
그래서 지금 규칙
생성물은 커밋하지 않습니다. 정본은 스키마 하나입니다. 어긋남이 생길 자리를 없앱니다.
설치가 생성합니다. postinstall 에 두면 사람이 기억할 것이 하나 줄어듭니다.
그 약속을 도커 빌드에서도 지킵니다. 설치 단계가 스키마를 필요로 하게 됐으면, 그 단계에 스키마를 넣어야 합니다. 한쪽에서만 성립하는 약속은 약속이 아닙니다.
package.json 의 scripts 를 건드리는 변경은 로컬 실행만으로 검증되지 않습니다. npm ci 가 도는 자리는 여러 곳이고, 자리마다 있는 파일이 다릅니다. 이번에 제가 놓친 것이 정확히 그 차이였습니다.
아직 정리하지 못한 것도 있습니다. 생성 코드를 커밋하는 그 리포는 여전히 스키마와 어긋난 상태입니다. 되돌려 두었고, 옮기는 작업은 따로 잡아야 합니다.
함께 읽기
- Next.js Docker 빌드 캐시: 배포 CSS에서 패키지 스타일이 빠진 문제테마 메뉴에서 디자인 시스템을 바꾸는 기능을 배포했습니다. 메뉴에는 「Betelgeuse 라임 다크」라고 정확히 찍히는데, 화면은 기본 시스템 그대로였습니다. 바로 전 배포에서는 같은 기능이 잘 동작했습니다. 그 사이에 바뀐 것은 디자인 시스템 패키지의 버전 하나였습니다.
- EAS 빌드 큐 40분, 로컬 5분: 로컬로 옮기기 전에 백업해야 할 것앱 하나를 출시하려고 EAS에 안드로이드 production 빌드를 걸었습니다. 큐에서 40분을 기다렸습니다. 무료 티어라 빌드 머신이 빌 때까지 순서를 기다리는 구조입니다.
- Prisma 마이그레이션 이전 리허설: migrate diff로는 못 잡는 것들운영 중인 서비스의 데이터베이스를 온프레미스에서 Supabase로 옮기는 중입니다. 지난번에 1단계로 앱을 무상태화했고, 이번이 2단계인 스키마 복제입니다. 프로젝트를 만들고 접속 문자열을 받아 prisma migrate deploy 한 줄 돌리면 끝나는 일이라고 생각했습니다. 그 전에 빈 데이터베이스에 한 번 리허설을…
- Prisma DIRECT_URL: 운영이 아니라 풀러가 가르는 기준Prisma 설정에서 이런 줄을 자주 봅니다.
- Prisma 운영 DB 마이그레이션: .env 파일 없이 접속 정보를 주입해 실행하는 방법새 테이블이 필요한 기능을 배포하면서 운영 DB에 Prisma 마이그레이션을 직접 돌려야 했습니다. 이 프로젝트는 Vercel 빌드에서 마이그레이션을 돌리지 않습니다. 프리뷰 빌드가 운영 DB를 건드리거나, 빌드가 중간에 실패해서 스키마가 반쯤 바뀐 채 남는 일을 막으려고 일부러 뺐습니다. 그래서 스키마를 바꾸는 배포는…