TechFeedTechFeed
Backend

Prisma P2003, 외래키, foreign key | 부모 아이디를 넣었는데 자식만 막히면?

프리즈마 P2003은 표와 칸이 있어도 자식이 넣은 부모 아이디가 그 디비에 없을 때 납니다. generate 초록과 프리뷰 User 빈 표는 다른 칸이고, meta.field_name과 카카오 회원번호, 트랜잭션 upsert를 먼저 봅니다. Prisma, 외래키, 슈퍼베이스, 버셀, Next.js, 카카오 로그인, 1인 개발자 기준. 2026년 9월 프리즈마 오류·포스트그레스 외래키 공식 문서.

by

프리즈마 P2003은 표와 칸이 있어도, 자식이 넣은 부모 아이디가 그 디비에 없을 때 납니다. 클라이언트 생성이 초록이어도 프리뷰 사용자 표가 비어 있으면 세션 넣기는 거절되고, 카카오 콜백은 500으로만 보입니다.


로컬 스튜디오엔 행이 보이는데 프리뷰만 외래키 거절이 뜨면, 클라이언트를 다시 만들기 전에 메타의 칸 이름과 부모 표부터 보세요. 카카오 회원번호를 사용자 아이디 칸에 넣었는지도 같이 봅니다.


유니크가 겹친 P2002, 고칠 행이 없는 P2025와 코드를 섞지 마세요. 근거는 프리즈마 오류 참고포스트그레스 외래키에 있습니다.


P2003이 뜨면 어디를 보나

부모 표에 그 아이디 행이 있는지부터 봅니다. 프리즈마 오류 참고의 P2003 문구는 그 칸에서 외래키가 실패했다는 뜻이고, 포스트그레스는 같은 거절을 23503으로 남깁니다. 표가 없거나 칸이 없거나 연결이 끊긴 줄이 아닙니다.


자식 표에 값을 넣을 때 그 값이 부모 표의 기본키(또는 유니크)에 있어야 합니다. 세션의 사용자 아이디를 넣는데 사용자 표에 그 값이 없으면 디비가 거절합니다. 로컬에선 예전에 가입해 둔 사용자가 있고, 프리뷰 디비는 비어 있으면 같은 코드가 한쪽에선 통과하고 한쪽에선 P2003이 납니다.


제가 쓰는 카카오 콜백도 닉네임 화면까지는 되고, 세션 행을 넣는 줄에서만 500이 난 적이 있습니다. 로그에 P2003이 찍히면 콘솔 리다이렉트 주소가 아니라 부모 행을 봐야 합니다. 화면이 조용해져도 부모 표가 비어 있으면 다음 넣기에서 같은 줄이 반복됩니다.


먼저 기억할 것 | 클라이언트 생성 초록은 타입만 맞춘 상태입니다. 프리뷰 사용자 표에 그 아이디가 있는지는 다른 확인입니다.


자식 표가 부모 아이디를 가리키는데 부모 행이 없어 외래키가 거절되는 서버실 통로
자식이 넣은 아이디가 부모 표에 없으면 디비가 그 넣기를 거절한다

P2025, P2002, P2014는 다른 코드다

P2003은 외래키, P2002는 유니크, P2025는 그 행 없음입니다. 세 코드가 카카오 콜백 500 뒤에 같이 숨어 보여도, 고치는 줄이 다릅니다.


코드디비가 거절한 이유먼저 볼 칸
P2003자식이 가리킨 부모 행이 없음, 또는 지우기가 자식을 남김메타의 칸 이름, 부모 표 조회
P2002유니크 칸이 이미 참메타의 대상, 이메일과 카카오 아이디
P2025고치기나 지우기가 찾는 그 행이 없음조건 칸, 프리뷰 빈 표
P2014필수 관계를 깨는 변경 (클라이언트 쪽 메시지)관계 모드, 필수 관계 이름
P2021, P2022표 없음, 칸 없음마이그레이션 배포, 스키마 이름

