13. 에러 처리

이전 장에서는 서버 액션을 사용하여 데이터를 변경하는 방법을 배웠다. JavaScript의 try/catch 문과 Next.js API를 사용하여 오류를 우아하게 처리하는 방법을 살펴보자.

 

이번 장의 주제는 아래와 같다.

  • 특수 error.tsx 파일을 사용하여 경로 세그먼트에서 오류를 포착하고 사용자에게 대체 UI를 표시하는 방법.
  • notFound 함수와 찾을 수 없는 파일을 사용하여 404 오류(존재하지 않는 리소스의 경우)를 처리하는 방법.

 


서버 액션에 try/catch 추가

먼저 오류를 적절하게 처리할 수 있도록 JavaScript의 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: 'Database Error: Failed to Create Invoice.',
    };
  }
 
  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: 'Database Error: Failed to Update Invoice.' };
  }
 
  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: 'Deleted Invoice.' };
  } catch (error) {
    return { message: 'Database Error: Failed to Delete Invoice.' };
  }
}

 

try/catch 블록 외부에서 리디렉션이 어떻게 호출되는지 확인해보자. 이는 리디렉션이 catch 블록에 의해 포착되는 오류를 발생시켜 작동하기 때문이다. 이를 방지하려면 try/catch 후에 리디렉션을 호출하면 된다. 호출 시도가 성공한 경우에만 리디렉션에 연결할 수 있다.

이제 서버 액션에서 오류가 발생하면 어떤 일이 발생하는지 확인해 보자. 이전에 오류를 발생시켜 이를 수행할 수 있다. 예를 들어 deleteInvoice 작업에서 함수 상단에 오류를 발생시켜보자.

 

/app/lib/actions.ts

export async function deleteInvoice(id: string) {
  throw new Error('Failed to Delete Invoice');
 
  // Unreachable code block
  try {
    await sql`DELETE FROM invoices WHERE id = ${id}`;
    revalidatePath('/dashboard/invoices');
    return { message: 'Deleted Invoice' };
  } catch (error) {
    return { message: 'Database Error: Failed to Delete Invoice' };
  }
}

 

송장을 삭제하려고 하면 localhost에 오류가 표시된다.

이러한 오류를 확인하면 잠재적인 문제를 조기에 발견할 수 있으므로 개발 중에 도움이 된다. 그러나 갑작스러운 오류를 방지하고 애플리케이션이 계속 실행될 수 있도록 사용자에게 오류를 표시할 수도 있다.

이것이 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(() => {
    // Optionally log the error to an error reporting service
    console.error(error);
  }, [error]);
 
  return (
    <main className="flex h-full flex-col items-center justify-center">
      <h2 className="text-center">Something went wrong!</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={
          // Attempt to recover by trying to re-render the invoices route
          () => reset()
        }
      >
        Try again
      </button>
    </main>
  );
}

 

위의 코드에서 알 수 있는 몇 가지 사항이 있다.

  • "클라이언트 사용" - error.tsx는 클라이언트 컴포넌트여야 한다.
  • 두 가지 prop 을 허용다:
    • error : 이 객체는 JavaScript의 기본 Error 객체의 인스턴스이다.
    • reset : 오류 경계를 재설정하는 기능이다. 실행되면 함수는 경로 세그먼트를 다시 렌더링하려고 시도한다.

 

송장을 다시 삭제하려고 하면 다음 UI가 표시된다.

 

layout 오류처리

error.tex 바운더리는 동일한 세그먼트의 layout.tsx 또는 template.tsx 컴포넌트에서 발생한 에러를 잡아내지 않는다. 특정 layout이나  template 내에서 에러를 처리하려면, layout의 부모 세그먼트에 error.tsx 파일을 배치해야 한다. 그리고 root layout이나 template 내에서 에러를 처리하려면, global-error.tex를 사용해야 한다.

 

root error.tsx와는 달리, global-error.tsx 에러 바운더리는 전체 애플리케이션을 감싸며, 활성화될 때 root layout을 대체하는 대체 컴포넌트를 가지고 있다. 따라서 global-error.js는 자체 <html> 및 <body> 태그를 정의해야한다.

 

/app/global-error.tsx

'use client'
 
export default function GlobalError({
  error,
  reset,
}: {
  error: Error & { digest?: string }
  reset: () => void
}) {
  return (
    <html>
      <body>
        <h2>Something went wrong!</h2>
        <button onClick={() => reset()}>Try again</button>
      </body>
    </html>
  )
}

 

 

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

export async function fetchInvoiceById(id: string) {
  noStore();
  try {
    // ...
 
    console.log(invoice); // Invoice is an empty array []
    return invoice[0];
  } catch (error) {
    console.error('Database Error:', error);
    throw new Error('Failed to fetch invoice.');
  }
}

 

이제 데이터베이스에 송장이 없다는 것을 알았으므로 notFound를 사용하여 이를 처리해 보자.

/dashboard/invoices/[id]/edit/page.tsx로 이동하고 'next/navigation'에서 { notFound }를 가져온다.

그 다음 송장이 존재하지 않는 경우 조건을 사용하여 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();
  }
 
  // ...
}

 

이제 특정 청구서를 찾을 수 없으면 <페이지>에서 오류가 발생한다. 사용자에게 오류 UI를 표시하기 위해 /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>Could not find the requested invoice.</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"
      >
        Go Back
      </Link>
    </main>
  );
}

 

경로를 새로 고치면 이제 다음 UI가 표시된다.

 

 notFound는 error.tsx보다 우선하므로 더 구체적인 오류를 처리하고 싶을 때 이를 활용 할 수 있다.

 

 

심화 학습

Next.js의 오류 처리에 대해 자세히 알아보려면 다음 문서를 확인하자.

  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유