TechFeedTechFeed
Frontend

error.tsx, not-found, global-error, digest | 없는 글인데 500이 뜨면?

없는 글인데 500이 뜨면 행이 없을 때 throw 한 것이다. notFound는 없음 칸을 열고, 에러 파일은 예기치 않은 예외만 받는다. 같은 폴더 레이아웃은 에러 파일이 안 감싸고, 프로덕션은 digest로 서버 로그와 맞춘다. 개발자, Next.js, 프론트엔드, React, API, 버셀 기준으로 404와 500 칸만 보고 폼 훅 글, 프록시 글과 자리를 섞지 않는다. 2026년 8월 공식 문서.

by

없는 글 주소인데 화면이 500으로 뜨면, 행이 없을 때 일반 에러를 던진 겁니다. 없는 자원은 notFound를 렌더 길에서 부릅니다. 그 함수가 없음 칸을 열고, 에러 파일은 예기치 않은 예외만 받습니다. 트라이캐치로 감싸면 그 예외가 막혀 404 칸이 안 열립니다. 프로덕션에서는 서버 메시지 대신 다이제스트만 남습니다.


저는 12개 사이트 글 주소를 옮긴 뒤, 옛 슬러그가 에러 화면으로 나갔습니다. 디비에 행이 없는데 throw를 썼기 때문입니다. 제출 중 버튼은 폼 훅 글, 응답 뒤 메일은 after 글입니다. 앱 라우터 전체 점검은 체크리스트 글입니다. 여기는 없는 글과 터진 렌더를 가르는 칸만 봅니다.


숫자는 2026년 8월 넥스트 에러 처리 문서notFound 함수 문서 기준입니다.


없는 글을 throw 하면 404가 아니라 에러 칸이다

글이 없으면 에러를 던지지 않습니다. notFound를 부르면 없음 파일이 열리고, 일반 throw는 에러 파일이 받습니다.


넥스트 에러 처리 문서는 예상된 실패와 예기치 않은 예외를 나눕니다. 폼 검증 실패, 없는 슬러그는 예상된 실패입니다. 예상된 실패는 반환 값이나 notFound로 끝냅니다. 렌더 중에 터진 버그만 에러 경계가 받습니다. 저는 옛 슬러그 페이지에서 throw new Error('not found')를 썼다가, 검색 봇이 500을 받았습니다. 한글 없음 문구를 만들어 둔 파일은 한 번도 안 열렸습니다.


언제 쓰나열리는 파일상태 코드
notFound행이 없음, 주소가 없음not-found스트리밍 전이면 404
throw디비 끊김, 코드 버그error500 쪽
루트 레이아웃 throw공통 셸이 터짐global-error500 쪽
액션 반환 값문의 폼 검증같은 화면 메시지페이지는 그대로

제출 중 잠금과 검증 메시지는 폼 훅 글입니다. 오늘은 페이지가 열릴 때 행이 없는 칸만 봅니다. 관리자 경로 잠금은 프록시 글입니다. 그 글은 화면과 액션 입구입니다. 없음과 권한 거부는 자리가 다릅니다.


없음은 에러가 아닙니다 | 검색이 옛 주소를 따라오면 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만 있어서, 복사하면 동작은 해도 데이터를 안 다시 받습니다.


다이제스트만 화면에 두고 원문은 서버 로그에서 본다
// https://nextjs.org/docs/app/api-reference/file-conventions/error 'use client' import { useEffect } from 'react' export default function Error({ error, retry }) { useEffect(() => { console.error(error.digest) }, [error]) return ( <div> <h2>저장은 됐을 수 있으니 새로고침해 주세요</h2> {error.digest ? <p>코드 {error.digest}</p> : null} <button type="button" onClick={() => retry()}>다시 시도</button> </div> ) }

프로덕션에서 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
  • not-found 파일 | 스트리밍 200과 비스트리밍 404, 전역 없음 실험 플래그
  • notFound 함수 | 렌더 길에서 호출, 트라이캐치가 삼킴, 미대기 프라미스
  • 구성 위치 | 에러가 로딩과 페이지를 감싸는 위치
  • 리액트 에러 경계 | 렌더 중 에러, 이벤트 핸들러는 제외
  • 액션 상태 훅 | 예상된 폼 실패는 반환 값
  • catchError | 라우트 파일이 아닌 컴포넌트 단위 경계
  • 넥스트 블로그 | 16.3 retry 안정 등 릴리스 메모

내부 연계: 폼 훅, after, 프록시, 캐시 컨트롤, 서버 액션, 센트리, 앱 라우터 체크리스트, 플레이라이트


없는 슬러그와 다이제스트가 표보다 우선입니다. 위 동작은 2026년 8월 공개 문서 기준입니다.


자주 묻는 질문

없는 글인데 에러 파일이 열리면?

행이 없을 때 throw를 쓴 겁니다. notFound로 바꿉니다. 트라이캐치가 있으면 그 예외를 다시 던지는지 봅니다. 화면이 한글 없음으로 바뀌면 통과입니다.


에러 파일에 use client가 왜 필요한가?

에러 경계는 클라이언트 컴포넌트여야 합니다. 서버 컴포넌트로 두면 파일이 안 붙습니다. 전역 에러도 같습니다. 메타데이터 export는 전역 에러에서 못 쓰니 title 태그로 둡니다.


레이아웃이 터져도 세그먼트 에러가 받나?

같은 폴더 레이아웃은 안 받습니다. 자식 페이지만 받습니다. 루트 셸이 터지면 global-error입니다. 헤더 쿼리를 레이아웃에 두면 그 실패는 전역 칸입니다.


프로덕션에서 에러 문구가 비는 이유는?

서버 원문을 숨깁니다. digest로 버셀 로그와 맞춥니다. 한글 안내 문장은 직접 적습니다. 센트리로 모을 거면 그 글의 SDK 칸을 따릅니다.


retry와 reset 중 무엇을 쓰나?

넥스트 16.3부터 retry가 안정입니다. 다시 가져와 그립니다. reset은 가져오기 없이 자식만 다시 그립니다. 일시 장애 버튼은 retry입니다.


문의 폼 실패도 에러 파일로 보내나?

보내지 않습니다. 검증 실패는 액션이 메시지를 반환하고 같은 화면에 그립니다. 에러 파일은 렌더가 터진 자리입니다. 제출 중 버튼은 폼 훅 글입니다.


error.tsxnot-foundglobal-errordigestNext.js프론트엔드ReactAPI개발자버셀404에러 경계앱 라우터

관련 도구

함께 보면 좋은 문제 해결

EXPLORE / Frontend

이어서 읽어보기

전체 토픽 둘러보기