P2014는 예전 클라이언트 검사나 관계 모드를 프리즈마로 둔 설정에서 필수 관계를 깨면 납니다. 디비 외래키가 살아있는 기본 모드에선 같은 장면이 P2003으로 옵니다. 프리즈마 참조 동작 문서도 제한(Restrict)이나 동작 없음(NoAction) 지우기가 디비에서 막히면 P2003이라고 적습니다.


유니크 중복은 P2002, 고칠 행이 없는 갱신은 P2025입니다. 여기서는 부모-자식 아이디만 봅니다. 코드를 보고 스키마를 고치기 전에, 지금 붙은 디비에 부모 행이 있는지만 먼저 확인해도 시간이 줄어듭니다.


클라이언트 생성 초록과 프리뷰 부모 행

클라이언트 생성은 타입만 만들고, 부모 행은 그 디비에 있어야 합니다. 로컬 슈퍼베이스에 사용자 세 명이 있어도 버셀 프리뷰가 붙는 프로젝트는 다른 디비입니다.


스키마에 세션의 사용자 아이디가 사용자 표 아이디를 가리키면, 마이그레이션은 제약만 만듭니다. 제약이 있어도 사용자 표가 비어 있으면 자식 넣기는 매번 거절됩니다. 표 자체가 없으면 P2021, 칸이 없으면 P2022입니다. P2003은 표와 칸과 제약이 다 있는 다음 단계입니다.


프리뷰 환경 변수의 디비 주소가 로컬과 같은 프로젝트인지, 스테이징 빈 프로젝트를 가리키는지 한 줄로 확인하세요. 넥스트 환경 변수와 버셀 환경 변수 문서 기준으로, 프리뷰 칸에 넣은 값이 빌드가 아니라 런타임 함수에 들어가야 콜백이 그 디비를 봅니다. 주소만 같고 표가 비어 있으면 연결은 되고 자식만 막힙니다.


콘솔 주소는 디비 행 밖 | 카카오 개발자 콘솔의 리다이렉트 주소가 맞아도, 프리뷰 사용자 표가 비어 있으면 세션 넣기는 P2003입니다. 콘솔을 고치기 전에 부모 표를 엽니다.


메타 칸 이름으로 부모를 찾기

메타의 칸 이름이 가리키는 자식 칸의 부모 표를 엽니다. 메시지에 세션 사용자 아이디 제약 같은 이름이 오면 세션이 사용자 표 아이디를 못 찾은 줄입니다. 모델 이름이 아니라 제약 이름이 찍히는 경우가 많아서, 스키마의 관계 칸과 맞춰 읽습니다.


흔한 실수는 카카오 회원번호(토큰의 회원 식별값, 또는 사용자 정보 아이디)를 사용자 표 기본키에 넣는 줄입니다. 기본키가 자동 아이디이고 카카오 아이디 칸이 따로 있으면, 세션의 사용자 아이디에는 그 자동 아이디가 들어가야 합니다. 회원번호를 그대로 넣으면 로컬에 우연히 같은 문자열이 없는 한 프리뷰와 로컬 모두 P2003입니다.


타입도 갈립니다. 부모 아이디가 문자열인데 자식에 숫자를 넣거나, 큰정수 칸에 문자 자동 아이디를 넣으면 디비가 그 값을 부모에서 못 찾습니다. 스튜디오에 행이 보여도 비교하는 값이 다르면 같은 거절입니다. 로컬에서 한 번 가입해 둔 행을 보고 프리뷰도 같다고 믿으면, 그 착각이 500으로 남습니다.


