본문 바로가기

개발

에러 핸들링 feat. Error Boundary, react-error-boundary

개발을 하다보면 새하얀 배경에 에러 메세지로 가득한 화면을 심심치 않게 맞닥뜨린다. 실제 서비스 단계에서 이런 상황이 닥친다면 아무 안내도 받지 못한 사용자는 당황하다가 페이지를 이탈할 가능성이 크다. 반대로 예상치 못한 에러가 발생하더라도 재시도나 홈으로 이동 등 유저에게 에러 대처 기능을 제공하면 사용자가 바로 이탈할 가능성은 줄어들 것이다. 즉, 에러를 어떻게 처리하는지는 웹에서 상당히 중요하다.


이전 회사에서는 에러 처리 로직이 제대로 되어 있지 않았다. 기능 구현하느라 바빴고, 에러가 발생하면 빠르게 해결해 재배포하는 식이었다. 언제 한 번 제대로 공부해야지 생각만 하고 있다가 이번에 기회가 되어 에러 처리 로직을 개선해보게 되었다. 이 과정에서 얻은 지식을 토대로 FE에서의 에러 처리에 대해 전반적으로 정리해보려고 한다.

프론트 단에서 에러가 발생하는 경우는 몇 가지가 있다. 런타임에 동적으로 렌더링하는 과정에서 생기는 렌더링 에러, Event Handler에서 발생하는 에러, 서버에 비동기 요청에서 발생하는 에러 정도다. 서버 컴포넌트에서 발생하는 에러도 있지만 이번 글에서는 클라이언트에서 발생하는 에러에 집중해보겠다.

기본적인 에러 처리 : try / catch

기본적으로 javascript에서 에러를 처리하는 방법은 try / catch가 있다. try문 안에서 발생한 에러는 catch문 안으로 던져지고 거기서 에러를 처리하는 것이다. 웬만한 에러는 이 방식을 통해 처리할 수 있다.

 

 

UI를 담당하는 컴포넌트 내부에서도 try에서 정상적인 UI를, catch에서는 fallback UI를 반환하도록 코드를 작성하면 가능은 하다. 다만, 여러 컴포넌트를 한번에 try catch로 처리할 수는 없다.

아래와 같은 코드에서 Component1에서 에러가 난다고 가정했을 때, catch 안의 Error div는 렌더링되지 않는다.

try {
    return (
        <div>
            <Component1 />
            <Component2 />
        </div>
    )
} catch {
	return <div>Error occurred!</div>
}


그 이유는 우리가 리액트 컴포넌트를 call 하는 주체가 아니기 때문이다. 우리는 '함수' 컴포넌트를 선언만 해 놓을 뿐이고, React가 컴포넌트를 직접 'call' 한다. try catch로 컴포넌트 선언부를 감싸놔도 React는 해당 컴포넌트에서 에러가 발생했는지 아닌지 파악할 수 없는 것이다. 이 문제를 해결하기 위해 React가 내놓은 대안이 바로 ErrorBoundary다.

 

UI 에러 처리 : ErrorBoundary

https://ko.legacy.reactjs.org/docs/error-boundaries.html

 

ErrorBoundary는 React 컴포넌트의 트리 내에서 발생하는 렌더링 오류를 잡기 위한 기능이다. 내부적으로 동작하는 getDerivedStateFromError()로 에러를 포착해 깨진 UI 대신, Fallback UI를 렌더링한다. 이를 통해 유저에게 의도된 에러 화면을 보여줄 수 있다.

<ErrorBoundary fallback={<div>Error occurred!</div>}>
    <Component1 />
    <Component2 />
</ErrorBoundary>


문제는 비동기 요청이나 event handler에서 발생하는 에러의 경우에는 단순히 ErrorBoundary로는 에러를 catch 할 수 없다는 것이다. 렌더링이나 리액트 라이프사이클과 별개로 실행되는 작업에서는 React가 감지할 수 없기 때문이다. 이런 경우에는 try catch를 이용해 처리를 해야한다. 내 경우에는 그럼에도 불구하고, 이런 상황들에서 에러가 발생할 때 특정 컴포넌트를 바꿔주고 싶었다. 검색을 통해 두 가지 방법을 찾았다.

비동기 에러 처리 : react-error-boundary

첫 번째는 에러를 담는 상태값을 하나 선언해 리렌더링을 유도하는 방법이다. 에러가 발생하면 setState로 상태값을 변경 해준다. 변경된 상태값으로 리렌더링된 컴포넌트 내부에서 `if (error) { throw error }` 코드가 작동해 수동으로 에러를 다시 throw 하는 것이다. 그러면 이제 ErrorBoundary가 동작하게 된다.

 

이 방법이 상당히 번거롭다고 생각했는데 두 번째 방법으로 react-error-boundary라는 아주 유용한 라이브러리를 찾았다. 첫 번째 방법과 사실 원리는 동일한데 함수형으로 되어 있어 사용하기 아주 편리했다. class 컴포넌트를 직접 선언할 필요 없이 ErrorBoundary를 편리하게 가져다 쓸 수 있다.

 

useErrorBoundary 훅으로 showBoundary와 resetBoundary라는 유용한 함수들을 가져올 수 있다.

"use client";

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

function Example() {
  const { showBoundary } = useErrorBoundary();

  useEffect(() => {
    fetchGreeting(name).then(
      response => {
        // Set data in state and re-render
      },
      error => {
        // Show error boundary
        showBoundary(error);
      }
    );
  });

  // Render ...
}

showBoundary는 수동으로 에러를 throw하는 것과 같이 동작한다. 에러 상황에서 showBoundary 함수만 실행시키면 간단하게 ErrorBoundary의 Fallback UI가 렌더링된다.

"use client";

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

function ErrorFallback({ error }) {
  const { resetBoundary } = useErrorBoundary();

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

resetBoundary는 현재 실행된 ErrorBoundary를 dismiss 할 수 있다. 재시도 버튼 등을 활용해 사용자가 기존에 불러오는데 실패했던 UI 렌더링을 다시 시도할 수 있게 도와준다.

참고

https://ko.legacy.reactjs.org/docs/error-boundaries.html

 

에러 경계(Error Boundaries) – React

A JavaScript library for building user interfaces

ko.legacy.reactjs.org

https://ko.legacy.reactjs.org/docs/react-component.html#static-getderivedstatefromerror

 

React.Component – React

A JavaScript library for building user interfaces

ko.legacy.reactjs.org

 

https://github.com/bvaughn/react-error-boundary

 

GitHub - bvaughn/react-error-boundary: Simple reusable React error boundary component

Simple reusable React error boundary component. Contribute to bvaughn/react-error-boundary development by creating an account on GitHub.

github.com