# Chapter 13. Handling Errors

제 13장

## 에러 처리

이전 장에서 서버 액션을 사용하여 데이터를 변경하는 방법을 배웠습니다. 자바스크립트의 `try/catch` 문과 Next.js API를 사용하여 에러를 *우아하게* 처리하는 방법을 알아봅시다.

&#x20;

> #### 이번 장에서는...
>
> 다음과 같은 내용을 다룰 예정입니다.
>
> * 라우트 세그먼트에서 에러를 잡고 사용자에게 대체 UI를 보여주기 위해 특별한 `error.tsx` 파일을 사용하는 방법
> * 존재하지 않는 리소스에 대한 404 에러를 처리하기 위해 `notFound` 함수와 `not-found` 파일을 사용하는 방법

&#x20;

***

&#x20;

### 서버 액션에 `try/catch` 추가하기

먼저 서버 액션에 자바스크립트의 `try/catch` 문을 추가하여 에러를 우아하게 처리해봅시다.

이미 이를 알고 계시다면, 몇 분을 투자하여 서버 액션을 업데이트하거나 아래 코드를 복사할 수 있습니다:

&#x20;

<details>

<summary><strong>솔루션 보기</strong></summary>

`/app/lib/actions.ts`

```ts
export async function createInvoice(formData: FormData) {
  const { customerId, amount, status } = CreateInvoice.parse({
    customerId: formData.get('customerId'),
    amount: formData.get('amount'),
    status: formData.get('status'),
  });

  const amountInCents = amount * 100;
  const date = new Date().toISOString().split('T')[0];

  try {
    await sql`
      INSERT INTO invoices (customer_id, amount, status, date)
      VALUES (${customerId}, ${amountInCents}, ${status}, ${date})
    `;
  } catch (error) {
    return {
      message: '데이터베이스 오류: 인보이스 생성에 실패했습니다.',
    };
  }

  revalidatePath('/dashboard/invoices');
  redirect('/dashboard/invoices');
}
```

</details>

&#x20;

<details>

<summary><strong>솔루션 보기</strong></summary>

`/app/lib/actions.ts`

```ts
export async function updateInvoice(id: string, formData: FormData) {
  const { customerId, amount, status } = UpdateInvoice.parse({
    customerId: formData.get('customerId'),
    amount: formData.get('amount'),
    status: formData.get('status'),
  });

  const amountInCents = amount * 100;

  try {
    await sql`
        UPDATE invoices
        SET customer_id = ${customerId}, amount = ${amountInCents}, status = ${status}
        WHERE id = ${id}
      `;
  } catch (error) {
    return { message: '데이터베이스 오류: 인보이스 업데이트에 실패했습니다.' };
  }

  revalidatePath('/dashboard/invoices');
  redirect('/dashboard/invoices');
}
```

</details>

&#x20;

<details>

<summary><strong>솔루션 보기</strong></summary>

`/app/lib/actions.ts`

```ts
export async function deleteInvoice(id: string) {
  try {
    await sql`DELETE FROM invoices WHERE id = ${id}`;
    revalidatePath('/dashboard/invoices');
    return { message: '인보이스가 삭제되었습니다.' };
  } catch (error) {
    return { message: '데이터베이스 오류: 인보이스 삭제에 실패했습니다.' };
  }
}
```

</details>

&#x20;

`redirect`가 `try/catch` 블록 외부에서 호출되는 것을 주목하세요. 이는 `redirect`가 에러를 던지는 방식으로 작동하기 때문에 `catch` 블록에서 잡힐 것입니다. 이를 피하기 위해 `try/catch` 이후에 `redirect`를 호출할 수 있습니다. `redirect`는 `try`가 성공했을 때에만 도달 가능합니다.

이제 서버 액션에서 에러가 발생했을 때의 동작을 확인해보겠습니다. 이를 위해 이전보다 더 일찍 에러를 던져서 확인할 수 있습니다. 예를 들어, `deleteInvoice` 액션에서 함수 상단에 에러를 던져보세요:

`/app/lib/actions.ts`

```ts
export async function deleteInvoice(id: string) {
  throw new Error('인보이스 삭제에 실패했습니다.');

  // 도달할 수 없는 코드 블록
  try {
    await sql`DELETE FROM invoices WHERE id = ${id}`;
    revalidatePath('/dashboard/invoices');
    return { message: '인보이스가 삭제되었습니다.' };
  } catch (error) {
    return { message: '데이터베이스 오류: 인보이스 삭제에 실패했습니다.' };
  }
}
```

