본문 바로가기
Language/React

[React 공식 Advanced Doc] Error Boundaries

by ocwokocw 2021. 2. 11.

- 이 글은 React 공식 홈페이지 Docs v16.8.3 (https://reactjs.org/docs) 에 기반하여 작성한 글입니다.

- Error Boundaries

이전 버전까지는 component 안에서 발생한 Javascript error가 React의 내부상태를 망가뜨리거나, emit, cryptic, errors 에러들의 원인이었다. 이런 에러들은 app 코드에서는 미리알 수 있는 에러들이었지만, React에서는 Component안에서 그 에러들을 다룰 수 있는 방법이 없었고, 복구기능도 없었다.


- Introducing Error Boundaries

어떤 특정 UI에서 발생한 JavaScript 에러가 전체 app을 망가뜨려서는 안된다. React를 쓰는 사용자들에게 이런 문제를 해결해주기 위해서, React 16 에서는 "error boundary"라는 새로운 개념을 추가하였다.

 

Error boundaries는 하나의 React Component인데, 자식 컴포넌트 어느곳에서 JavaScript 에러가 발생하더라도 이런 에러들을 로그로 남기고, 에러 상태의 화면을 표시하는 대신 대체 콘텐츠(React element 형태)를 표시하는 기능을 제공한다.

 

Error boundaries는 rendering중에 자식 Component들의 lifecycle 함수들이나 생성자들에서 발생한 에러를 탐지한다.

주의: Error boundaries 는 다음과 같은 에러는 처리하지 않는다.

  • Event handler
  • 비동기 코드
  • Server side rendering
  • Error boundaries 자기 자신의 에러

error boundary는 class component 여야 하는데 조건이 있다. 아래의 lifecycle method들중 적어도 하나 이상을 구현해야 한다.

  • static getDerivedStateFromError()
  • componentDidCatch()

static getDerivedStateFromError 함수는 에러가 발생하면 대체 콘텐츠를 rendering 하는데 사용된다. componentDidCatch 함수는 에러 정보를 로그로 남긴다.

 

import React from 'react';

class ErrorBoundary extends React.Component{

    constructor(props){
        super(props);
        this.state = {
            hasError: false
        };
    }

    static getDerivedStateFromError(error){
        return {
            hasError: true
        };
    }

    componentDidCatch(error, info){
        console.log("log service:");
        console.log(error);
    }

    render() {

        if(this.state.hasError){
            return <h1>Something went wrong.</h1>;
        }

        return this.props.children;
    }

}

export default ErrorBoundary;
if(this.state.isShowOtherComponent){
	return (
		<React.Suspense fallback={<div>Loading ..</div>}>
			<ErrorBoundary>
				<OtherComponent errorTrigger={true}/>
			</ErrorBoundary>
		</React.Suspense>
	);
}

 

위와 같이 Error Boundary를 정의한 후 ErrorBoundary를 정규 Component 처럼 사용하면 rendering중 발생하는 error를 잡을 수 있다.

 

Test를 위해 errorTrigger에 강제로 true 값을 주고, OtherComponent에서는 해당값이 true 일때 강제로 throw new Error("Test Error"); 코드를 주었다.(해당 부분 생략)

 

버튼을 누르면 에러를 강제로 던지는데 componentDidCatch에서 console.log 함수를 이용해 찍혔는지 개발자도구로 확인해보자.

Error boundaries는 JavaScript의 catch {} 처럼 작동하지만, 오직 class component에 대해서만 작동한다. 실질적으로 error boundary component를 정의하고 application 전반적으로 사용하게 될 것이다. 하지만 error boundaries가 자신의 자식 component들의 에러들에 대해서만 동작한다는 것을 꼭 기억해야 한다. error boundary는 자기 자신의 에러에 대해서 처리할 수 없다. 만약 error boundary가 에러 메시지를 rendering 하는데 실패하면, 에러는 가까운 상위의 error boundary에 전파된다.


- Where to Place Error Boundaries

error boundaries에 대한 분포도는 프로그래머가 어떻게 정의하냐에 따라 달려있다. 어떤 사람은 server-side framework 처럼 최상위 레벨의 route component에 "Something went wrong" 문구를 단순하게 보여주기로 할 수도 있고, 어떤 사람은 각 widget 별로 error boundaries를 감싸서 rendering에 실패한 component외의 나머지 component들은 정상적으로 rendering 하게 할 수도 있다.


- New Behavior for Uncaught Errors

React 16 에서는 위의 소제목처럼 중요한 변경사항이 있다. error boundary 에서 처리되지 않은 에러들이 있다면 React component tree 전체를 마운팅 하지 않는다.

 

React측은 위와 같은 결정을 내리기까지 고민한것으로 보인다. 위와같이 결정한 이유는 깨지는 UI를 보여주느니 완전히 그 UI를 보여주지 않는게 낫다고 판단했다고 한다.

 

예를 들면 Messanger에서 UI를 깨진채로 보여준다면 잘못된 사람에게 메시지를 전송할 수 있거나, 결재앱에서 잘못된 합계를 보여주는니 차라리 보여주지 않는게 낫다는 논지에서 위와같은 결정을 했다고 한다. 이와 같은 이유 때문에 만약 React 16 으로 버전을 마이그레이션 한다면 조심해야 한다. error boundaries를 구현해서 app이 잘못되었을 때 사용자에게 더 나은 UX를 제공해야 한다.

 

예를 들면 Facebook Messenger는 sidebar의 내용과 정보 panel, 대화 log, message 입력각각에 대해 error boundaries를 따로 구현하였다고 한다. 만약 위의 UI 파트중에 어떤 component가 잘못된다 하더라도 나머지는 여전히 정상적으로 동작할 것이다.


- Component Stack Traces

React 16은 개발자도구 console에 rendering 하면서 발생한 모든 에러들에 대해서 출력한다.(application에서 무시하게 처리를 하더라도 출력한다.)

 

또 에러메시지와 JavaScript stack뿐만 아니라 component stack trace도 제공한다. 이 기능덕분에 component tree 어디에서 에러가 발생했는지 알 수 있다.

또 component stack trace의 해당 파일이름과 몇 번째 줄인지도 볼 수 있다. Create React App project에서는 이 설정이 default 이다.

 

만약 Create React App을 이용하여 프로젝트를 생성하지 않았다면 Babel 환경설정에 직접 plugin을 추가해야 한다. 그리고 주의사항이 있는데 원래 개발용으로 만든기능이라고 production에서는 해당기능을 반드시 제거해야 한다.

 

주의: stack trace에서 보이는 Component 이름은 Function.name 속성을 참조한다. 만약 구버전의 브라우저나 기기를 지원해야 한다면 이를 지원하지 않으므로 function.name-polyfill과 같은 Function.name polyfill(해당 기능을 지원할 수 있게 해주는 대체 프로그램)을 추가해야 한다.

 

아니면 component에 displayName을 명시적으로 선언할 수도 있다.


- How About try/catch?

try/catch는 에러처리에 있어서 강력하긴 하지만 코드를 강제로 작성해야 한다.

 

try {
  showButton();
} catch (error) {
  // ...
}

 

반면 React component는 정의하는 시점에 어떤것이 rendering 되어야 하는지 알 고 있다. Error boundaries는 React의 기존의 선언문법을 변경하지 않고서 코더가 원하는대로 에러처리에 대해 동작한다.

 

Component tree 어딘가에서 setState함수에 의해 componentDidUpdate 함수에서 에러가 발생했다 하더라도, 가장 가까운 error boundary로 정확히 에러를 전파한다.


- How About Event Handlers?

Error boundaries는 event handler의 내부에러를 처리하진 않는다. React는 event handler에서 발생한 error를 복구하는데 error boundaries를 필요로 하지 않는다. render method나 lifecycle method와 달리 event handler는 rendering하는 도중 일어나는 동작이 아니기 때문이다.

 

만약 에러가 발생해도 React는 화면에 어떤 요소가 rendering rendering되어야 하는지 알고 있기 때문이다.

event handler에서 에러를 처리하려면 우리가 알고 있듯이 javascript 의 try/catch 문을 사용해야 한다.

 

class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = { error: null };
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    try {
      // Do something that could throw
    } catch (error) {
      this.setState({ error });
    }
  }

  render() {
    if (this.state.error) {
      return <h1>Caught an error.</h1>
    }
    return <div onClick={this.handleClick}>Click Me</div>
  }
}

 

위의 예제는 JavaScript의 try/catch 문을 사용했지만 error boundaries는 사용하지 않았다.


- Naming Changes from React 15

React 15 에서는 unstable_handleError 라는 메소드 이름으로 error boundaries 기능을 제한적으로 지원하였다. 이 메소드는 사장되었고 대신 16버전 부터는 componentDidCatch 로 변경해서 사용해야 한다. 공식홈에서는 자동으로 code를 migration 하고 싶다면 codemod를 사용하라고 권장하고 있다.(나는 사용해보지 않았다.)

https://github.com/reactjs/react-codemod#error-boundaries

 

댓글