React-Error-Boundary를 이용한 에러처리

이수빈·2024년 3월 5일
0

React

목록 보기
16/20
post-thumbnail

ErrorBoundary란?

이전 글 참고) ErrorBoundary

공식문서) https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary

  • 에러를 명령형방식 => 선언형방식으로 처리할 수 있게 해주는 컴포넌트임.

  • 보통 Suspense와 함께 사용해서 Class형 컴포넌트로 작성함.

  • React-Error-Boundary 라이브러리를 사용하면 => Functional Component로 ErrorBoundary를 사용가능함. (컴포넌트로 제공)

React-Error-boundary

  • Error Boundary 컴포넌트 : ErrorBoundary라는 특수한 컴포넌트를 제공하여 에러가 발생한 경우 이를 캐치하고 대체 UI를 표시할 수 있음 => 컴포넌트 하위에서의 에러감지

  • Fallback UI 표시 : 에러코드나 메세지에 따라 여러 fallback UI 설정가능

  • 에러 정보를 추적: ErrorBoundary는 에러에 대한 정보를 onError 콜백 함수를 통해 제공하여 에러를 추적하고 로깅하는 데 사용할 수 있습니다.

  • reset : 에러가 발생한 페이지에서 api호출 재시도기능 구현가능.

  • 에러바운더리는 클라이언트 컴포넌트에서만 사용가능함.

  • 여기서 에러의 종류나 상태코드에 따라 에러를 처리하도록 개발자가 설정 할 수 있다.

  • 기본적으로 에러는 클라이언트측에서 발생한 에러와 서버에 요청할 때 상태코드에 따른 에러를 따로 처리해줘야한다.

ErrorBoundary.ts 코드

  • 동작방식 => componentDidCatch 생명주기 메소드에서 하위컴포넌트에서 던진 에러를 잡음 or useErrorBoundary hook을 이용해 의도적으로 에러를 발생하는 것도 가능

  • 이후 render 메소드를 호출할 때 => didcatch가 true인 상태라면, 미리설정한 fallback을 보여주고, development환경이라면 또 다시 상위로 에러를 던짐 (production에서는 따로 에러처리를 해주지 않는다면 빈 화면만 나타남) => 개발에서 무슨 에러 났는지 알려줌

  • resetErrorBoundary 메소드는 결국 에러 발생이전으로 상태를 돌리는 역할을 하는 메소드임. Errorboundary의 didcatch와 error 값을 initial State로 set함

import { isDevelopment } from "#is-development";
import { Component, createElement, ErrorInfo, isValidElement } from "react";
import { ErrorBoundaryContext } from "./ErrorBoundaryContext";
import { ErrorBoundaryProps, FallbackProps } from "./types";

type ErrorBoundaryState =
  | {
      didCatch: true;
      error: any;
    }
  | {
      didCatch: false;
      error: null;
    };

const initialState: ErrorBoundaryState = {
  didCatch: false,
  error: null,
};

export class ErrorBoundary extends Component<
  ErrorBoundaryProps,
  ErrorBoundaryState
> {
  constructor(props: ErrorBoundaryProps) {
    super(props);

    this.resetErrorBoundary = this.resetErrorBoundary.bind(this);
    this.state = initialState;
  }

  static getDerivedStateFromError(error: Error) {
    return { didCatch: true, error };
  }

  resetErrorBoundary(...args: any[]) {
    const { error } = this.state;

    if (error !== null) {
      this.props.onReset?.({
        args,
        reason: "imperative-api",
      });

      this.setState(initialState);
    }
  }

  componentDidCatch(error: Error, info: ErrorInfo) {
    this.props.onError?.(error, info);
  }

  componentDidUpdate(
    prevProps: ErrorBoundaryProps,
    prevState: ErrorBoundaryState
  ) {
    const { didCatch } = this.state;
    const { resetKeys } = this.props;

    // There's an edge case where if the thing that triggered the error happens to *also* be in the resetKeys array,
    // we'd end up resetting the error boundary immediately.
    // This would likely trigger a second error to be thrown.
    // So we make sure that we don't check the resetKeys on the first call of cDU after the error is set.

    if (
      didCatch &&
      prevState.error !== null &&
      hasArrayChanged(prevProps.resetKeys, resetKeys)
    ) {
      this.props.onReset?.({
        next: resetKeys,
        prev: prevProps.resetKeys,
        reason: "keys",
      });

      this.setState(initialState);
    }
  }

  render() {
    const { children, fallbackRender, FallbackComponent, fallback } =
      this.props;
    const { didCatch, error } = this.state;

    let childToRender = children;

    if (didCatch) {
      const props: FallbackProps = {
        error,
        resetErrorBoundary: this.resetErrorBoundary,
      };

      if (typeof fallbackRender === "function") {
        childToRender = fallbackRender(props);
      } else if (FallbackComponent) {
        childToRender = createElement(FallbackComponent, props);
      } else if (fallback === null || isValidElement(fallback)) {
        childToRender = fallback;
      } else {
        if (isDevelopment) {
          console.error(
            "react-error-boundary requires either a fallback, fallbackRender, or FallbackComponent prop"
          );
        }

        throw error;
      }
    }

    return createElement(
      ErrorBoundaryContext.Provider,
      {
        value: {
          didCatch,
          error,
          resetErrorBoundary: this.resetErrorBoundary,
        },
      },
      childToRender
    );
  }
}

