본문으로 건너뛰기
개발 머꼬
개발 노트Next.js
hohyeon.dev20

notFound()를 호출했는데 404 화면이 안 뜨고 그냥 넘어간 이유

  • #Engineering Note
  • #Next.js

문제 발생

데이터 조회를 안전하게 감싸려고 try/catch를 둘렀더니, 없는 글에 접근해도 404 화면이 뜨지 않고 그냥 아래로 흘러갔습니다.

try {
  const post = await getPost(slug);
  if (!post) notFound();          // 여기서 던진 예외를
  return <Article post={post} />;
} catch (error) {
  console.error(error);           // 여기가 삼킨다
  return <ErrorView />;
}

원인 분석

notFound()는 값을 반환하는 함수가 아니라 예외를 던지는 함수입니다. 공식 문서에 따르면 NEXT_HTTP_ERROR_FALLBACK;404 에러를 던져 그 라우트 세그먼트의 렌더링을 중단시키고, Next.js가 <meta name="robots" content="noindex" />도 함께 넣어 색인되지 않게 합니다.

예외이므로 다른 예외와 똑같이 콜 스택을 타고 올라갑니다. 중간에 try/catch가 있으면 거기서 잡히고, not-found UI는 렌더되지 않습니다. redirect()도 같은 방식으로 동작합니다.

부수적으로 알아 둘 점이 둘 있습니다.

  • return notFound()라고 쓸 필요가 없습니다. 호출만으로 실행이 멈추고, TypeScript도 반환 타입이 never인 것을 이해하므로 그 뒤에서 값이 좁혀진 상태로 남습니다.
  • 렌더 경로에서 호출해야 합니다. 컴포넌트나 컴포넌트가 await하는 함수 안이어야 합니다. await하지 않은 프로미스 안에서 던지면 아무도 잡지 않아 not-found UI가 렌더되지 않습니다(개발 서버 로그에 unhandledRejection으로 남습니다).

해결 방안

  1. 호출 지점을 try/catch 밖으로 뺍니다. 가장 단순하고 확실합니다.
const post = await getPost(slug);   // 내부 오류는 여기서 처리
if (!post) notFound();              // catch 밖
return <Article post={post} />;
  1. 꼭 감싸야 한다면 unstable_rethrow로 통과시킵니다. Next.js의 내부 제어 흐름 예외를 먼저 다시 던지고, 나머지만 처리합니다.
import { unstable_rethrow } from "next/navigation";

try {
  await mutate();
} catch (error) {
  unstable_rethrow(error);   // notFound/redirect는 통과
  logToSentry(error);
  return <ErrorView />;
}
  1. 데이터 접근 함수 안에서 호출하는 편이 낫습니다. 조회하는 쪽이 "없음"을 가장 먼저 알기 때문에, 화면마다 같은 검사를 반복하지 않아도 됩니다.
  2. HTTP 상태 코드를 신경 쓴다면 검사 위치가 중요합니다. <Suspense> 안에서 스트리밍이 시작된 뒤에 호출하면 응답은 이미 200으로 나간 상태라 상태 코드를 바꿀 수 없습니다. 진짜 404 상태가 필요하면 스트리밍 전에 판정해야 합니다.
  3. Server Action에서도 같은 함정이 있습니다. 액션 전체를 try/catch로 감싸는 습관이 있다면 redirect()가 조용히 무력화됩니다 — 성공 후 리다이렉트가 안 되는 증상의 흔한 원인입니다.

공식 문서

마지막 수정

좋아요북마크

댓글0

아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.