과거에는, 컴포넌트 내부에서 발생한 자바스크립트 에러가 리액트의 internal 상태를 오염시키고 다음 렌더링에서 암호화된 에러를 방출 (emit cryptic error)했습니다. 이런 에러는 언제나 application code이전 단계의 문제로 발생했지만, 리액트에선 컴포넌트 내부에서 이런 에러를 방지하거나 회복하는 방법을 제공해주지 않았습니다.

Introducing Error Boundaries

UI의 일부분에 있는 에러가 전체 app을 중단시켜선 안됩니다. 이러한 문제를 해결하기 위해 리액트 16은 리액트 유저들에게 "error boundary"라는 새로운 개념을 소개했습니다.

Error boundaries는 하위 컴포넌트 트리의 어디에서든 자바스크립트 에러를 기록하며 깨진 컴포넌트 트리 대신 fallback UI를 보여주는 React 컴포넌트입니다. Error boundary는 렌더링 과정중 생명주기 메서드 및 그 아래에 있는 전체 트리의 생성자에서 에러를 잡아냅니다.

Error boundary는 다음과 같은 에러를 포착하지 않습니다

  • Event handler
  • Asynchronous code (e.g. setTimeout or requestAnimationFrame callbacks)
  • Server side rendering
  • Errors thrown in the error boundary itself (rather than its children)

클래스형 컴포넌트에서 생명주기 메소드인 static getDerivedStateFromError()componentDidCatch() 를 구현한다면 (둘 다 구현도 가능) 컴포넌트는 Error boundary로써 동작합니다.

  • static getDerivedStateFromError() - 에러가 발생한 경우 상태 변화를 통해 fallback UI를 렌더링하도록 도와줍니다.
  • componentDidCatch() - 에러 정보를 기록하기 위해 사용합니다.
class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    // Update state so the next render will show the fallback UI.
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    // You can also log the error to an error reporting service
    logErrorToMyService(error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      // You can render any custom fallback UI
      return <h1>Something went wrong.</h1>;
    }

    return this.props.children; 
  }
}

이런 방식으로 구현한 Error boundary는 일반적인 컴포넌트처럼 사용할 수 있습니다.

<ErrorBoundary>
  <MyWidget />
</ErrorBoundary>

Error boundary는 자바스크립트의 catch {} 블록처럼 동작하지만 컴포넌트에 적용됩니다. 클래스형 컴포넌트만이 Error boundary가 될 수 있습니다. 실제로 대부분의 경우 Error boundary 컴포넌트를 한 번만 선언하여 어플리케이션 전체에서 활용하려고 할 것입니다.

Error boundary는 오직 자신의 하위에 있는 컴포넌트 트리의 에러만을 포착합니다 Error boundary 자체적으로 발생하는 에러는 포착할 수 없습니다. 만약 error boundary가 에러 메시지 렌더링에 실패하면, 에러는 트리 상위의 closest error boundary로 전파(propagate)됩니다. 이러한 특성 또한 자바스크립트의 catch {} 블록과 유사합니다.

Live Demo

Check out this example of declaring and using an error boundary with React 16

Where to Place Error Boundaries

Error boundary의 세분화된 부분은 여러분에게 달렸습니다. Top-level 라우터를 감싸 "Something went wrong"등 서버사이드 프레임워크가 자주 사용하는 메시지를 표시할 수도 있습니다. 또한 각각의 위젯을 감싸 에러가 프로젝트 전체를 충돌에서 보호할 수 있습니다.

New Behavior for Uncaught Errors

React 16부터는 Error boundary에서 포착되지 않은 에러로 인해 전체 React 컴포넌트 트리의 마운트가 해제됩니다. 리액트팀은 경험적으로 잘못된 UI(에러로 인해)를 보여주는 것 보다 완전히 제거하는 것이 더 좋다고 판단했습니다.

예를 들어, 메신저등에서 잘못된 UI가 표시되는 경우 유저로 하여금 잘못된 사람에게 메시지를 보낼 수 있습니다. 비슷하게 payment등 금액과 관련된 정보가 잘못 합계되어 보여지는 것 보다 아무것도 표시하지 않는 것이 더 좋을 수 있습니다.