P2003에서 제약 이름과 부모 행을 같이 찍기
import { Prisma } from '@prisma/client' try { await prisma.session.create({ data: { userId, kakaoAccessToken }, }) } catch (e) { if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2003') { console.error('fk field', e.meta && e.meta.field_name) const parent = await prisma.user.findUnique({ where: { id: userId } }) console.error('parent row', parent ? parent.id : '없음') } throw e }
자식 칸이 부모 기본키를 가리키는 외래키 제약 도해
field_name은 자식 쪽 제약 이름이고, 그 값이 부모 표에 있어야 넣기가 통과한다

넣기, 지우기, 연결하기 세 갈래

넣기, 지우기, 연결하기 중 어느 줄이 거절됐는지로 갈립니다. 같은 P2003이라도 고치는 명령이 다릅니다.


갈래증상먼저 할 일
넣기자식만 넣다가 거절부모를 찾고, 없으면 부모부터 생성
고치기사용자 아이디를 다른 값으로 바꿈새 값이 부모에 있는지 확인
지우기부모를 지우려다 거절제한이면 자식부터, 연쇄면 스키마 확인
연결하기중첩 연결 아이디가 없음연결 대신 생성, 또는 부모 아이디를 다시 조회
잘못된 칸카카오 아이디를 사용자 아이디에 넣음사용자 기본키와 카카오 아이디를 분리

중첩 쓰기는 관계 쿼리 문서의 연결하기가 이미 있는 행만 붙입니다. 없는 아이디를 연결하면 이 코드가 납니다. 부모와 자식을 한 번에 만들 때는 중첩 생성이 맞고, 이미 로그인된 사용자만 붙일 때는 그 사용자 아이디를 조회한 뒤 연결합니다. 카카오 콜백처럼 첫 로그인과 재로그인이 섞이면, 연결하기만 쓰면 첫 방문에서 바로 거절됩니다.


kakaoId를 userId에 넣으면 거절되는 줄
// User.id 가 cuid 인데 카카오 회원번호를 넣으면 부모를 못 찾는다 await prisma.session.create({ data: { userId: kakaoSub }, // P2003 }) // User.id 를 조회한 뒤 그 값을 넣는다 const user = await prisma.user.findUnique({ where: { kakaoId: kakaoSub } }) if (!user) throw new Error('부모 User 없음') await prisma.session.create({ data: { userId: user.id }, })

카카오 콜백 500 뒤에 숨은 외래키

콜백 500은 화면이고, 로그의 P2003이 외래키입니다. 카카오 로그인 문서의 토큰 발급과 사용자 정보는 통과해도, 그 다음에 세션이나 계정 행을 넣는 서버 액션이 거절되면 사용자는 빨간 화면만 봅니다.


순서는 보통 토큰 교환, 사용자 정보, 사용자 있으면 갱신, 세션 넣기입니다. 네 번째 줄만 실패하면 콘솔 리다이렉트 주소를 만지작거리게 됩니다. 버셀 함수 로그에 P2003과 칸 이름이 있으면, 카카오 콘솔이 아니라 프리뷰 사용자 표입니다.


로컬에선 예전에 제가 한 번 로그인해 사용자가 남아 있고, 프리뷰는 마이그레이션만 돌려 표만 있는 경우가 많습니다. 콜백을 프리뷰 주소로 열면 그 사용자가 없으니 세션이 막힙니다. 토큰 응답의 아이디는 카카오 쪽 식별자고, 우리 사용자 기본키와 같은 칸이 아닙니다. 콘솔을 고치기 전에 프리뷰 스튜디오에서 사용자 표를 한 번 열어 보세요.


콜백 로그를 보는 모니터와 책상, 화면 500과 디비 외래키는 다른 칸
카카오 화면이 돌아와도 세션 넣기의 부모 User는 그 프리뷰 디비에 있어야 한다

onDelete와 트랜잭션으로 맞추기

부모를 같은 트랜잭션에서 만든 뒤 자식을 넣습니다. 콜백처럼 사용자와 세션을 연달아 쓰면, 중간에 끊기면 자식만 남은 상태가 아니라 둘 다 취소됩니다. 지우기 갈래는 스키마의 지우기 동작이 제한인지 연쇄인지부터 맞춥니다.


제한(또는 기본 동작 없음)이면 사용자를 지우기 전에 세션을 먼저 지워야 합니다. 연쇄면 사용자 지우기가 자식을 같이 지웁니다. 프리뷰에서만 거절되면 로컬 스키마에 연쇄를 적어 두고 프리뷰에는 마이그레이션이 안 들어간 경우도 있습니다. 스키마 파일과 그 디비 제약이 같아야 합니다.


혼자 돌리는 넥스트라면 있으면 갱신과 넣기를 한 트랜잭션으로 묶는 편이 짧습니다. 부모를 만든 다음 그 사용자 아이디만 자식에 넣으면, 카카오 회원번호를 외래키 칸에 넣는 실수도 줄어듭니다. 제약 이름만 보고 칸을 추측하지 말고, 스튜디오에서 부모 조회를 한 번 실행하세요.


User upsert와 Session create를 한 트랜잭션으로
const session = await prisma.$transaction(async (tx) => { const user = await tx.user.upsert({ where: { kakaoId }, create: { kakaoId, nickname }, update: { nickname }, }) return tx.session.create({ data: { userId: user.id, kakaoAccessToken }, }) }) // 스키마 예: 부모를 지우면 세션도 같이 지울 때 // user User @relation(fields: [userId], references: [id], onDelete: Cascade)
프리뷰 디비에서 부모 행과 외래키 제약 확인
SELECT id, "kakaoId" FROM "User" WHERE "kakaoId" = '카카오회원번호'; SELECT conname, conrelid::regclass AS child, confrelid::regclass AS parent FROM pg_constraint WHERE contype = 'f'; -- 자식에 넣으려던 값이 첫 쿼리 결과에 없으면 P2003이 맞다

참고 자료


내부 연계: P2025 행 없음, P2002 유니크, P2021 표 없음, P2022 칸 없음, P1001 연결


인용한 코드와 문구는 2026년 9월 공개 문서 기준입니다.


자주 묻는 질문

로컬 스튜디오엔 User가 보이는데 프리뷰만 P2003입니다.

프리뷰가 붙는 디비의 사용자 표가 비어 있는 경우가 많습니다. 클라이언트 생성 초록은 타입만 맞춘 상태라 부모 행을 만들지 않습니다. 프리뷰 디비 주소로 스튜디오를 열어 그 아이디가 있는지 보세요.


카카오 로그인은 됐는데 세션만 500입니다.

토큰과 사용자 정보는 카카오 쪽이고, 세션 넣기는 우리 디비입니다. 로그에 P2003과 칸 이름이 있으면 콘솔 리다이렉트 주소가 아니라 부모 사용자 아이디를 봅니다. 카카오 회원번호를 사용자 아이디 칸에 넣지 마세요.


P2002랑 어떻게 나누나요?

P2002는 같은 표의 유니크 칸이 이미 차서 거절한 줄입니다. P2003은 다른 표(부모)에 그 값이 없어서 거절한 줄입니다. 메타의 대상은 유니크, 메타의 칸 이름은 외래키 쪽으로 읽는 경우가 많습니다.


사용자를 지우는데 P2003이 납니다.

지우기 동작이 제한이거나 기본 동작 없음이면 자식 세션이 남아 있을 때 부모 지우기를 디비가 막습니다. 자식을 먼저 지우거나, 스키마에 연쇄를 적고 그 디비에 마이그레이션이 들어갔는지 확인하세요.


연결하기를 썼는데도 거절됩니다.

연결하기는 이미 있는 부모만 붙입니다. 프리뷰에 그 아이디가 없으면 P2003입니다. 콜백 첫 로그인이면 있으면 갱신으로 부모를 만든 뒤 그 사용자 아이디를 넣거나 중첩 생성을 쓰세요.


P1001, P2024와 같이 보이나요?

아닙니다. P1001은 호스트에 못 붙고, P2024는 풀에서 빈 칸을 못 받는 줄입니다. P2003은 연결이 된 다음, 그 디비가 외래키를 거절한 줄입니다. 타임아웃과 슬러그를 섞지 마세요.


P2003은 자식이 가리킨 부모 행이 그 디비에 없다는 뜻입니다. 메타의 칸 이름으로 칸을 읽고, 카카오 회원번호와 사용자 기본키를 나누고, 같은 트랜잭션에서 부모를 만든 뒤 자식을 넣으면 대부분 풀립니다. 관련 글: P2025, P2002, P2021, P1001.


PrismaP2003외래키foreign key슈퍼베이스버셀PostgreSQL넥스트카카오백엔드개발자

함께 보면 좋은 문제 해결

EXPLORE / Backend

이어서 읽어보기

전체 토픽 둘러보기