인보이스를 삭제하려고 시도하면 로컬호스트에서 에러가 표시될 것입니다.

이러한 에러는 개발 중에 잠재적인 문제를 조기에 발견할 수 있어 도움이 됩니다. 하지만 갑작스러운 실패를 피하고 애플리케이션을 계속 실행할 수 있도록 사용자에게 에러를 표시하고 싶을 것입니다.

여기서 Next.js의 [`error.tsx`](https://nextjs.org/docs/app/api-reference/file-conventions/error) 파일이 등장합니다.

&#x20;

***

&#x20;

### `error.tsx`로 모든 에러 처리하기

`error.tsx` 파일은 라우트 세그먼트에 대한 UI 경계를 정의하는 데 사용될 수 있습니다. 예기치 않은 에러에 대한 **전체 캐치** 역할을 하며 사용자에게 대체 UI를 표시할 수 있습니다.

`/dashboard/invoices` 폴더 내부에 새로운 파일 `error.tsx`를 만들고 다음 코드를 붙여넣어보세요:

`/dashboard/invoices/error.tsx`

```tsx
'use client';

import { useEffect } from 'react';

export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  useEffect(() => {
    // 선택적으로 에러를 에러 보고 서비스에 기록할 수 있습니다.
    console.error(error);
  }, [error]);

  return (
    <main className="flex h-full flex-col items-center justify-center">
      <h2 className="text-center">문제가 발생했습니다!</h2>
      <button
        className="mt-4 rounded-md bg-blue-500 px-4 py-2 text-sm text-white transition-colors hover:bg-blue-400"
        onClick={
          // 다시 시도하여 인보이스 라우트를 다시 렌더링하는 시도
          () => reset()
        }
      >
        다시 시도
      </button>
    </main>
  );
}
```

위 코드에 대해 몇 가지 주목할 점이 있습니다:

* **"use client"** - `error.tsx`는 클라이언트 컴포넌트여야 합니다.
* 두 가지 프롭을 받습니다:
  * `error`: 이 객체는 자바스크립트의 네이티브 [`Error`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error) 객체의 인스턴스입니다.
  * `reset`: 이는 에러 경계를 재설정하기 위한 함수입니다. 실행되면 함수가 라우트 세그먼트를 다시 렌더링하려고 시도합니다.

인보이스를 다시 삭제하려고 하면 다음과 같은 UI가 표시될 것입니다:

![error.tsx 파일이 수용하는 프롭을 보여주는 이미지](https://nextjs.org/_next/image?url=%2Flearn%2Flight%2Ferror-page.png\&w=1920\&q=75\&dpl=dpl_8s8Dnm8T2UqYs4Sz3a1AK4vKuj5w)

&#x20;

***

&#x20;

### `notFound` 함수로 404 에러 처리하기

에러를 우아하게 처리하는 다른 방법으로 `notFound` 함수를 사용하는 것이 있습니다. `error.tsx`가 **모든** 에러를 잡는 데 유용하다면, `notFound`는 존재하지 않는 리소스를 가져오려고 할 때 사용할 수 있습니다.

예를 들어, <http://localhost:3000/dashboard/invoices/2e94d1ed-d220-449f-9f11-f0bbceed9645/edit>를 방문해보세요.

이것은 데이터베이스에 존재하지 않는 가짜 UUID입니다.

이것은 `error.tsx`가 정의되어있는 `/invoices`의 자식 라우트이기 때문에 `error.tsx`가 바로 튀어나오는 것을 바로 확인할 수 있습니다.

그러나 좀 더 구체적으로 처리하고 싶다면, 사용자에게 접근하려는 리소스가 찾을 수 없음을 알려주기 위해 404 에러를 표시할 수 있습니다.

`data.ts`의 `fetchInvoiceById` 함수로 이동하고 반환된 `송장`을 콘솔에서 기록하여 리소스를 찾지 못했음을 확인할 수 있습니다.:

`/app/lib/data.ts`

```ts
export async function fetchInvoiceById(id: string) {
  noStore();
  try {
    // ...

    console.log(invoice); // 인보이스는 빈 배열입니다 []
    return invoice[0];
  } catch (error) {
    console.error('데이터베이스 오류:', error);
    throw new Error('인보이스를 가져오는 데 실패했습니다.');
  }
}
```

이제 데이터베이스에 인보이스가 존재하지 않음을 알게 되었습니다. 이를 처리하기 위해 `notFound`를 사용해봅시다. `/dashboard/invoices/[id]/edit/page.tsx`로 이동하고 `'next/navigation'`에서 `{ notFound }`를 import하세요.

그런 다음, 인보이스가 존재하지 않는 경우 `notFound`를 호출할 수 있는 조건문을 사용할 수 있습니다:

`/dashboard/invoices/[id]/edit/page.tsx`

```tsx
import { fetchInvoiceById, fetchCustomers } from '@/app/lib/data';
import { updateInvoice } from '@/app/lib/actions';
import { notFound } from 'next/navigation';

export default async function Page({ params }: { params: { id: string } }) {
  const id = params.id;
  const [invoice, customers] = await Promise.all([
    fetchInvoiceById(id),
    fetchCustomers(),
  ]);

  if (!invoice) {
    notFound();
  }

  // ...
}
```

좋습니다! `<Page>`는 이제 특정 인보이스가 없는 경우 에러를 던질 것입니다. 사용자에게 에러 UI를 표시하기 위해 `/edit` 폴더 내에 `not-found.tsx` 파일을 생성하세요.

![edit 폴더 내의 not-found.tsx 파일](https://nextjs.org/_next/image?url=%2Flearn%2Flight%2Fnot-found-file.png\&w=3840\&q=75\&dpl=dpl_8s8Dnm8T2UqYs4Sz3a1AK4vKuj5w)

그런 다음, `not-found.tsx` 파일 내에 다음 코드를 붙여넣어보세요:

`/dashboard/invoices/[id]/edit/not-found.tsx`

```tsx
import Link from 'next/link';
import { FaceFrownIcon } from '@heroicons/react/24/outline';

export default function NotFound() {
  return (
    <main className="flex h-full flex-col items-center justify-center gap-2">
      <FaceFrownIcon className="w-10 text-gray-400" />
      <h2 className="text-xl font-semibold">404 Not Found</h2>
      <p>요청한 인보이스를 찾을 수 없습니다.</p>
      <Link
        href="/dashboard/invoices"
        className="mt-4 rounded-md bg-blue-500 px-4 py-2 text-sm text-white transition-colors hover:bg-blue-400"
      >
        뒤로 가기
      </Link>
    </main>
  );
}
```

라우트를 새로고침하면 다음의 UI가 표시됩니다:

![404 Not Found 페이지](https://nextjs.org/_next/image?url=%2Flearn%2Flight%2F404-not-found-page.png\&w=1920\&q=75\&dpl=dpl_8s8Dnm8T2UqYs4Sz3a1AK4vKuj5w)

이것은 기억해야 할 중요한 점입니다. `notFound`는 `error.tsx`보다 우선순위가 있으므로 더 구체적인 에러를 처리하려고 할 때 `notFound`를 활용할 수 있습니다!

&#x20;

> #### 퀴즈 시간입니다!
>
> 지금까지 배운 내용을 테스트해보세요.
>
> **Next.js에서 라우트 세그먼트의 예기치 않은 에러를 캐치하는 데 사용되는 파일은 무엇인가요?**
>
> * A: 404.tsx
> * B: not-found.tsx
> * C: error.tsx
> * D: catch-all.tsx
>
> &#x20;
>
> **정답 확인**
>
> **C: error.tsx**
>
> `error.ts` 파일은 예상치 못한 오류를 모두 캐치하고 사용자에게 대체 UI를 표시할 수 있는 역할을 합니다.

&#x20;

***

&#x20;

### 추가 자료

Next.js에서 에러 처리에 대해 더 알고 싶다면 아래 문서를 확인해보세요:

* [에러 처리](https://nextjs.org/docs/app/building-your-application/routing/error-handling)
* [`error.js` API 레퍼런스](https://nextjs.org/docs/app/api-reference/file-conventions/error)
* [`notFound()` API 레퍼런스](https://nextjs.org/docs/app/api-reference/functions/not-found)
* [`not-found.js` API 레퍼런스](https://nextjs.org/docs/app/api-reference/file-conventions/not-found)


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://wontory.gitbook.io/next.js-journey/learn-next.js/docs/chapter-13.-handling-errors.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
