React - Error Boundary

sarang_daddy·2023년 9월 17일
0

React

목록 보기
18/26
post-thumbnail

웹 앱을 구축하는 것은 복잡한 과정이다. 때때로 문제가 발생하게 되며, 이런 문제를 완전히 막을 수는 없다. 예상치 못한 방식으로 오류가 발생할 것을 예상하고 그에 대응할 준비가 되어 있어야 한다.
이를 위한 Error Boundary를 알아보고 직접 만들어 보자.

Error Boundary 란?

에러에 대한 경계를 의미하는 Error Boundary는 경계 내의 구간에서 에러가 발생하면 그 에러를 잡아내서 처리할 수 있는 역할을 한다. 기본적인 로직은 아래와 같다.

  1. React에서는 특정 컴포넌트 렌더링 도중 에러가 발생하면 해당 컴포넌트 UI를 제거한다.
  2. 에러에 대한 준비가 따로 되어 있지 않다면 React에서 제공하는 에러페이지로 넘어간다.

    • 실제 환경에서는 아무 페이지도 뜨지 않을 것이다. 사용자는 사라진다..😢
  3. 이를 방지하기 위해 컴포넌트를 Error Boundary로 감싸준다.
  4. 경계 내의 컴포넌트에서 에러가 발생한다면 준비된 UI를 보여 줄 수 있다.
  5. 준비된 UI를 fallback이라 한다.

    • 예상치 못한 에러가 발생하면 사용자에게 다시 홈부터 시작하도록 유도할 수 있다.

Error Boundary 생성

공식문서를 살펴보면 Error Boundary는 현재 함수형 컴포넌트로 작성할 수 있는 방법이 없으며 클래스로 예시 코드를 알려주고 있다.

클래스를 직접 작성하기 싫다면 react-error-boundary를 제공해 준다.

클래스 예시 코드를 참고하여 코드를 생성해보자.

import { Component, ErrorInfo, ReactNode } from 'react';

// 에러바운더리를 사용할 때 fallback prop을 제공하지 않으면 사용되는 기본 UI
import { DefaultErrorFallback } from './DefaultErrorFallback';

// 에러바운더리 컴포넌트 props 타입 정의
interface ErrorBoundaryProps {
  children: ReactNode; // 렌더링될 자식 컴포넌트들
  fallback?: ReactNode; // 에러 발생시 보여주는 컴포넌트
}

// 에러바운더리 컴포넌트 state 타입 정의
interface ErrorBoundaryState {
  hasError: boolean; // 에러 발생 여부
  error?: Error; // 에러 객체
}

export class ErrorBoundary extends Component<
  ErrorBoundaryProps,
  ErrorBoundaryState
> {
  // 생성자에서 초기 상태 설정
  constructor(props: ErrorBoundaryProps) {
    super(props);
    this.state = { hasError: false, error: undefined };
  }

  // React의 생명주기 메서드 'getDerivedStateFromError'
  // 하위 컴포넌트에서 에러가 발생하면 호출된다.
  // 에러 객체를 받아서 새로운 상태를 반환한다.
  static getDerivedStateFromError(error: Error) {
    return { hasError: true, error };
  }

  // React의 생명주기 메서드 'componentDidCatch'
  // 에러와 에러 정보를 받아서 처리한다.
  // 분석 서비스에 에러를 기록하는 등 추가 작업이 가능하다.
  componentDidCatch(error: Error, errorInfo: ErrorInfo) {
    console.error(error, errorInfo);
  }

  // 컴포넌트 렌더링
  render() {
    // 에러가 있으면 fallback 렌더링
    if (this.state.hasError && this.state.error) {
      return (
        this.props.fallback || <DefaultErrorFallback error={this.state.error} />
      );
    }
    // 에러 없으면 children 렌더링
    return this.props.children;
  }
}

Error Boundary 적용하기

  • Error Boundary를 적용하고 싶은 컴포넌트를 감싸주면 된다.

전체 페이지에 적용

// Layout
import { Outlet } from 'react-router-dom';
import { ErrorBoundary } from '@/components/ErrorBoundary';

export const Layout = () => {
  return (
    <ErrorBoundary>
      <Outlet />
    </ErrorBoundary>
  );
};

개별 라우트 내에 적용

// Router
import { createBrowserRouter } from 'react-router-dom';

import { Home } from '@/routes/Home';
import { Detail } from '@/routes/Detail';
import { Layout } from '@/routes/Layout';
import { ROUTE_PATH } from '@/router/routePath';
import { NotFound } from '@/routes/NotFound';
import { ErrorBoundary } from '@/components/ErrorBoundary';

export const router = createBrowserRouter([
  {
    element: <Layout />,
    path: ROUTE_PATH.ROOT,
    errorElement: <NotFound />,
    children: [
      {
        path: ROUTE_PATH.HOME,
        element: <Home />,
      },
      {
        path: ROUTE_PATH.DETAIL,
        element: (
          <ErrorBoundary>
            <Detail />
          </ErrorBoundary>
        ),
      },
    ],
  },
]);
  • Detail 라우트에서만 발생하는 오류를 ErrorBoundary로 캡처한다. Home 라우트에서 발생하는 오류는 캡처되지 않는다.

