React 애플리케이션의 에러 처리 전략: ErrorBoundary vs ErrorPage

React 애플리케이션에서 에러를 처리하는 방법은 크게 두 가지가 있습니다: ErrorBoundary와 React Router의 ErrorPage입니다. 각각의 사용 사례와 차이점을 알아보겠습니다.

1. ErrorBoundary

ErrorBoundary는 React 컴포넌트 트리 내에서 발생하는 JavaScript 에러를 캐치하고 처리하는 React 컴포넌트입니다.

ErrorBoundary가 처리하는 에러:

  1. 컴포넌트 렌더링 중 발생하는 에러
  2. 이벤트 핸들러에서 throw된 에러
  3. React Query의 throwOnError가 true일 때 발생하는 쿼리 에러

ErrorBoundary 구현 예시:

import { Component, ErrorInfo, ReactNode } from 'react';
import { NetworkError } from '../errors';

interface ErrorBoundaryState {
  hasError: boolean;
  errorInfo?: {
    message: string;
    code?: number;
  };
}

class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
  constructor(props: ErrorBoundaryProps) {
    super(props);
    this.state = {
      hasError: false,
      errorInfo: undefined,
    };
  }

  public static getDerivedStateFromError(error: Error): ErrorBoundaryState {
    if (error instanceof NetworkError) {
      return {
        hasError: true,
        errorInfo: {
          message: error.message,
          code: error.code,
        },
      };
    }

    return {
      hasError: true,
      errorInfo: {
        message: '예상치 못한 오류가 발생했습니다.',
      },
    };
  }

  public render() {
    if (this.state.hasError) {
      return (
        <div className="error-container">
          <h2>문제가 발생했습니다</h2>
          <p>{this.state.errorInfo?.message}</p>
          <button onClick={() => window.location.reload()}>
            다시 시도
          </button>
        </div>
      );
    }

    return this.props.children;
  }
}

사용 예시:

// React Query에서 에러 발생 시
const query = useQuery({
  queryKey: ['key'],
  queryFn: async () => {
    throw new NetworkError({ code: 500 });  // ErrorBoundary가 캐치
  },
  throwOnError: true
});

// 컴포넌트에서 직접 에러 throw
if (someError) {
  throw new Error('Component Error');  // ErrorBoundary가 캐치
}

2. ErrorPage (React Router)

React Router의 errorElement는 라우팅 관련 에러를 처리하는 데 사용됩니다.

ErrorPage가 처리하는 에러:

  1. 라우팅 관련 에러 (404 등)
  2. loader나 action에서 throw된 에러
  3. 잘못된 URL 접근

라우터 설정 예시:

const router = createBrowserRouter([
  {
    path: '/',
    element: <RootPage />,
    errorElement: <ErrorPage />,  // 라우팅 에러 처리
    loader: async () => {
      const response = await fetch('/api/data');
      if (!response.ok) {
        throw new Error('Failed to fetch');  // ErrorPage로 이동
      }
      return response.json();
    },
    children: [
      // ... 라우트 설정
    ]
  }
]);

에러 처리 전략

1. ErrorBoundary 사용

  • 컴포넌트 렌더링 관련 에러
  • React Query 에러 (throwOnError: true)
  • UI 관련 예외 상황

2. ErrorPage 사용

  • 라우팅 관련 에러
  • API 로딩 실패
  • 인증/인가 관련 에러

3. 함께 사용하기

createRoot(document.getElementById('root') as HTMLElement).render(
  <ErrorBoundary>
    <RouterProvider router={router} />
  </ErrorBoundary>
);

이렇게 구성하면:

  • ErrorBoundary: 컴포넌트 레벨의 에러 처리
  • ErrorPage: 라우팅/데이터 로딩 관련 에러 처리
  • 계층적 에러 처리 가능

결론

  • ErrorBoundary는 컴포넌트 트리 내의 JavaScript 에러를 처리
  • ErrorPage는 라우팅과 데이터 로딩 관련 에러를 처리
  • 두 가지 방식을 적절히 조합하여 사용하면 더 견고한 에러 처리 가능

같은 API 호출의 다른 에러 처리 경로

1. loader에서 호출 시 → ErrorPage

// route 정의
{
  path: '/articles',
  element: <ArticlesPage />,
  loader: async () => {
    const response = await fetch('/api/articles');
    if (!response.ok) {
      throw new Error('Failed to fetch'); // ErrorPage로 이동
    }
    return response.json();
  }
}

2. 컴포넌트 내부에서 호출 시 → ErrorBoundary

const ArticlesPage = () => {
  const query = useQuery({
    queryKey: ['articles'],
    queryFn: async () => {
      const response = await fetch('/api/articles');
      if (!response.ok) {
        throw new Error('Failed to fetch'); // ErrorBoundary로 이동
      }
      return response.json();
    },
    throwOnError: true
  });

  return <div>{/* ... */}</div>;
};

차이가 발생하는 이유

  1. 로더(Loader)의 에러
    • 컴포넌트가 렌더링되기 전에 발생
    • React Router가 처리
    • 라우팅 수준의 에러로 간주
    • ErrorPage로 이동
  2. 컴포넌트 내부의 에러
    • 컴포넌트 렌더링 중에 발생
    • React의 에러 경계가 처리
    • UI 수준의 에러로 간주
    • ErrorBoundary가 캐치

+ Recent posts