
not found - 길 잃은 사용자를 위한 친절한 나침반 설계
현재는 존재하지 않는 페이지 혹은 잘못된 페이지로 이동할 경우 못생긴 404 페이지가 등장합니다.
이는 좋지 않은 UX의 예시로, 사용자 이탈률의 증가를 초래합니다.
그래서, 사용자가 길을 잃었을 때 조차 브랜드 경험을 제공하는 not-found.tsx를 소개합니다.
먼저, src/ 안에 app/ 폴더 안에 not-found.tsx를 생성합니다.
Next.js는 파일 시스템 기반 라우팅을 사용하기 때문에 이 이름을 가진 파일이 존재하면
404 에러가 발생했을 때 자동으로 이 컴포넌트를 랜더링합니다.
여기서 중요한 점은 이 파일을 폴더마다 따로 만들 수 있다는 것입니다.
Next.js는 에러가 발생했을 때 가장 가까운 위치의 not-found.tsx를 먼저 찾습니다.

즉, app/ 폴더 최상위에 만든 파일은 전체를 관장하는 글로벌 404 페이지가 될 것이고
특정 폴더 안에 별도의 not-found.tsx 파일을 만들면, 그 섹션 전용 404 페이지가 됩니다.
이제 다음과 같은 의문이 생깁니다.
"주소가 틀렸을 때 not-found.tsx를 랜더링하는 건 알겠는데, 주소는 맞는데 데이터가 없다면?"

이 때는 주소 자체가 유효하므로, Next.js가 자동으로 404 페이지를 띄워주지 않습니다.
그래서 우리는 코드 안에서 강제로 이건 없는 페이지라고 선언할 수 있는 함수가 필요합니다.
바로, next/navigation 패키지에서 제공하는 notFound 함수입니다.

과거 Page Router 시절에는 useRouter로 페이지 이동, 페이지 읽기, 페이지 제어를 모두 한 번에 사용했지만
App Router 도입 이후에는 아래와 같이 역할이 세분화 되었습니다.
이것이 2026년 현재 Next.js 15+ 이후의 표준입니다.

또한, notFound 함수는 서버 컴포넌트 및 클라이언트 컴포넌트에서도 호출할 수 있습니다. (주로 서버에서 사용)
loading - 기다림을 디자인하는 스트리밍 SSR
만약 우리가 만드는 쇼핑몰 프로젝트에 상품 데이터가 수천 개여서 로딩에 많은 시간이 소요된다면 어떠할까요?
아마 사용자는 이 시간동안 하얗게 멈춘 화면을 보며, 인터넷이 끊겼다고 착각할지 모릅니다.
이는 사용자 이탈을 초래합니다.
이를 해결하기 위해 loading.tsx 파일을 사용하여 데이터를 기다리는 동안 우아한 로딩 화면을 표현하는 방법을 알아보고
그 뒤에서 작동하는 스트리밍 SSR의 원리에 대해 알아봅니다.
다음과 같이 데이터를 의도적으로 랜더링을 멈추게 한다고 가정해봅시다.
await new Promise((resolve) => setTimeout(resolve, 3000))이렇게 되면 3초 동안 브라우저 탭에 스피너만 빙글빙글 돌게 됩니다. 하지만 이 때 화면은 빈 화면입니다.
이것이 우리가 피해야 할 최악의 사용자 경험(UX) 입니다.
이 문제를 해결하기 위해 loading.tsx 파일을 생성해야 합니다.
export default function GlobalLoading() {
return (
<div className="fixed inset-0 flex items-center justify-center backdrop-blur-sm z-50">
<div className="flex flex-col items-center gap-4">
<div className="w-16 h-16 border-4 border-indigo-600 animate-spin">
<p className="text-indigo-600 font-smemibold text-lg animate-pulse">
Loading amazing Products...
</p>
</div>
</div>
</div>
)
}브라우저를 새로고침 해보면, 이 로딩 컴포넌트가 3초 정도 랜더링 되었다가, 다시 원래 컴포넌트가 정상 랜더링됩니다.
이 마법같은 원리를 한번 살펴봅시다.
핵심은 React의 Suspense 기술입니다. 이는 한국어로 유예 혹은 정지를 의미합니다.
프로그래밍 측면에서는 컴포넌트의 랜더링을 데이터가 준비될때까지 미룬다는 의미로 쓰입니다.
Next.js에서는 올바른 위치에 loading.tsx 파일로 만들기만 하면
빌드 과정에서 Next.js가 자동으로 계층 구조를 변환해 랩핑하여 로딩 UI를 적용합니다.

또한, 이 과정에서 스트리밍 SSR 이란 기술이 작동합니다.
기존의 SSR은 서버가 모든 데이터를 다 가져와서
HTML을 100% 완성할 때까지 브라우저에게 아무것도 보여주지 않았습니다. 이를 Blocking이라고 합니다.
스트리밍 방식은 서버는 레이아웃이나 로딩 UI 처럼 당장 보낼 수 있는 껍데기를 브라우저로 전송합니다.
그리고, 몇 초 뒤에 데이터가 준비되면, 나머지 알맹이를 스트림으로 흘려보내서 화면을 채웁니다.

