iBetter Books
수정

Ch 06. 길을 잃었을 때 — error.tsx와 loading.tsx

완벽한 여행은 없다

어떤 앱이든 세 가지 상태가 있습니다. 정상적으로 동작하는 상태, 데이터를 불러오는 중인 상태, 그리고 뭔가 잘못된 상태.

지금까지는 정상 상태만 다뤘습니다. 이번 챕터에서는 나머지 두 상태, 로딩과 에러를 어떻게 처리하는지 살펴봅니다. Next.js는 이를 위해 특별한 파일명을 예약해두었습니다.

loading.tsx — 기다리는 동안 보여줄 화면

서버에서 데이터를 가져오는 동안 사용자는 빈 화면을 봅니다. 아무런 반응이 없으면 사용자는 "이 앱이 멈췄나?"라고 생각할 수 있습니다. loading.tsx는 데이터를 기다리는 동안 보여줄 UI입니다.

// 파일: app/posts/loading.tsx
export default function PostsLoading() {
  return (
    <div>
      <h1>블로그</h1>
      <ul>
        {Array.from({ length: 5 }).map((_, i) => (
          <li
            key={i}
            style={{
              height: '80px',
              background: '#f0f0f0',
              marginBottom: '16px',
              borderRadius: '8px',
              animation: 'pulse 1.5s infinite',
            }}
          />
        ))}
      </ul>
    </div>
  );
}
```text

`loading.tsx`를 같은 폴더에 두면, `page.tsx`가 데이터를 가져오는 동안 자동으로 이 컴포넌트가 표시됩니다. 내부적으로 React의 `Suspense`를 사용합니다. 개발자가 `Suspense`를 직접 쓰지 않아도 됩니다.

### `error.tsx` — 뭔가 잘못됐을 때

API 호출이 실패하거나, 예상치 못한 에러가 발생하면 사용자에게 에러 화면을 보여줍니다. `error.tsx`는 React의 Error Boundary 역할을 합니다.

중요한 점이 있습니다. `error.tsx`는 반드시 `"use client"`를 선언해야 합니다. 에러를 리셋하는 `reset` 함수를 사용하기 위해 클라이언트 컴포넌트여야 합니다.

```tsx
// 파일: app/posts/error.tsx
'use client';

import { useEffect } from 'react';

interface ErrorProps {
  error: Error & { digest?: string };
  reset: () => void;
}

export default function PostsError({ error, reset }: ErrorProps) {
  useEffect(() => {
    // 에러 로깅 서비스에 보고
    console.error('게시글 로드 에러:', error);
  }, [error]);

  return (
    <div
      style={{
        padding: '48px',
        textAlign: 'center',
      }}
    >
      <h2>게시글을 불러오지 못했습니다</h2>
      <p>{error.message}</p>
      <button
        onClick={reset}
        style={{
          marginTop: '16px',
          padding: '8px 24px',
          cursor: 'pointer',
        }}
      >
        다시 시도
      </button>
    </div>
  );
}
```text

`reset` 함수를 호출하면 페이지를 다시 렌더링 시도합니다. 일시적인 네트워크 오류라면 재시도로 해결될 수 있습니다.

### `not-found.tsx` — 존재하지 않는 페이지

존재하지 않는 URL에 접근했을 때 표시할 페이지입니다. Next.js는 기본 404 페이지를 제공하지만, 직접 커스터마이징할 수 있습니다.

```tsx
// 파일: app/not-found.tsx
import Link from 'next/link';

export default function NotFoundPage() {
  return (
    <div
      style={{
        minHeight: '60vh',
        display: 'flex',
        flexDirection: 'column',
        alignItems: 'center',
        justifyContent: 'center',
      }}
    >
      <h1 style={{ fontSize: '72px', margin: 0 }}>404</h1>
      <p>찾으시는 페이지가 없습니다.</p>
      <Link href="/">홈으로 돌아가기</Link>
    </div>
  );
}
```text

### `notFound()` 함수로 404 트리거하기

데이터가 없는 경우 프로그래밍적으로 404를 발생시킬 수 있습니다.

```tsx
// 파일: app/posts/[slug]/page.tsx
import { notFound } from 'next/navigation';

interface Props {
  params: Promise<{ slug: string }>;
}

export default async function PostPage({ params }: Props) {
  const { slug } = await params;

  const res = await fetch(`https://api.example.com/posts/${slug}`, {
    cache: 'no-store',
  });

  if (res.status === 404) {
    notFound(); // 404 페이지로 이동
  }

  const post = await res.json();

  return (
    <article>
      <h1>{post.title}</h1>
      <div>{post.content}</div>
    </article>
  );
}
```text

`notFound()`를 호출하면 가장 가까운 `not-found.tsx`가 렌더링됩니다. 같은 폴더에 없으면 상위 폴더를 탐색하고, 최종적으로 `app/not-found.tsx`가 사용됩니다.

### 파일 계층 요약

```text
app/
├── layout.tsx        ← 모든 페이지 공통 레이아웃
├── not-found.tsx     ← 전역 404 페이지
├── error.tsx         ← 전역 에러 페이지
└── posts/
    ├── page.tsx      ← /posts 페이지
    ├── loading.tsx   ← /posts 로딩 UI
    └── error.tsx     ← /posts 에러 UI (전역보다 우선)

같은 이름의 파일이 여러 계층에 있으면, 가장 가까운 것이 우선 적용됩니다.

다음 파트에서는

다음 파트에서는 Next.js에서 가장 중요하고 낯선 개념인 Server Components를 깊이 파고듭니다. 서버와 클라이언트의 경계를 어떻게 설정하고, 왜 그런 구분이 존재하는지, 비유를 통해 자연스럽게 이해해봅니다.