[React] ErrorBoundary 사용하여 에러 핸들링 하기

이성헌·2021년 10월 30일
19

React

목록 보기
3/4
post-thumbnail

📌 Intro


React는 16 버전부터 앱의 하위 컴포넌트 트리에서 발생하는 자바스크립트 에러를 기록하며 깨진 컴포넌트 트리 대신 폴백 UI를 보여주는 ErrorBoundary라는 개념을 도입하였다.

ErrorBoundary를 통해 컴포넌트에서 에러가 발생했을 때 이를 캐치하여 리포팅하고, 사용자들에게 에러가 발생하여 앱이 중단되는 것이 아닌 다른 대체 화면을 보여줄 수 있다.

ErrorBoundary를 잘 이용하여 앱단과 오류가 발생할 컴포넌트에 잘 감싸주고, 사용자에게 보여줄 화면을 잘 활용(ex-새로고침 화면 보여주기)한다면, 좋은 에러 핸들링이 될 것이다.

이 포스팅에서는 내가 실제로 적용을 해보면서 필요에 맞게 커스텀한 ErrorBoundary를 소개해보고자 한다.

⚙️ ErrorBoundary


리엑트의 공식 홈페이지에서는 다음 코드와 같이 에러핸들링을 보여주고 있다.

// React ErrorBoundary
class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    // 다음 렌더링에서 폴백 UI가 보이도록 상태를 업데이트 합니다.
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    // 에러 리포팅 서비스에 에러를 기록할 수도 있습니다.
    logErrorToMyService(error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      // 폴백 UI를 커스텀하여 렌더링할 수 있습니다.
      return <h1>Something went wrong.</h1>;
    }

    return this.props.children;
  }
}

대충 보자면, 에러의 상태를 state로 관리하고, 에러가 발생하면 이를 getDerivedStateFromError로 확인하여 에러의 상태를 바꿔 fallback UI를 보여주는 식이다.

React에서 제공한 예제만으로 에러핸들링을 하기에는 조금 아쉬움이 있어, ErrorBoundary를 나만의 방식으로 커스텀을 해보았다.

커스텀을 하기위해 많은 도움을 받은 글과 오픈소스가 있는데, 다음과 같다.

Reference

시간이 난다면, 이 포스팅보다 훨씬 수준 높고 좋은 글과 소스이니 위 두 개를 정독하기를 추천한다.

🛠 Custom ErrorBoundary


내가 ErrorBoundary 의 Custom을 하기 위해 신경을 쓴 점은 다음과 같다.

  • Fallback 컴포넌트를 Props로 받을 수 있도록 해서 상황에 따른 Fallback의 활용
  • 전체 앱을 감싸는 ErrorBoundary는 정해진 ErrorPageIntroPage로 에러핸들링
  • 특정 컴포넌트를 감싸는 ErrorBoundary새로고침 버튼을 통해 에러를 reset하고 다시 해당 컴포넌트를 불러오도록 에러핸들링

Error Boundaryreact-error-boundary 의 코드에서 필요한 것만 커스텀하여 사용하였다.

// Custom: ErrorBoundary

import * as React from 'react';

const changedArray = (prevArray: Array<unknown> = [], nextArray: Array<unknown> = []) =>
  prevArray.length !== nextArray.length || prevArray.some((item, index) => !Object.is(item, nextArray[index]));

interface ErrorBoundaryProps {
  onError?: (error: Error, info: { componentStack: string }) => void;
  resetKeys?: Array<unknown>;
  fallback?: React.ReactElement<unknown, string | React.FunctionComponent | typeof React.Component> | null
}

type ErrorBoundaryState = { error: Error | null };

const initialState: ErrorBoundaryState = { error: null };

class ErrorBoundary extends React.Component<React.PropsWithRef<React.PropsWithChildren<ErrorBoundaryProps>>,
  ErrorBoundaryState> {
  static getDerivedStateFromError(error: Error) {
    return { error };
  }

  state = initialState;

  resetErrorBoundary = () => {
    this.reset();
  };

  reset() {
    this.setState(initialState);
  }

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

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

    if (
      error !== null &&
      prevState.error !== null &&
      changedArray(prevProps.resetKeys, resetKeys)
    ) {
      this.reset();
    }
  }

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

    if (error !== null) {
      const props = {
        error,
        resetErrorBoundary: this.resetErrorBoundary,
      };
      if (React.isValidElement(fallback)) {
        return fallback;
      }
      props.resetErrorBoundary();
    }
    return children;
  }
}