덕분에 사용자는 하얀 화면 대신 즉시 반응하는 UI를 보게되고 체감속도가 향상됩니다.
하지만 로딩 UI에서 빙글빙글 도는 스피너만으로는 부족합니다.
최근에 모던 웹 사이트는 실제 컨텐츠의 뼈대 모양을 미리 보여주는 스켈레톤 UI를 사용합니다.

이는 사용자가 컨텐츠의 배치를 미리 예상할 수 있게 하여, 체감 대기시간을 획기적으로 줄입니다.
여기서 스켈레톤 UI를 만들 때 실제 레이아웃을 유지하여, Layout Shift 현상을 완벽히 방지합니다.
Error - 예기치 못한 재앙에 대비하기
개발자가 아무리 완벽하게 코드를 구성해도 피할 수 없는 재앙이 있습니다.
바로 예기치 못한 에러입니다.

외부 시스템의 API가 갑자기 멈추거나, DB 연결이 끊어지거나, 심지어 코드의 오타 등의 에러 등이 있습니다.
이때 사용자에게 화얀 화면을 제공한다면, 사용자에게 최악의 인상 및 UX를 심어주게 됩니다.
그래서 Next.js가 제공하는 마지막 안전장치이자, 최후의 방어선인 error.tsx에 대해 알아봅니다.
시스템이 무너지는 순간에도 UX를 지켜내고, 더 나아가 시스템을 스스로 복구하는 방법에 대해 알아봅니다.
우선 멀쩡한 시스템을 고장내보겠습니다. 강제로 시스템에 실패 신호를 보냅니다.
if (Math.random() < 0.5) {
throw new Error("상품을 가져오는데 오류가 발생했습니다.")
}이 문제를 해결하기 위해 해당 폴더 안에 error.tsx 파일을 생성합니다.
그리고, error.tsx는 반드시 클라이언트 컴포넌트이어야 합니다.
그 이유는 에러는 서버에서 날 수도 있고 클라이언트에서 날 수도 있지만, 에러가 터진 후 사용자에게 보여준 UI와
사용자가 다시 클릭할 버튼 등은 브라우저에서 상호작용해야 하기 때문입니다.
error.tsx는 기본적으로 2가지 Props를 받습니다.
첫번째는 error 객체로, Javascript의 기본 에러 객체와 동일하지만, Next.js에서는 digest라는 속성이 추가됩니다.
digest는 Next.js가 서버에서 발생한 에러에 부여하는 고유한 해시 값입니다.
보안상의 이유로 실제 배포 환경에서는 민감한 에러 메시지가 실제로 전송되지 않는데 대신 이 해시값이 전송됩니다.
따라서 서버 로그에서 digest 값을 검색하여 에러 발생을 역추적할 수 있습니다.
따라서 Error의 타입을 Error와 digest 속성 객체가 포함된 객체로 설정합니다.
type Props = {
error : Error & {digest?: string}
.....
}두번째는 reset 함수로, 이는 에러 상태를 초기화하고, 해당 세그먼트의 리 랜더링을 시도하는 함수로
매개변수와 반환값이 없게 void로 정의합니다.
type Props = {
...
reset: () => void
}이 개념을 합쳐 error.tsx의 기본 구조는 다음과 같습니다.
export default function Error({ error, reset } : PageProps) {
.....
}그리고 Error 컴포넌트 안에서 useEffect를 사용하여 에러 객체가 변경될 때마다 로그에 출력합니다.
useEffect(() => {
console.error("[에러 발생] : " + error)
}, [error])다음으로, 에러 UI는 다음과 같이 구성합니다.
return (
<div
className={cn('min-h-[400px] flex flex-col items-center justify-center',
'gap-6 p-8 bg-gray-50 rounded-xl mt-8'
)}>
<div className="text-center space-y-4">
<h2 className="text-2xl font-bold text-gray-900">
이런! 문제가 발생했습니다!
</h2>
<p className="text-gray-600 max-w-md mx-auto">
상품 정보를 불러오는 도중 오류가 발생했습니다 <br />
잠시 후 다시 시도해주세요.
</p>
{
error.digest && (
<p className="text-xs text-gray-400 bg-gray-100 p-2 rounded">
Error Digest: {error.digest}
</p>
)
}
<button
onClick={() => reset()}
className="px-6 bg-red-600 text-white rounded-lg shadow-lg"
>
다시 시도하기
</button>
</div>
</div>
)가장 중요한 것은 복구 버튼인데, onClick Event Handler에 props로 받은 reset() 함수를 연결합니다.
실제 UI에서 복구하기 버튼을 누르면, 전체 페이지가 새로 고침되는 것이 아니라
에러 났던 부분만 스피너(Spinner)가 돌면서 성공 시 정상적인 페이지를 가져옵니다.
이런 이유는 Next.js가 내부적으로 React Error Boundary 기술을 사용하여 바깥 레이아웃은 영향이 없습니다.

하지만, error.tsx는 부모인 layout.tsx의 에러는 잡을 수 없으므로, Root Layout의 에러를 잡을 수 없게 됩니다.
이를 위해 최후 방어선으로 global-error.tsx 기능을 제공합니다.
이 파일은 HTML 전체를 다시 그릴 권한을 가집니다.
그러므로, 최종적으로 에러 방지 패턴은 다음 그림과 같이 구축합니다.
