error.tsx, not-found, global-error, digest | 없는 글인데 500이 뜨면?
없는 글인데 500이 뜨면 행이 없을 때 throw 한 것이다. notFound는 없음 칸을 열고, 에러 파일은 예기치 않은 예외만 받는다. 같은 폴더 레이아웃은 에러 파일이 안 감싸고, 프로덕션은 digest로 서버 로그와 맞춘다. 개발자, Next.js, 프론트엔드, React, API, 버셀 기준으로 404와 500 칸만 보고 폼 훅 글, 프록시 글과 자리를 섞지 않는다. 2026년 8월 공식 문서.
없는 글 주소인데 화면이 500으로 뜨면, 행이 없을 때 일반 에러를 던진 겁니다. 없는 자원은 notFound를 렌더 길에서 부릅니다. 그 함수가 없음 칸을 열고, 에러 파일은 예기치 않은 예외만 받습니다. 트라이캐치로 감싸면 그 예외가 막혀 404 칸이 안 열립니다. 프로덕션에서는 서버 메시지 대신 다이제스트만 남습니다.
저는 12개 사이트 글 주소를 옮긴 뒤, 옛 슬러그가 에러 화면으로 나갔습니다. 디비에 행이 없는데 throw를 썼기 때문입니다. 제출 중 버튼은 폼 훅 글, 응답 뒤 메일은 after 글입니다. 앱 라우터 전체 점검은 체크리스트 글입니다. 여기는 없는 글과 터진 렌더를 가르는 칸만 봅니다.
글이 없으면 에러를 던지지 않습니다. notFound를 부르면 없음 파일이 열리고, 일반 throw는 에러 파일이 받습니다.
넥스트 에러 처리 문서는 예상된 실패와 예기치 않은 예외를 나눕니다. 폼 검증 실패, 없는 슬러그는 예상된 실패입니다. 예상된 실패는 반환 값이나 notFound로 끝냅니다. 렌더 중에 터진 버그만 에러 경계가 받습니다. 저는 옛 슬러그 페이지에서 throw new Error('not found')를 썼다가, 검색 봇이 500을 받았습니다. 한글 없음 문구를 만들어 둔 파일은 한 번도 안 열렸습니다.
칸
언제 쓰나
열리는 파일
상태 코드
notFound
행이 없음, 주소가 없음
not-found
스트리밍 전이면 404
throw
디비 끊김, 코드 버그
error
500 쪽
루트 레이아웃 throw
공통 셸이 터짐
global-error
500 쪽
액션 반환 값
문의 폼 검증
같은 화면 메시지
페이지는 그대로
제출 중 잠금과 검증 메시지는 폼 훅 글입니다. 오늘은 페이지가 열릴 때 행이 없는 칸만 봅니다. 관리자 경로 잠금은 프록시 글입니다. 그 글은 화면과 액션 입구입니다. 없음과 권한 거부는 자리가 다릅니다.
없음은 에러가 아닙니다 | 검색이 옛 주소를 따라오면 500은 장애로 집계됩니다. 행이 없는 주소는 notFound로 404 칸을 엽니다.
notFound는 렌더 길에서 불러야 칸이 열린다
notFound는 예외를 던져 렌더를 멈춥니다. 컴포넌트나 그 컴포넌트가 await한 함수 안에서 부릅니다.
함수 문서는 이 호출이 NEXT_HTTP_ERROR_FALLBACK;404를 던진다고 적습니다. return을 붙일 필요는 없습니다. 트라이캐치로 감싸면 그 예외가 삼켜져 없음 파일이 안 열립니다. 프라미스만 띄우고 await를 빼면 개발 서버에 처리되지 않은 거부가 남고, 화면은 404가 아닙니다. 넥스트 15부터 params는 프라미스입니다. slug를 꺼낸 뒤 글을 찾고, 없을 때만 부릅니다.
스트리밍이 이미 시작된 뒤에 부르면 셸은 200으로 나갑니다. 문서도 상태 코드를 못 바꾼다고 적습니다. 대신 노인덱스 메타가 붙습니다. 진짜 404가 필요하면 응답이 나가기 전에 존재를 확인합니다. 캐시 헤더 칸은 캐시 컨트롤 글입니다. 오늘은 상태 숫자가 어디서 고정되는지만 봅니다.
글이 없으면 notFound, 그 외 실패만 throw
// https://nextjs.org/docs/app/api-reference/functions/not-found
import { notFound } from 'next/navigation'
async function getPost(slug) {
const res = await fetch('https://example.com/posts/' + slug)
if (res.status === 404) {
notFound()
}
if (!res.ok) {
throw new Error('post load failed: ' + res.status)
}
return res.json()
}
export default async function Page({ params }) {
const { slug } = await params
const post = await getPost(slug)
return <article><h1>{post.title}</h1></article>
}
트라이캐치가 404를 삼킵니다 | notFound도 예외입니다. 근처에서 잡으면 없음 화면이 안 나옵니다. 다른 에러만 잡고, 이 예외는 다시 던져야 칸이 열립니다.
행이 없을 때 throw 하면 없음 파일이 아니라 에러 파일이 열린다
에러 파일은 같은 폴더 레이아웃을 안 감싼다
에러 파일은 클라이언트 컴포넌트입니다. 같은 세그먼트의 페이지와 중첩 레이아웃은 감싸고, 바로 위 레이아웃은 안 감쌉니다.
파일 관례 문서는 에러 파일이 로딩, 없음, 페이지, 자식 레이아웃을 감싼다고 적습니다. 같은 폴더 레이아웃이 터지면 그 파일은 못 받습니다. 루트 레이아웃이 터지면 전역 에러 파일이 받습니다. 전역 에러는 루트 셸을 통째로 바꿉니다. html과 body를 직접 써야 하고, 전역 스타일은 안 따라옵니다. 넥스트 15.2부터는 개발에서도 이 화면이 뜹니다. 예전에는 프로덕션에서만 보여 로컬에서 빈 화면으로 착각했습니다.
버튼 클릭 안의 에러는 경계가 못 받습니다. 렌더 중에 터진 것만 받습니다. 클릭은 상태 값으로 메시지를 그립니다. 서버 액션 자리는 서버 액션 글입니다. 액션 예상 실패는 throw가 아니라 반환 값입니다.
세그먼트 에러와 루트 전역 에러
// https://nextjs.org/docs/app/api-reference/file-conventions/error
'use client'
export default function Error({ error, retry }) {
return (
<div>
<h2>화면을 다시 불러 주세요</h2>
<p>{error.digest ? '코드 ' + error.digest : '잠시 후 다시 시도'}</p>
<button type="button" onClick={() => retry()}>다시 시도</button>
</div>
)
}
// app/global-error.js 는 html 과 body 를 직접 둔다
// 'use client'
// export default function GlobalError({ error, retry }) {
// return (
// <html>
// <body>
// <h2>화면을 다시 불러 주세요</h2>
// <button type="button" onClick={() => retry()}>다시 시도</button>
// </body>
// </html>
// )
// }
프로덕션에서 메시지가 가려지면 digest를 본다
서버에서 난 에러는 프로덕션에서 원문 메시지를 안 줍니다. 화면에는 다이제스트가 남고, 그 해시로 서버 로그와 맞춥니다.
개발에서는 원문이 직렬화되어 디버깅이 쉽습니다. 배포 뒤에는 시크릿이 섞일 수 있어 일반 문구로 바뀝니다. 에러 파일 문서는 error.digest로 서버 쪽 기록과 짝을 맞추라고 적습니다. 버셀 함수 로그에 같은 해시가 있습니다. 센트리로 런타임을 모으는 칸은 센트리 글입니다. 그 글은 SDK와 소스맵입니다. 오늘은 화면에 무엇을 찍을지만 봅니다.
넥스트 16.3부터 retry가 안정입니다. 누르면 경계를 다시 가져와 그립니다. reset은 다시 가져오지 않고 자식만 다시 그립니다. 일시 장애면 retry가 맞습니다. 예전 예시는 reset만 있어서, 복사하면 동작은 해도 데이터를 안 다시 받습니다.
프로덕션에서 error.message를 그대로 그리지 않습니다 | 서버 원문은 가려집니다. 한글 안내와 다이제스트만 두고, 원문은 버셀 로그에서 봅니다.
서버 에러 원문은 화면에 안 나오고, 다이제스트로 로그와 짝을 맞춘다
스트리밍이 시작된 404는 상태 코드가 200으로 남을 수 있다
셸이 먼저 나가면 상태 숫자는 이미 200입니다. 뒤에서 notFound를 불러도 숫자는 안 바뀝니다.
없음 파일 문서는 스트리밍 응답은 200, 스트리밍 전이면 404라고 적습니다. 서스펜스 안에서 글을 기다리다 없으면, 바깥 셸은 이미 보낸 뒤입니다. 노인덱스 메타가 붙어 검색에는 안 쌓이게 막는 쪽입니다. 네이버가 본 숫자가 200이면 색인 신호가 섞입니다. 글 존재 여부를 페이지 맨 앞에서 await하면 숫자가 404로 나갑니다. 플레이라이트로 상태 숫자를 보는 칸은 플레이라이트 글입니다.
주소 자체가 라우트에 없으면 루트 없음 파일이 받습니다. 실험 플래그 전역 없음 파일은 15.4에 들어왔고, 여러 루트 레이아웃이 있을 때 씁니다. 형제 사이트처럼 앱이 하나면 루트 not-found면 충분합니다. 헤더 정책은 보안 헤더 글입니다. 에러 화면에도 같은 헤더가 붙는지는 배포 응답에서 한 번 봅니다.
404 숫자를 지키려면 스트리밍 전에 존재를 확인
// https://nextjs.org/docs/app/api-reference/functions/not-found
import { notFound } from 'next/navigation'
export default async function Page({ params }) {
const { slug } = await params
const post = await getPostBySlug(slug)
if (!post) {
notFound()
}
return <article>{post.title}</article>
}
// 서스펜스 자식에서 부르면 셸은 이미 200
// 검색에 안 쌓이게 noindex 는 붙는다
// 숫자 자체 404가 필요하면 위처럼 페이지에서 먼저 확인
오늘 글 페이지에 notFound 한 줄만 넣는다
없는 슬러그 하나를 직접 엽니다. 한글 없음 화면이면 통과입니다. 에러 파일이 열리면 throw를 notFound로 바꿉니다.
글 폴더에 not-found와 error를 같이 둡니다. 루트에는 global-error에 html과 body를 둡니다. 문의 폼 검증 실패는 에러 파일로 보내지 않습니다. 반환 값으로 같은 칸에 메시지를 그립니다. 그 자리는 폼 훅 글입니다. 응답이 나간 뒤 메일과 로그는 after 글입니다. 두 글과 오늘 칸을 한 파일에 섞지 않습니다.
로컬에서 한 번 터뜨릴 때는 개발 오버레이를 끄고 에러 파일이 한글인지 봅니다. 배포 미리보기에서 없는 주소를 열어 상태 숫자와 노인덱스를 확인합니다. 다이제스트가 보이면 버셀 로그에서 같은 해시를 찾습니다.
오늘 넣을 최소 세 파일
// app/posts/[slug]/page.js
import { notFound } from 'next/navigation'
export default async function Page({ params }) {
const { slug } = await params
const post = await getPostBySlug(slug)
if (!post) notFound()
return <article>{post.title}</article>
}
// app/posts/[slug]/not-found.js
export default function NotFound() {
return <p>글을 찾을 수 없습니다</p>
}
// app/posts/[slug]/error.js 는 맨 위에 'use client'
// app/global-error.js 는 html, body 필수
같은 글 폴더에 not-found와 error를 두고, 루트에만 global-error를 둔다
참고 자료
에러 처리 | 예상된 실패와 예외, notFound, 에러 경계, 전역 에러. 2026년 8월 확인
error 파일 | 같은 세그먼트 레이아웃은 안 감쌈, digest, retry, 전역 에러 html