React

Error Boundary

갬쿠 2023. 7. 15. 16:23

이번 주 플젝 중 백엔드 팀원들이 에러 핸들러를 만들면서 고생하는걸 지켜봤다. 클라이언트에서는 서버에서 보내주는 상태 코드를 처리하거나 자체 에러를 핸들링 할 때 어떻게 해야 할까?

 

Error Boundary React 공식 문서

 

Component – React

The library for web and native user interfaces

react.dev

레거시로 분류되어 있는 error boundary.. 그러나 카카오페이지에서 사용한다는걸 보고 그냥 쓰기로 했다 ㅎ


리액트는 렌더링 중에 에러가 생기면 화면에서 UI를 지워버린다. 이걸 "컴포넌트가 깨진다"고들 표현한다. 즉, 에러가 생긴 컴포넌트만 언마운트 되는 게 아니라 모든 컴포넌트가 언마운트 되는 것이다.

잘못된 정보를 사용자에게 보여 주는 것보다는 아무것도 보여주지 않는 것이 낫다는 것인데, 문제는 사용자 경험이 나빠진다는 것이다. 이 문제를 해결하기 위해 error boundary가 등장했다.

 

React 공식 문서 피셜로 error boundary는 클래스형으로 작성되어야 하며, 함수형으로 쓸 방법은 없다. (대신 react-error-boundary와 같은 대안을 추천하고 있다.)

클래스형으로 작성되어야 한다고 코드 전체가 클래스형이어야 한다는 건 아니고 error boundary만 클래스형으로 만들어서 감싸주면 된다.

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

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

  componentDidCatch(error, info) {
    // 에러를 기록할 수도 있음
    logErrorToMyService(error, info.componentStack);
  }

  render() {
    if (this.state.hasError) {
      // 폴백 UI 커스텀해서 렌더링 할 수 있음
      return this.props.fallback;
    }

    return this.props.children;
  }
}
<ErrorBoundary fallback={<p>Something went wrong</p>}>
  <Profile />
</ErrorBoundary>

 

에러가 발생할 수 있는 UI의 일부분을 error boundary로 감싸서 빈 화면이 보이는 것을 예방할 수 있다. error boundary는 깨진 부분 대신 fallback UI를 표시한다.

위 이미지처럼 부분적으로 폴백 UI를 띄워 사용자에게 에러가 발생했음을 알리고, 에러가 발생하지 않은 나머지 컴포넌트는 보여줄 수 있다.

 

error boundary는 하위 트리에서 렌더링 중에 throw된 에러를 catch하도록 동작한다. 해당 error boundary에서 에러 처리를 실패하면 가장 가까운 error boundary에서 catch한다. 위 이미지의 출처인 카카오페이지에서는 api error boundary로 컴포넌트들을 감싸고, 제일 위에 global error boundary를 둬서 rethrow된 에러를 처리하도록 구현했다.

<GlobalErrorBoundary>
  <ApiErrorBoundary>
    <ContentProductListFetcher>
      <ContentProductListContainer />
    </ContentProductListFetcher>
  </ApiErrorBoundary>
  <ApiErrorBoundary>
    <ContentRecommendFetcher>
      <ContentHomeRecommendContainer />
    </ContentRecommendFetcher>
  </ApiErrorBoundary>
  <ApiErrorBoundary>
    <CommentFetcher>
      <CommentContainer />
    </CommentFetcher>
  </ApiErrorBoundary>
</GlobalErrorBoundary>

// 출처: https://fe-developers.kakaoent.com/2022/221110-error-boundary/

global error boundary는 루트 레벨에서 에러를 처리하는데, 서버 점검과 같이 페이지 전체에 에러를 보여줘야 하는 경우나 하위 error boundary에서 처리하지 못한 에러를 catch하는 용도다.

설명에서 짐작할 수 있듯이 error boundary는 루트에 위치할 필요도 없고 모든 컴포넌트를 감싸지 않아도 된다. 개발자의 의도에 따라 배치할 수 있다는 것이다.

 

단, error boundary가 처리하지 않는 예외 상황이 있다.

  • 이벤트 핸들러
  • 비동기적 코드 (setTimeout 등)
  • 서버사이드 렌더링
  • 자식이 아닌 Error Boundary 자체에서 발생한 예외

error boundary는 렌더링 중에 발생하는 예외만 처리한다. 이벤트 핸들러에서 에러 처리를 하려면 try-catch로 직접 처리해야 한다.

const throwError = () => {
  try {
    throw new Error("Error!");
  } catch (err) {
    console.error(err);
  }
};

앞서 언급한 react-error-boundary에서는 이벤트 핸들러에서 직접 try-catch하는 것을 쉽게 해주는 useErrorHandler 기능을 제공한다.

// App.js
import {ErrorBoundary} from 'react-error-boundary';

const ErrorFallback = (err) => {
  return(<div>폴백 UI</div>)
}

function App() {
  return (
    <div className="App">
      <ErrorBoundary FallbackComponent={ErrorFallback}>
      <MyComponent />
      </ErrorBoundary>
    </div>
  );
}

export default App;
// MyComponent
import { useErrorHandler } from 'react-error-boundary';
...
const handleError = useErrorHandler();
...
try {
		...
      }
      catch(err) {
        handleError(err);
      }
...

그동안 신경쓰지 못했던 사용자 경험을 위한 기술들을 공부하기로 결심했다. 이번 프로젝트에 잘 적용해 봐야겠다.

728x90