Next.js 교재 · 11편 · loading·error

loading.tsx·error.tsx — 로딩과 에러 UI 분리

useState·try-catch 없이 파일 두 개로 UX 80% 끝.

로딩 스피너와 에러 폴백이 페이지 자리에 나타나는 컨셉 일러스트

옛 React 에선 모든 페이지마다 같은 패턴을 반복해야 했다 — useState({loading: false, error: null, data: null}) 만들고, fetch 호출 전후로 setState, JSX 안에서 {loading ? <Spinner /> : error ? <Error /> : <Real />} 분기. 10번 페이지 만들면 같은 코드 10번.

App Router 의 답이 특수 파일로 분리. loading.tsxerror.tsx 를 한 폴더에 두면 — 그 폴더 안 모든 페이지가 자동으로 그 UI 를 쓴다. 따로 import 도 분기도 없음. React 의 <Suspense>ErrorBoundary 를 Next 가 자동으로 감싸준다.

1. loading.tsx — 자동 로딩 UI

가장 단순한 예. app/dashboard/loading.tsx 만 만들면 끝.

// app/dashboard/loading.tsx export default function Loading() { return ( <div className="skeleton"> <div className="skeleton-card" /> <div className="skeleton-card" /> <div className="skeleton-card" /> </div> ); } // app/dashboard/page.tsx export default async function DashboardPage() { const stats = await fetchSlowStats(); // 1초 걸림 return <Dashboard stats={stats} />; }

사용자가 /dashboard 로 이동하면 — Next 가 자동으로 먼저 loading 컴포넌트를 즉시 렌더, 백그라운드에서 fetchSlowStats 가 끝나면 진짜 페이지로 교체. useState·useEffect 한 줄 없이.

실제 메커니즘 — Next 가 내부적으로 <Suspense fallback={<Loading />}><Page /></Suspense> 로 자동 감싼다. React Suspense 의 표준 패턴을 파일 시스템 컨벤션으로 단순화한 것. 익숙해지면 옛 useState 분기 코드가 끔찍해 보인다.

2. error.tsx — 자동 에러 폴백

같은 패턴. app/dashboard/error.tsx 만 만들면 그 폴더의 모든 페이지가 에러 발생 시 이 UI 로 자동 대체.

// app/dashboard/error.tsx 'use client'; // ← 반드시 client (재시도 버튼이 인터랙티브하니까) export default function ErrorBoundary({ error, reset, }: { error: Error; reset: () => void; }) { return ( <div className="error-state"> <h2>뭔가 잘못됐어요</h2> <p>{error.message}</p> <button onClick={() => reset()}>다시 시도</button> </div> ); }

두 가지를 받는다 — error(에러 객체) 와 reset(재시도 함수). reset() 을 호출하면 Next 가 그 에러 바운더리 안쪽을 다시 렌더. 사이드 메뉴는 그대로, 깨진 영역만 다시 시도.

'use client' 를 적어야 한다. 재시도 버튼이 onClick 으로 인터랙티브하니까. 서버 컴포넌트에선 동작 안 함.

3. not-found.tsx — 404 전용

비슷한 결의 셋째. 동적 라우트에서 데이터가 없을 때.

// app/blog/[slug]/not-found.tsx import Link from 'next/link'; export default function NotFound() { return ( <div> <h2>글을 찾을 수 없어요</h2> <Link href="/blog">블로그 목록으로</Link> </div> ); } // app/blog/[slug]/page.tsx import { notFound } from 'next/navigation'; export default async function PostPage({ params }) { const post = await getPost((await params).slug); if (!post) notFound(); // ← 이 한 줄이 404 트리거 return <article>…</article>; }

페이지 안에서 notFound() 호출 → Next 가 즉시 가장 가까운 not-found.tsx 로 점프. throw new Error('Not Found') 같은 거 안 해도 됨.

4. 4가지 특수 UI 한 표로

파일언제'use client'
loading.tsxpage 가 async 라 데이터 기다리는 중선택 (보통 서버)
error.tsx렌더 도중 예외 throw필수 (reset 버튼)
not-found.tsxnotFound() 호출 또는 404 URL선택
global-error.tsx루트 layout 자체 깨짐필수 + <html> 포함

global-error.tsx 는 거의 마지막 안전망. 루트 app/layout.tsx 가 깨졌을 때 (드물지만 존재). 자체 <html>·<body> 를 포함해야 한다.

5. 부분 스트리밍 — 한 페이지 안에서 영역별 로딩

page 전체가 아니라 일부 컴포넌트만 로딩 처리하고 싶을 때. React <Suspense> 를 직접 사용.

// app/dashboard/page.tsx import { Suspense } from 'react'; export default function DashboardPage() { return ( <div> <h1>대시보드</h1> <StatsHeader /> {/* 즉시 렌더 */} <Suspense fallback={<ChartSkeleton />}> <SlowChart /> {/* fetch 끝날 때까지 ChartSkeleton */} </Suspense> <Suspense fallback={<TableSkeleton />}> <SlowTable /> </Suspense> </div> ); }

차트와 테이블이 독립적으로 로드되어 각자 끝나는 대로 화면에 채워진다. Streaming SSR 의 진수. 사용자는 빠르게 헤더부터 보고, 차트는 1초 뒤, 테이블은 2초 뒤 자동 채움.

흔한 함정loading.tsx 는 그 폴더 전체를 감싼다. 부분만 로딩하고 싶으면 <Suspense> 를 페이지 안에서 직접 써야. 또 error.tsx같은 레벨의 layout 은 못 잡는다 — 부모 폴더로 한 단계 올려야 잡힘.

요약 — 11편 좌표

여기까지 정리. 네 가지 특수 파일loading.tsx(자동 Suspense), error.tsx('use client' + reset, 자동 ErrorBoundary), not-found.tsx(notFound() + 404), global-error.tsx(최후 안전망). 부분 영역만 로딩 처리하려면 <Suspense> 를 페이지 안에서 직접. useState·try/catch 분기 코드가 거의 사라진다. 다음 편에서 Suspense 의 진짜 잠재력 — Streaming.

다음 편 예고 — Suspense와 스트리밍

큰 페이지를 부분 부분 빠르게 보여주는 기법. 12편.

© 2026 주나이테크(주) @JUNAITECH