export { ErrorBoundary };
export type { ErrorBoundaryProps };

ErrorBoundary를 커스텀한 뒤, 새로고침을 할 수 있도록 CustomErrorBoundary 다음과 같이 컴포넌트를 생성해 주었다.

// CustomErrorBoundary

import { ComponentProps, CSSProperties, ReactNode, useState } from 'react';
import { ErrorBoundary } from './ErrorBoundary';

type ErrorBoundaryProps = ComponentProps<typeof ErrorBoundary>;

interface Props extends Omit<ErrorBoundaryProps, 'renderFallback'> {
  fallback?: ErrorBoundaryProps['fallback'];
  isRefresh?: boolean;
  style?: CSSProperties;
  onError?: (error: Error, info: { componentStack: string }) => void;
  children?: ReactNode;
}

const RefreshButton = ({
                         dispatch,
                         style
                       }: {
  dispatch: (flag: boolean) => void;
  style?: CSSProperties;
}): JSX.Element => {
  return (
    <div style={{ width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', ...style }}>
      <button
        style={{ padding: '5px' }}
        onClick={() => {
          dispatch(false);
        }}
      >
        {'새로고침'}
      </button>
    </div>
  );
};

function CustomErrorBoundary({
                               fallback,
                               isRefresh,
                               style,
                               onError,
                               children,
                               ...errorBoundaryProps
                             }: Props): JSX.Element {
  const [refresh, setRefresh] = useState<boolean>(false);
  return (
    <ErrorBoundary
      fallback={fallback}
      resetKeys={[refresh]}
      onError={(error, info) => {
        isRefresh && setRefresh(true);
        onError && onError(error, info);
      }}
      {...errorBoundaryProps}
    >
      {!refresh ? children : <RefreshButton dispatch={(flag) => setRefresh(flag)} style={style}/>}
    </ErrorBoundary>
  );
}

export default CustomErrorBoundary;

이제 CustomErrorBoundary를 에러핸들링을 하려는 컴포넌트에 감싸주면 된다.

// 특정 앱을 감싸고, fallback으로 새로고침 버튼 나타나게 하기
<CustomErrorBoundary
  isRefresh={true}
  style={{ height: 'calc(100vh - 140px)' }}
>
  <PostList/>
</CustomErrorBoundary>

이제, 감싼 컴포넌트에서 다음과 에러가 발생한다면, 다른 컴포넌트는 그대로 실행이 되고 에러가 발생한 해당 컴포넌트만 새로고침 fallback이 발생한다.

여기서, API 호출 같이 일시적인 오류일 때 새로고침을 눌러줘서 컴포넌트가 다시 정상적으로 렌더링 되는 것을 확인할 수 있다.

// 특정 앱을 감싸고, 지정 fallback 나타나게 하기
<CustomErrorBoundary
  fallback={<BoardLoading/>}
>
  <PostList/>
</CustomErrorBoundary>

fallback을 지정한다면, 에러가 발생 시, 다음과 같이 지정한 fallback 컴포넌트가 나오는 것을 확인할 수 있다.

🔑 결론


지금까지 ErrorBoundary 를 커스텀하는 방식을 소개해보았다. ErrorBoundary를 잘 활용한다면, 사용자에게 매우 좋은 경험을 선사해주고 개발과정에서도 에러를 핸들링하는데 큰 도움이 될 것이라 생각한다.

내가 커스텀한 에러 핸들링도 특정 상황에서 예기치 못한 문제가 생길 수 있을 것이다. 상황과 목표하는 에러 핸들링 방식에 따라 적절하게 커스텀을 해보기를 추천한다. 추후에 사용하는 핸들링 방식에 문제가 있거나 더 좋은 방법이 생긴다면 포스팅을 업데이트 할 수 있도록 하겠다.

profile
프론트엔드 개발자

0개의 댓글