개발 환경에서는 "이미 사용 중인 아이디입니다" 같은 에러 메시지가 잘 뜨는데,
프로덕션에 배포하면 갑자기 이런 메시지로 바뀐 경험이 있을 것이다.
An error occurred in the Server Components render.
The specific message is omitted in production builds to avoid leaking sensitive details.
원인은 Next.js의 의도적인 보안 정책이다.
왜 이런 일이 생기나
Next.js의 Server Function('use server')은 이름 그대로 서버에서 실행되는 함수다.
클라이언트에서 호출하면 내부적으로 HTTP 요청을 만들어 서버에서 실행한다.
'use server'
export async function createAccount(body: CreateAccountBody) {
await apiFetch('/api/accounts/admins', { method: 'POST', body })
// 실패하면 여기서 ApiError("이미 사용중인 아이디 또는 이메일입니다.") throw
}
'use client'
try {
await createAccount({ ... })
} catch (err) {
setError(err instanceof Error ? err.message : '오류가 발생했습니다.')
// 개발: "이미 사용중인 아이디 또는 이메일입니다."
// 프로덕션: "An error occurred in the Server Components render..."
}
개발 환경에서는 왜 됐나
Next.js는 개발 모드(next dev)에서 Server Function이 throw한 에러의 메시지를
그대로 클라이언트에 전달한다. 디버깅 편의를 위해서다.
프로덕션에서는 왜 안되나
프로덕션(next build && next start)에서는 Next.js가 Server Function 에러를
의도적으로 검열한다.
이유는 보안이다. 서버에서 throw된 에러 메시지에는 DB 스키마, 내부 파일 경로,
라이브러리 버전 등 민감한 서버 정보가 섞일 수 있다.
이를 클라이언트에 그대로 노출하면 공격자에게 힌트를 줄 수 있기 때문에,
Next.js는 프로덕션에서 모든 Server Function 에러 메시지를 제네릭 문자열로 교체한다.
해결 방법
에러를 throw하는 대신 return값으로 전달하면 된다.
return값은 일반 데이터로 취급되어 프로덕션에서도 검열 없이 클라이언트에 전달된다.
실제로 Next.js 공식 문서도 이 방식을 명시적으로 권장하고 있다.
*"For these errors, avoid using try/catch blocks and throw errors.*
Instead, model expected errors as return values."
// ❌ 프로덕션에서 메시지가 사라짐
export async function createAccount(body: CreateAccountBody): Promise<void> {
await apiFetch('/api/accounts/admins', { method: 'POST', body })
}
// ✅ 프로덕션에서도 메시지가 전달됨
export async function createAccount(body: CreateAccountBody): Promise<{ error?: string }> {
try {
await apiFetch('/api/accounts/admins', { method: 'POST', body })
return {}
} catch (err) {
return { error: err instanceof Error ? err.message : '오류가 발생했습니다.' }
}
}
클라이언트 컴포넌트도 그에 맞게 수정한다.
// ❌ 기존
try {
await createAccount({ ... })
alert('등록되었습니다.')
} catch (err) {
setError(err instanceof Error ? err.message : '오류가 발생했습니다.')
}
// ✅ 수정 후
try {
const result = await createAccount({ ... })
if (result.error) { setError(result.error); return }
alert('등록되었습니다.')
} catch (err) {
setError(err instanceof Error ? err.message : '오류가 발생했습니다.')
}
try-catch는 남겨두는 게 좋다. 네트워크 오류 등 예상치 못한 예외가 여전히 발생할 수 있기 때문이다.
정리
| 개발(dev) | 프로덕션 | |
|---|---|---|
| throw된 에러 메시지 | 그대로 전달 | 제네릭 메시지로 교체 |
| return된 값 | 그대로 전달 | 그대로 전달 |
Server Function에서 사용자에게 보여줄 에러는 throw가 아니라 return으로 전달해야 한다.
로그인 실패, 중복 아이디, 유효성 검사 실패처럼 사용자에게 명확한 피드백이 필요한 경우라면
이 패턴을 일관되게 적용하는 것을 권장한다.
'개발 & IT > 프론트엔드' 카테고리의 다른 글
| 서버가 호출하는 API와 브라우저가 호출하는 API는 다르다 : NEXTJS (0) | 2026.05.14 |
|---|---|
| Next.js 캐싱의 기본값과 동적 렌더링 전환 조건 (0) | 2026.04.27 |
| Next.js의 redirect() vs router.push() — nginx 리버스 프록시 + basePath 환경에서의 함정 (0) | 2026.04.06 |
| Vue 개발자가 Next.js를 배우며 겪은 혼란: Server Component와 Client Component 이해하기 (1) | 2026.01.22 |
| JavaScript 개발자가 Flutter의 copyWith를 이해하기까지 (1) | 2026.01.06 |