특정 컴포넌트 내에 적용

// Component
import { ErrorBoundary } from '@/components/ErrorBoundary';

const Detail = () => {
  return (
    <div>
      <h1>Detail Page</h1>
      <ErrorBoundary>
        <Profile />
      </ErrorBoundary>
    </div>
  );
}
  • Profile 컴포넌트 내에서 발생하는 오류만 ErrorBoundary로 캡처된다.

Error Boundary의 한계

Error Boundary는 비동기 통신 에러는 잡지 못한다

export const Detail = () => {

  // 중략

  // 에러 바운더리 확인을 위한 에러 유도 코드
  useEffect(() => {
    async function test() {
      throw new Error('에러바운더리 테스트');
    }
    test();
  }, []);
  
  return (
  // 중략
  )
  • Detail 컴포넌트에서 useEffect를 사용하여 비동기 통신 중 에러가 발생했다.
  • 하지만 Error Boundary의 fallback UI가 아닌 정상적인 화면이 렌더링 된다.

Error Boundary는 렌더링 중 발생한 에러만을 잡는다

Error Boundary는 컴포넌트 트리 내에서 하위 컴포넌트의 렌더링에서 발생하는 에러를 잡는다.
즉, 아래의 경우에는 에러를 포착할 수 없다.

  • 이벤트 핸들러
    Error Boundary는 이벤트 핸들러 내에서 발생하는 오류를 포착하지 않는다. 예를 들어, onClick 또는 onChange와 같은 이벤트 핸들러에서 발생하는 오류는 Error Boundary로 잡히지 않는다.

  • 비동기 코드
    setTimeout, requestAnimationFrame, 서버 사이드 렌더링 등 비동기 코드에서 발생하는 오류는 Error Boundary에서 포착되지 않는다.

  • Server Side Rendering (SSR)
    서버 사이드 렌더링 중에 발생하는 오류는 Error Boundary에서 잡히지 않는다.

  • Error Boundary 자체의 오류
    Error Boundary 컴포넌트 내부에서 발생하는 오류는 해당 Error Boundary에서 포착되지 않는다. 이러한 오류를 포착하려면 상위 컴포넌트에서 다른 Error Boundary를 사용해야 한다.

  • 외부 라이브러리와 코드
    Error Boundary는 React 컴포넌트 트리 내에서 발생하는 오류만 포착한다. 따라서 React 외부에서 발생하는 오류나 외부 라이브러리에서 발생하는 오류는 포착되지 않는다.

Error Boundary의 사용

위 내용들처럼 Error Boundary는 에러에 대한 준비를 가능하게 해줌으로서 애플리케이션의 안정성을 향상 시켜주지만, 한계점이 분명하기에 전체 오류 관리 전략의 일부로서 사용되도록 해야한다.

참고자료

Error Boundary의 적절한 사용

  • 오류 경계를 너무 적게 사용하는 경우
    애플리케이션의 상단에 단 하나의 오류 경계만 있으면, 한 부분에서의 오류가 전체 애플리케이션에 영향을 미칠 수 있다.

  • 오류 경계를 과도하게 사용하는 경우
    각 컴포넌트마다 오류 경계를 설정하면, 사용자 경험이 혼란스러워질 수 있다. 또한 성능에도 영향을 미칠 수 있다.

Error Boundary의 적절한 위치 찾기

애플리케이션의 기능 경계를 확인하고 그곳에 오류 경계를 배치하는 것이 가장 좋다.

Twitter를 예로 들면, 메인 콘텐츠 섹션은 Home, Trends for you, Who to follow 등 세 가지로 구분된다.

Who to Follow 섹션 내에서도 여러 하위 섹션이 있으며, 각각의 섹션에서 오류가 발생할 경우 다른 섹션에 영향을 주어야 하는지를 고려하여 오류 경계를 설정한다.

Fault Tolerance 테스트

애플리케이션의 오류 허용성을 테스트하는 가장 좋은 방법은 일부러 오류를 발생시켜보는 것이다.

function CreditCardInput(props) {
  // What happens if I messed up here? Let's find out!
  throw new Error("oops, I made a mistake!")
  return <input className="credit-card" />
}

요약

  1. 애플리케이션 상단에 단 하나의 오류 경계만 두는 것은 피해야 한다.
  2. 오류 경계를 과도하게 사용하는 것도 피해야 한다.
  3. 애플리케이션의 기능 경계를 파악하고 그곳에 오류 경계를 배치한다.
  4. "이 컴포넌트가 충돌하면 형제 컴포넌트도 충돌해야 하는가?"라는 질문을 통해 기능 경계를 찾을 수 있다.
  5. 오류 상태를 위해 애플리케이션을 의도적으로 설계해야 한다.
profile
한 발자국, 한 걸음 느리더라도 하루하루 발전하는 삶을 살자.

0개의 댓글