이 변경사항은 React 16으로 마이그레이션 할 때 애플리케이션에서 이전에 알려지지 않았던 기존에 존재하던 충돌을 발견할 수 있음을 의미합니다. Error boundary를 추가함으로써 문제가 발생했을 때 더 나은 사용자 경험을 제공할 수 있습니다.

예를 들어 페이스북 메신저는 사이드 바, 정보 패널, 대화 기록과 메시지 입력을 각각 별도의 에러 경계로 감싸두었습니다. 이 UI 영역 중 하나의 컴포넌트에서 충돌이 발생하면 나머지 컴포넌트는 대화형으로 유지됩니다.

또한 프로덕션 환경에서 발생한 처리되지 않은 예외 상황에 대하여 학습하고 수정할 수 있도록 자바스크립트 에러 리포팅 서비스를 활용하거나 직접 작성하는 것을 권장합니다.

Component Stack Traces

React 16은 어플리케이션이 실수로 에러를 집어삼킨 경우에도 개발과정에서 렌더링 중 일어난 모든 에러를 콘솔창에 표시합니다. 에러메시지와 자바스크립트 스텍과 더불어 Component Stack Trace역시 제공해줍니다. 이제 여러분은 어떤 컴포넌트 트리에서 실패가 일어났는지 정확하게 확인할 수 있습니다.

Component stack trace에서 여러분은 파일명과 라인 넘버도 확인할 수 있습니다. 이는 CRA 프로젝트에서 기본적으로 동작합니다.

만약 CRA를 사용하지 않는다면, 플러그인을 바벨 설정에 등록하여 사용할 수 있습니다. 이러한 기능은 오로지 개발자들을 위한 기능으로 프로덕션에선 절대 사용되면 안됩니다.

stack traces에 표현되는 컴포넌트명은 Function.name 프로퍼티에 의존합니다. 만약 기본적으로 제공되지 않는 오래된 브라우저나 기기를 지원한다면 function.name-polyfill 같은 Function.name polyfill을 어플리케이션 번들에 포함시키는 것을 고려해 보아야 합니다. 대안으로
모든 컴포넌트에 displayName 프로퍼티를 설정할 수도 있습니다.

How About try/catch?

try/catch는 훌륭하지만 명령형 코드에서만 동작합니다.

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

반면 리액트 컴포넌트는 선언형으로 "무엇을" 렌더링할지 명세합니다.

<Button />

명령형과 선언형의 차이를 모른다면 명령형 vs 선언형 글을 읽어보시면 좋습니다.

Error bounadry는 React의 선언적 특성을 보존하고 예상한대로 동작합니다. 예를 들어 트리 깊숙한 어딘가에 있는 setState로 인해 componentDidUpdate 메소드에서 에러가 발생한다면, 에러는 여전히 정확하게 closest error boundary로 전파(propagate)됩니다.

How About Event Handlers?

Error boundary는 이벤트 핸들러 안의 에러를 포착하지 않습니다.

리액트는 이벤트 핸들러 내부의 에러를 회복하기 위해 Error boundary를 사용할 필요가 없습니다. render 메소드나 생명주기 메소드와 다르게, 이벤트 핸들러는 렌더링 중 동작하지 않습니다. 그래서 이벤트 헨들러 에러가 던져지면, 리액트는 여전히 어떤것을 화면에 표시해야 할지 알 수 있습니다.

만약 이벤트 핸들러 내부에서 에러를 포착해야 한다면, 자바스크립트 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 <button onClick={this.handleClick}>Click Me</button>
  }
}

위의 예시는 일반적인 자바스크립트 동작을 보여주며 Error boundary를 사용하지 않습니다.

Naming Changes from React 15

React 15는 unstable_handleError라는 이름으로 Error boundary기능을 매우 제한적으로 포함합니다. 이 메소드는 더 이상 동작하지 않으며 첫 16 베타 릴리즈부터 코드에서 componentDidCatch로 변경해야 합니다.

이 변경사항을 위해 코드를 자동으로 마이그레이션하기 위한 codemod를 제공했습니다.

profile
웹 개발을 공부하고 있는 윤석주입니다.

0개의 댓글