Web Devlopment/NextJs
#18. Loading UI , Error Handling
키모형
2025. 12. 28. 10:41
반응형
Next.js Special files
- page.tsx
- layout.tsx
- template.tsx
- not-found.tsx
- loading.tsx - loading states
- error.tsx - error handling
Next.js(App Router)에서 loading.tsx는 해당 라우트 세그먼트가 “서버에서 렌더/데이터 준비”되는 동안 보여주는 자동 fallback UI예요.
- 위치 예시
- app/loading.tsx → 전체 앱(루트) 로딩 UI
- app/movies/loading.tsx → /movies 이하에서만 로딩 UI
- app/movies/[id]/loading.tsx → /movies/123 같은 상세에서만
- 언제 뜨나
- page.tsx(Server Component)에서 await fetch(...), await db..., await new Promise(setTimeout...) 같은 느린 작업이 있을 때
- 또는 내부에서 Suspense 경계가 생겨 스트리밍이 걸릴 때
- 클라이언트 라우팅(Link 이동) 시에도 해당 세그먼트가 준비될 때까지 표시됨
- 중요한 포인트
- 이건 “진짜 % 진행률”이 아니라 대부분 indeterminate(움직이는 바) 형태가 일반적입니다.
- 세그먼트 단위로 동작해서, 어떤 부분만 늦으면 그 부분 로딩만 뜰 수 있어요(중첩 가능).
loading.tsx 예시 파일 (App Router)
1) app/movies/page.tsx
// app/movies/page.tsx
async function sleep(ms: number) {
return new Promise((r) => setTimeout(r, ms));
}
export default async function MoviesPage() {
// 로딩 UI를 확인하기 위한 인위적 지연
await sleep(1800);
return (
<main className="p-6">
<h1 className="text-2xl font-bold">Movies</h1>
<p className="mt-2 text-sm opacity-80">
이 페이지는 1.8초 뒤에 렌더됩니다. 그동안 loading.tsx가 표시됩니다.
</p>
</main>
);
}
2) app/movies/loading.tsx
// app/movies/loading.tsx
export default function Loading() {
return (
<div className="min-h-[40vh] p-6">
<h2 className="text-lg font-semibold">Loading…</h2>
<p className="mt-2 text-sm opacity-70">데이터를 불러오는 중입니다.</p>
</div>
);
}
- 로딩이 너무 빨리 끝나면(캐시/네트워크 빠름) loading이 “안 보이거나 깜빡” 할 수 있어요.
→ 위 예시처럼 sleep()로 확인하면 확실합니다. - “전 페이지 전환 내내(시작~완료) 상단바를 항상 띄우고 싶다”면
loading.tsx만으로는 “전환 시작/완료 이벤트”를 정확히 잡기 어렵고, 별도 전환바 컴포넌트(라이브러리 or 커스텀)가 필요해요.
Next.js Special files
- page.tsx
- layout.tsx
- template.tsx
- not-found.tsx
- loading.tsx - loading states
- error.tsx - error handling
error.tsx 핵심 규칙
1) 반드시 Client Component여야 함
error.tsx는 무조건 'use client' 가 필요합니다. Next.js+1
2) Props가 고정됨: error, reset
- error: JS Error 객체 (프로덕션에서는 digest가 포함될 수 있음)
- reset(): 에러 경계를 리셋하고 해당 세그먼트를 다시 렌더 시도 Next.js+1
3) “가장 가까운 위치”의 error.tsx가 잡는다
폴더 구조에서 더 가까운 error.tsx가 있으면 거기서 처리되고, 없으면 상위로 버블링됩니다. Next.js 한글 문서+1
4) 주의: 같은 세그먼트의 layout.tsx에서 난 에러는 못 잡는 경우가 있음
error.tsx는 보통 해당 세그먼트의 layout “안쪽”에 배치되는 구조라, 같은 세그먼트의 layout 자체에서 throw된 에러는 그 error.tsx가 못 잡을 수 있어요. 이런 경우는 한 단계 위 세그먼트에 error.tsx를 두거나, 루트까지면 global-error.tsx로 처리합니다.
예시 1) 리뷰 상세 라우트에서만 에러 처리
폴더 예:
src/app/(marketing)/products/[productId]/reviews/[reviewId]/
page.tsx
error.tsx
page.tsx (테스트용으로 랜덤 에러 발생)
// src/app/(marketing)/products/[productId]/reviews/[reviewId]/page.tsx
function getRandomInt(max: number) {
return Math.floor(Math.random() * max);
}
export default async function Page({
params,
}: {
params: Promise<{ productId: string; reviewId: string }>; // Next 15+ 스타일
}) {
const { productId, reviewId } = await params;
const random = getRandomInt(2);
if (random === 1) {
throw new Error('Error loading review!');
}
return (
<div style={{ padding: 24 }}>
<h1>Review</h1>
<p>
productId: {productId}, reviewId: {reviewId}
</p>
</div>
);
}
error.tsx (에러 UI + 재시도 버튼)
// src/app/(marketing)/products/[productId]/reviews/[reviewId]/error.tsx
'use client';
import { useEffect } from 'react';
import Link from 'next/link';
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
// Sentry 같은 곳에 보내도 됨
console.error(error);
}, [error]);
return (
<div style={{ padding: 24 }}>
<h2 style={{ fontSize: 18, fontWeight: 700 }}>
리뷰를 불러오는 중 문제가 발생했습니다.
</h2>
<p style={{ marginTop: 8, opacity: 0.8 }}>
잠시 후 다시 시도해 주세요.
</p>
{error.digest && (
<p style={{ marginTop: 8, fontSize: 12, opacity: 0.6 }}>
digest: {error.digest}
</p>
)}
<div style={{ marginTop: 16, display: 'flex', gap: 8 }}>
<button
onClick={() => reset()}
style={{
padding: '10px 14px',
borderRadius: 8,
border: '1px solid rgba(255,255,255,.2)',
}}
>
다시 시도
</button>
<Link href={`/products`}>
돌아가기
</Link>
</div>
</div>
);
}
- reset()은 “같은 라우트를 다시 렌더”하는 방식이라, 일시적 네트워크 오류/랜덤 오류 같은 케이스에 특히 잘 맞아요. Next.js+1
예시 2) 앱 전체(루트)까지 감싸는 global-error.tsx
루트 레벨:
src/app/global-error.tsx
// src/app/global-error.tsx
'use client';
export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
// global-error는 루트 레이아웃을 "대체"하기 때문에 html/body를 직접 포함해야 함
return (
<html lang="ko">
<body style={{ padding: 24 }}>
<h1 style={{ fontSize: 20, fontWeight: 800 }}>치명적인 오류가 발생했습니다.</h1>
{error.digest && <p style={{ opacity: 0.6 }}>digest: {error.digest}</p>}
<button onClick={() => reset()} style={{ marginTop: 12 }}>
다시 시도
</button>
</body>
</html>
);
}
global-error.tsx는 전체 앱을 감싸고, 활성화되면 root layout 자체를 대체하는 개념입니다. 그래서 <html>/<body>가 필요해요. Next.js+1
자주 헷갈리는 포인트
- notFound() / not-found.tsx는 “404 흐름”이고, error.tsx는 “예외(throw) 흐름”입니다. (역할이 다름) Next.js 한글 문서
- 클릭 이벤트 핸들러 안에서 난 에러처럼 “렌더링 바깥”의 에러는 Error Boundary로 안 잡히는 경우가 있어요(React Error Boundary 성격). 이런 건 try/catch로 직접 처리하는 게 안전합니다. Next.js+1
애러 위치에 따른 error 처리
예시 1) app/shop/layout.tsx에서 에러 → 어디에 error.tsx를 둬야 잡히나
✅ (문제) 같은 폴더 app/shop/error.tsx로는 못 잡음
app/
shop/
layout.tsx // 여기서 throw 발생
error.tsx // ❌ layout.tsx 에러는 못 잡음
page.tsx
✅ (정답) 부모인 app/error.tsx가 잡음
app/
error.tsx // ✅ shop/layout.tsx 에러를 잡는 위치(부모 세그먼트)
shop/
layout.tsx
page.tsx
app/shop/layout.tsx (일부러 에러 발생)
// app/shop/layout.tsx
export default function ShopLayout({ children }: { children: React.ReactNode }) {
throw new Error("Shop layout crashed!");
// return <section>{children}</section>;
}
app/error.tsx (부모 세그먼트 에러 경계)
// app/error.tsx
"use client";
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<div style={{ padding: 24 }}>
<h2>페이지 로딩 중 오류가 발생했습니다.</h2>
<p style={{ opacity: 0.8 }}>message: {error.message}</p>
{error.digest && <p style={{ opacity: 0.6 }}>digest: {error.digest}</p>}
<button onClick={() => reset()} style={{ marginTop: 12 }}>
다시 시도
</button>
</div>
);
}
결론: shop/layout.tsx 에러를 잡고 싶으면 shop/error.tsx가 아니라 app/error.tsx(부모)에 둬야 합니다. Next.js+1
예시 2) “특정 layout만” 별도로 잡고 싶을 때
예를 들어 app/(marketing)/layout.tsx에서 나는 에러를 잡고 싶다면:
- (marketing)의 **부모는 app/**이므로, **app/error.tsx**가 (marketing)/layout.tsx 에러를 잡습니다.
- 만약 (marketing) 위에 또 그룹이 있다면 그 “바로 위”에 두면 됩니다. (원칙: layout의 부모 세그먼트) Next.js
예시 3) 루트 app/layout.tsx에서 에러가 나면?
- app/error.tsx는 루트 layout 에러를 못 잡습니다.
- 이때는 **app/global-error.tsx**가 필요합니다. Next.js
// app/global-error.tsx
"use client";
export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<html lang="ko">
<body style={{ padding: 24 }}>
<h1>치명적 오류가 발생했습니다.</h1>
{error.digest && <p>digest: {error.digest}</p>}
<button onClick={() => reset()}>다시 시도</button>
</body>
</html>
);
}
실전 팁
- **layout에서 “예상 가능한 오류(권한/도메인 없음 등)”**는 throw로 터뜨리기보단 notFound()/리다이렉트/명시적 처리로 가는 게 UX가 더 좋습니다. (예상 불가 예외만 error.tsx로) Next.js+1
반응형