Chapter 13. Handling Errors

제 13장

에러 처리

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

이번 장에서는...

다음과 같은 내용을 다룰 예정입니다.

  • 라우트 세그먼트에서 에러를 잡고 사용자에게 대체 UI를 보여주기 위해 특별한 error.tsx 파일을 사용하는 방법

  • 존재하지 않는 리소스에 대한 404 에러를 처리하기 위해 notFound 함수와 not-found 파일을 사용하는 방법


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

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

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

솔루션 보기

/app/lib/actions.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');
}

솔루션 보기

/app/lib/actions.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');
}

솔루션 보기

/app/lib/actions.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: '데이터베이스 오류: 인보이스 삭제에 실패했습니다.' };
  }
}

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

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

/app/lib/actions.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 파일이 등장합니다.


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

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

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

/dashboard/invoices/error.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 객체의 인스턴스입니다.

    • reset: 이는 에러 경계를 재설정하기 위한 함수입니다. 실행되면 함수가 라우트 세그먼트를 다시 렌더링하려고 시도합니다.

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

error.tsx 파일이 수용하는 프롭을 보여주는 이미지


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.tsfetchInvoiceById 함수로 이동하고 반환된 송장을 콘솔에서 기록하여 리소스를 찾지 못했음을 확인할 수 있습니다.:

/app/lib/data.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

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 파일

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

/dashboard/invoices/[id]/edit/not-found.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 페이지

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

퀴즈 시간입니다!

지금까지 배운 내용을 테스트해보세요.

Next.js에서 라우트 세그먼트의 예기치 않은 에러를 캐치하는 데 사용되는 파일은 무엇인가요?

  • A: 404.tsx

  • B: not-found.tsx

  • C: error.tsx

  • D: catch-all.tsx

정답 확인

C: error.tsx

error.ts 파일은 예상치 못한 오류를 모두 캐치하고 사용자에게 대체 UI를 표시할 수 있는 역할을 합니다.


추가 자료

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

Last updated