ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [React] Concurrecy(동시성) - #2. Error Boundary
    Front-End(Web)/React - 프레임워크(React, Next) 2022. 10. 27. 19:58
    반응형

    지난 포스팅에선, Code Splitting(코드 분할) 그리고 이를 React에서 구현하는 Suspense와 React.lazy를 알아보았다.

    Suspense는 비동기 컴포넌트(React.lazy) 혹은 비동기 데이터 요청에 따른 선언적 Loading UI 노출에도 유용하다.

     

    물론 이 비동기 요청에서 에러가 발생할 수 있고, 이를 위임하는 기능인 Error Boundary 에 대해 정리해보겠다.

     


     

    💙 React의 Error Boundary

    컴포넌트 내의 Javascript 에러가 발생하면, 컴포넌트 트리가 깨지며 정상적으로 렌더링되지 않는다.

    기존에는 이를 처리 및 복구할 방법이 없었으나, React 16부터 Error Boundary 이라는 컨셉을 제안하게 된다.

    이는, 자식 컴포넌트 트리에서 발생한 에러를 잡아내고, 로그를 남기며, Fallback UI를 노출하는 등의 기능들을 포함하고 있다.

     

     

    - 구조 및 원리

    import React from "react";
    import Fallback from "@/components/common/error/Fallback";
    
    class ErrorBoundary extends React.Component {
      constructor(props) {
        super(props);
        this.state = { hasError: false };
      }
    
      static getDerivedStateFromError(error) {
        // 다음 렌더링에서 폴백 UI가 보이도록 상태를 업데이트 합니다.
        return { hasError: true };
      }
    
      componentDidCatch(error, errorInfo) {
        // 에러 리포팅 서비스에 에러를 기록할 수도 있습니다.
        console.log(error, errorInfo);
      }
    
      render() {
        if (this.state.hasError) {
          // 폴백 UI를 커스텀하여 렌더링할 수 있습니다.
          // return <h1>Something went wrong.</h1>;
          return <Fallback />;
        }
    
        return this.props.children;
      }
    }
    
    export default ErrorBoundary;

    React의 Error Boundary는 클래스형 컴포넌트로 설계되어 있으며, 간단한 구동원리는 아래와 같다.

    1. static 메서드인 getDerivedStateFromError()는 하위 컴포넌트의 에러가 발생하면 실행되는 LifeCycle 이다.
      여기서, hasError 상태값을 true로 바꿔서 Fallback UI가 노출되게 한다.
    2. componentDidCatch 역시 하위 컴포넌트의 에러가 발생하면 실행되는 LifeCycle로, 에러 로깅을 담당한다.
      인자는 error(throw 된 에러), info(componentStack 등 에러가 발생한 컴포넌트 정보) 2가지를 받는다.
    3. render 메서드를 수정하여 에러가 발생했을 때 Fallback Component를 커스텀 할 수 있다.

     

    보통은 이 <ErrorBoundary> 클래스형 컴포넌트확장한 커스텀 컴포넌트를 사용하게 될 것 같다.

    * Hooks(함수형 컴포넌트) 에선 아직 Error Boundary 핸들링을 지원하지 않는다. 이는, 아래에서 소개할 react-error-boundary 라이브러리를 사용하는 이유가 된다.

     

     

    - 사용법

    <ErrorBoundary>
      <MyWidget />
    </ErrorBoundary>

    Error Boundary를 사용하는 구조는 위와 같다.

    에러발생을 제어할 컴포넌트를 <ErrorBoundary>로 감싸주면 되며, Suspense와 같은 선언형 문법이다.

     

    단, Error Boundary는 렌더링 중에 발생한 하위 컴포넌트의 에러만 담당하므로, 아래와 같은 에러들은 제어할 수 없다.

    • Event Handler => 이벤트 핸들러 에러는 try-catch 문으로 보통 제어
    • 비동기 코드(setTimeout, requestAnimationFrame 콜백 등)
    • 서버 사이드 렌더링
    • Error Boundary 자체에서 발생하는 에러 (자식이 아닌)

     

    React 16부터는 Error Boundary에 잡히지 않은 렌더링 오류는 React 컴포넌트 트리 마운트 자체를 해제시킨다. (빈 화면)

    React 팀에서도 논의를 했었고, UI가 손상된 상태보다는 완전히 제거하는 것이 더 이점이 있다고 하여 이처럼 수정이 된 것이다.

     


     

    💙 react-error-boundary

    위에서, 제목을 React의 Error Boundary 라고 적은 것도 공식적으로 제공하는 Error Boundary는 클래스 형태이다.

    함수형 컴포넌트(hooks) 에서는 getDerivedStateFromError 나 componentDidCatch 같은 LifeCycle이 없기 때문이다.

     

    react-error-boundary 라이브러리는 간단하고 재사용 가능한 랩핑 컴포넌트로서, Hooks 친화적인 Error Boundary API들을 제공한다.

     

    * 설치

    npm i --save react-error-boundary
    yarn add react-error-boundary

     

     

    1. ErrorBoundary

    기존 Error Boundary 와 동일하게 에러를 캐치할 자식 컴포넌트를 랩핑하면 된다.

    react-error-boundary의 <ErrorBoundary>의 이점은 Props로 Fallback 컴포넌트나 에러 상호작용 등을 설정할 수 있다는 부분이다.

    import type { PropsWithChildren } from 'react';
    import { ErrorBoundary } from 'react-error-boundary';
    import ErrorFallback from '@/components/lazy/fallback/ErrorFallback';
    
    interface Props {
      onError?: (error: Error) => void;
      onReset?: () => void;
    }
    
    export default function ErrorBoundaryContainer({ children, onError, onReset }: PropsWithChildren<Props>): JSX.Element {
      return (
        <ErrorBoundary 
          FallbackComponent={ErrorFallback}
          onError={onError} 
          onReset={onReset}
         >
          {children}
        </ErrorBoundary>
      );
    }

    Props는 children(자식 컴포넌트)를 포함한 아래의 내용들이 있다.

    (FallbackComponent, fallbackRender, fallback 중 1개 props는 무조건 전달해야 함)

    • FallbackComponent : 에러 발생시 노출할 Fallback 컴포넌트를 전달하며, 이 형태를 가장 많이 사용한다.
      이 컴포넌트는, error(에러내용), resetErrorBoundary(재실행 메서드) 2가지를 Props로 받아서 활용할 수 있다.
    • fallbackRender : 에러 발생시 적용할 Fallback 콜백함수를 전달한다. Error Boundary가 포함된 부모단에서 제어할 때 유용
    • fallback : Suspense와 동일하게 간단한 Fallback UI만 전달할 때 사용. 에러정보 및 제어가 불가하여 추천하지 않음.
    • onError : error가 발생했을 때 실행되는 함수. error(에러정보)와 info(에러가 발생한 컴포넌트 정보) 2가지 인자를 받음.
    • onReset : resetErrorBoundary 메서드를 실행하기 전에 실행되는 함수.
      children 컴포넌트의 reset(리렌더링) 으로 에러가 재발생하지 않는다는 가정 하에 사용되는 것이 좋음
    • resetKeys : state 들의 배열을 인자로 받는다. dependency array 개념으로, 이 값이 바뀌면 자동으로 reset을 실행한다.
    • onResetKeysChange : resetKeys 값이 갱신되어 reset이 호출되기 전에 실행할 함수. 
      (onReset은 resetKeys가 변경되었을 때 실행되지 않으므로, 위 메서드를 추가적으로 제공하는 것)
    // FallbackComponent 예시
    import type { FallbackProps } from 'react-error-boundary';
    
    export default function ErrorFallback({ error, resetErrorBoundary }: FallbackProps): JSX.Element {
      return (
        <div>
          <p>Error: {error || 'Undefined Error'}</p>
          <button onClick={resetErrorBoundary}>
            reset
          </button>
        </div>
      );
    }
    // fallbackRender 예시
    
    const ui = (
      <ErrorBoundary
        fallbackRender={({error, resetErrorBoundary}) => (
          <div role="alert">
            <div>Oh no</div>
            <pre>{error.message}</pre>
            <button
              onClick={() => {
                // this next line is why the fallbackRender is useful
                resetComponentState()
                // though you could accomplish this with a combination
                // of the FallbackCallback and onReset props as well.
                resetErrorBoundary()
              }}
            >
              Try again
            </button>
          </div>
        )}
      >
        <ComponentThatMayError />
      </ErrorBoundary>
    )
    // fallback 예시
    
    const ui = (
      <ErrorBoundary fallback={<div>Oh no</div>}>
        <ComponentThatMayError />
      </ErrorBoundary>
    )

     

     

    2. useErrorHandler

    React의 ErrorBoundary는 LifeCycle에서 발생한 에러만 제어할 수 있다는 단점이 있었다. (이벤트 핸들러, 비동기 로직, SSR 등 불가)

    하지만, 이러한 케이스들에서도 상위의 <ErrorBoundary>를 활용하고 싶을 경우가 있을 것이다.

     

    이러한 경우 활용하기 위해 제공되는 Hooks가 useErrorHandler() 인 것이다.

    // Child Component
    import { useState } from 'react';
    import { useErrorHandler } from 'react-error-boundary';
    import { fetchError } from '@/api/fetchSimple';
    
    export default function ErrorChild(): JSX.Element {
      const [value, setValue] = useState<string>('No Value');
      const handleError = useErrorHandler();
    
      const triggerApi = async () => {
        try {
          const res = await fetchError(true, 'Triggered Error!');
          setValue(res as string);
        } catch (e) {
          handleError(e);
        }
      };
    
      return (
        <>
          <p className="mb-[10px] font-bold">value: {value}</p>
          <button className="p-[5px] border-black border-[1px] rounded-[4px]" onClick={triggerApi}>
            Trigger
          </button>
        </>
      );
    }
    • useErrorHandler Hooks를 가져온 뒤, handleError() 라는 메서드로 선언한다. (훅 자체를 사용해도 됨)
    • 에러가 발생할 수 있는 로직의 catch(e) 구문으로 위의 handleError 메서드를 사용한다. 이 때, 인자로 error를 넘겨준다.
    • 에러가 발생할 때, 해당 컴포넌트에서 가장 가까운 ErrorBoundary로 에러를 위임시켜준다.
      <ErrorBoundary FallbackComponent={ErrorFallback}>
        <Greeting />
      </ErrorBoundary>

     

    기존의 렌더링 에러만 제어할 수 있었던 Error Boundary를 좀 더 폭넓게 사용할 수 있게 해주는 Hooks 인 것이다.

     

     

    3. withErrorBoundary(HOC)

    react-error-boundary는 withErrorBoundary() 의 HOC 형태로도 에러를 제어할 수 있는 기능을 제공한다.

    바로 상위에서 에러를 제어할 때 유용하며, onError 함수로 에러 함수를 분리하며 비교적 간단한 템플릿을 짤 수 있다.

    import {withErrorBoundary} from 'react-error-boundary'
    
    const ComponentWithErrorBoundary = withErrorBoundary(ComponentThatMayError, {
      FallbackComponent: ErrorBoundaryFallbackComponent,
      onError(error, info) {
        // Do something with the error
        // E.g. log to an error logging client here
      },
    })
    
    const ui = <ComponentWithErrorBoundary />

     


     

    💙 Async Boundary

    type ErrorBoundaryProps = ComponentProps<typeof ErrorBoundary>;
    
    interface Props extends Omit<ErrorBoundaryProps, 'renderFallback'> {
      pendingFallback: ComponentProps<typeof SSRSafeSuspense>['fallback'];
      rejectedFallback: ErrorBoundaryProps['renderFallback'];
    }
    
    function AsyncBoundary({
      pendingFallback,
      rejectedFallback,
      children,
      ...errorBoundaryProps,
    }: Props) {
      return (
        <ErrorBoundary
          renderFallback={rejectedFallback}
          {...errorBoundaryProps}
        >
          <SSRSafeSuspense fallback={pendingFallback}>
            {children} {/* <- fulfilled */}
          </SSRSafeSuspense>
        </ErrorBoundary>
      );
    });
    
    export default AsyncBoundary;

    위 예시처럼, isLoading과 isError를 같이 설정하기 위한 Async Boundary 컴포넌트를 커스텀할 수 있다.

    인자는, children component와 Suspense, ErrorBoundary에 설정할 Fallback Component 등을 Props로 넘겨준다.

     

    * 라이브러리도 제공은 되지만, Error Boundary Props가 onError 하나인 단점이 보인다.(async-boundary npm)

     

    async-boundary

    A React async-boundary that couples an error-boundary as well as a suspense container. Latest version: 0.1.1, last published: 6 months ago. Start using async-boundary in your project by running `npm i async-boundary`. There are no other projects in the npm

    www.npmjs.com

     


     

    📎 출처

    - [Error Boundaries] React 공식문서(번역본) : https://reactjs-kr.firebaseapp.com/docs/error-boundaries.html  

    https://reactjs.org/docs/error-boundaries.html

    - [Error Boundary 개념] rkd028 님의 블로그 : https://nukw0n-dev.tistory.com/24

     

    - [react-error-boundary] Github 공식문서 : https://github.com/bvaughn/react-error-boundary

    - [react-error-boundary] logRocket Tech Blog : https://blog.logrocket.com/react-error-handling-react-error-boundary/

    반응형
Designed by Tistory.