function hasArrayChanged(a: any[] = [], b: any[] = []) {
  return (
    a.length !== b.length || a.some((item, index) => !Object.is(item, b[index]))
  );
}

그렇다면 어떻게 에러를 핸들링 할 수 있을까?

클라이언트 UI 오류

  • production 환경에서 클라이언트 UI를 그리는데 에러가 발생한다면 사용자는 흰 화면을 보고 에러발생여부를 확인 할 수 없다.

  • 이런 경우를 대비해서, 상위 컴포넌트에서 Global ErrorBoundary로 다른 컴포넌트를 감싼 후, UI를 대체할 fallback을 보여주도록 설정해야한다.

  • 코드예시는 대강 이런식이다.

"use client";

import { ErrorBoundary } from "react-error-boundary";

function fallbackRender({ error, resetErrorBoundary }) {
  // Call resetErrorBoundary() to reset the error boundary and retry the render.

  return (
    <div role="alert">
      <p>Something went wrong:</p>
      <pre style={{ color: "red" }}>{error.message}</pre>
    </div>
  );
}

<ErrorBoundary
  fallbackRender={fallbackRender}
  onReset={(details) => {
    // Reset the state of your app so the error doesn't happen again
  }}
>
  <ExampleApplication />
</ErrorBoundary>;

서버 호출 오류

  • 만약 서버에 api를 호출하는 과정에서 오류가 발생했다면 => 상태코드에 따라 에러메세지로 분기처리를 해주는 방법이 존재한다.

  • 이것을 구현하는 방법은 여러가지가 있을 수 있다.

1. Error Boundary fallback 으로 분기처리

  • 에러가 발생해 => 클라이언트측 에러가 아니라 AxiosError 라면 fallback Renderer 속성을 통해 다른 fallback을 렌더링하는 방식이다

  • axios에서 제공하는 isAxiosError를 사용해 에러 type을 판별 할 수 있다.

  • 여기서 error객체의 response 상태값에 따라 어떤 에러 메세지가 발생했는지를 알 수 있다.

  • error 의 상태값들이 여러가지마다 모두 분기처리를 해줘야 하는 단점이 있다.

  • 예를들어 api 요청을 보냈는데
    400 : BAD REQUEST
    401 : 로그인 필요
    403 : 권한이 없음
    404 : NOT FOUND
    500 : INTERNAL SERVER ERROR .. 이런식의 상태 코드가 존재 할 수도 있고,

NET_WORK_ERROR 처럼 네트워크 문제일 수도 있고,
요청을 했는데 CANCELED되는 상황이 존재할 수도 있다.


 <ErrorBoundary
      fallbackRender={({ resetErrorBoundary, error }) => {
        if (isAxiosError(error)) {
          return (
              <AxiosErrorFallback error={error}/>
          );
        }
        return <ClientErrorFallback error={error}/>;
      }}
      onReset={handleReset}
    >
      {children}
    </ErrorBoundary>
  • 두번째 방식은 Axios Interceptor와 클라이언트 전역상태를 이용해 Error를 핸들링하는 방식이다.

  • 이 방식은 좀 더 세밀하게 클라이언트 단에서 에러를 핸들링 할 수 있다.

  • Interceptor에서 response를 받은 후 클라이언트 store 전역상태에 에러를 저장해 놓는다.

  • 에러를 클라이언트 전역상태에 저장 후, Promise.reject로 상위 컴포넌트로 에러를 던진다.

  • 이러면 Global Error Boundary가 Error를 Catch 하고 실행되고

  • 이후 다른 에러를 처리하는 모달에서 클라이언트 전역상태를 subscribe 한 뒤 에러를 가져와서 처리하는 방식이다.

  • 전역상태관리의 store에서는 보통 subscribe 메소드가 존재한다. => atom들의 변화를 감지해 컴포넌트를 실행 할 수 있게 해준다.(jotai store의 sub함수)

  • 조금 더 세밀한 예외처리가 가능하다. (CUSTOM 하기 편한 장점)

ref)
라이브러리 공식문서 : https://github.com/bvaughn/react-error-boundary
https://xionwcfm.tistory.com/372
https://jotai.org/docs/core/store#createstore

profile
응애 나 애기 개발자

0개의 댓글