RSS듀오랩스
React

React Error Boundary와 try/catch의 차이

작성자
듀오랩스 대표·7분 읽기

대시보드에 위젯이 여섯 개 있습니다. 그중 매출 차트 위젯이 서버에서 예상과 다른 모양의 데이터를 받아 렌더링 중에 오류를 냅니다.

function SalesChart({ data }) {
  const max = Math.max(...data.points.map((p) => p.value)); // data.points 가 없으면 여기서 오류
  // ...
}

화면에는 차트 하나가 아니라 대시보드 전체가 사라지고 흰 화면만 남습니다. 나머지 다섯 위젯은 멀쩡했는데 함께 보이지 않습니다.

오류는 try/catch 로 잡으면 된다는 생각

자바스크립트에서 오류를 다루는 기본 도구는 try/catch 입니다. 그래서 컴포넌트의 오류도 컴포넌트를 쓰는 쪽에서 감싸 잡으면 된다고 생각하기 쉽습니다.

function Dashboard() {
  try {
    return <SalesChart data={data} />;
  } catch (e) {
    return <p>차트를 불러오지 못했습니다</p>;
  }
}

이 코드는 SalesChart 의 오류를 잡지 못합니다. <SalesChart /> 라는 JSX 를 만드는 순간에는 SalesChart 함수가 아직 호출되지 않았기 때문입니다. 리렌더링 글에서 렌더링은 React 가 컴포넌트를 호출하는 일이라고 했습니다. Dashboard 는 「이 자리에 SalesChart 를 그려 달라」는 설명만 돌려주고 끝나며, 실제 호출은 나중에 React 가 합니다. 그때 난 오류는 Dashboardtry 블록 밖입니다.

렌더링 오류를 잡는 경계, Error Boundary

그래서 React 는 렌더링 중의 오류를 잡는 전용 장치를 둡니다. Component 문서의 설명입니다. 렌더링 중에 오류가 나면 React 는 기본적으로 화면 전체의 UI 를 제거하고, 이를 막으려면 Error Boundary 로 감싸 무너진 부분 대신 대체 UI 를 보여 줄 수 있습니다.

처음의 흰 화면이 바로 이 기본 동작입니다. 앱 어디에서든 렌더링 오류가 경계 없이 올라가면 루트까지 올라가 전체가 사라집니다.

Error Boundary 는 Suspense 글에서 본 Suspense 경계와 모양이 닮았습니다. Suspense 가 「아직 준비되지 않음」을 가장 가까운 경계에서 fallback 으로 바꾸듯, Error Boundary 는 「렌더링 실패」를 가장 가까운 경계에서 대체 UI 로 바꿉니다.

<ErrorBoundary fallback={<p>차트를 불러오지 못했습니다</p>}>
  <SalesChart data={data} />
</ErrorBoundary>

이제 차트가 실패해도 그 자리에만 안내 문구가 나오고, 나머지 위젯은 그대로 보입니다.

함수 컴포넌트로는 직접 만들 수 없다

Error Boundary 를 만드는 방법에는 제약이 있습니다. 문서의 문장입니다.

There is currently no way to write an Error Boundary as a function component.

클래스 컴포넌트에서 static getDerivedStateFromError 로 오류가 났다는 state 를 만들고, componentDidCatch 로 오류를 기록합니다. 문서는 이 클래스를 직접 쓰지 않아도 되며 react-error-boundary 같은 라이브러리를 쓸 수 있다고 안내합니다. Next.js 의 error.tsx 파일처럼 프레임워크가 파일 규칙으로 경계를 만들어 주기도 합니다.

Error Boundary 가 잡지 못하는 오류

이름 때문에 「모든 오류를 잡는 장치」로 이해하기 쉽지만, 문서는 잡지 못하는 경우를 분명히 적습니다.

  • 이벤트 핸들러 안의 오류
  • 서버 렌더링 중의 오류
  • Error Boundary 자신이 낸 오류 (자식이 아니라)
  • setTimeout 이나 requestAnimationFrame 콜백 같은 비동기 코드의 오류

공통점은 렌더링 도중이 아니라는 것입니다. 버튼을 눌러 저장하다 난 오류는 렌더링이 끝난 뒤 이벤트 핸들러에서 일어나므로, 거기서 try/catch 로 잡아 state 로 보여 줘야 합니다. 처음에 try/catch 가 통하지 않던 자리(렌더링)와 정확히 반대입니다. 렌더링 오류는 경계로, 렌더링 밖의 오류는 try/catch 로 잡습니다.

문서는 예외를 하나 적어 둡니다. useTransition 이 돌려준 startTransition 에 넘긴 함수 안에서 던진 오류는 Error Boundary 가 잡습니다. transition 으로 실행한 Action 의 실패를 경계에서 대체 UI 로 보여 줄 수 있다는 뜻입니다.

경계를 어디에 둘 것인가

경계를 몇 개, 어디에 둘지는 「어디까지 함께 무너져도 되는가」의 결정입니다. 문서도 채팅 앱을 예로 들어 설명합니다. 대화 목록을 감싸는 경계를 두는 것이 맞고, 개별 메시지마다 경계를 두는 것도 말이 되지만, 아바타 하나하나를 감싸는 것은 의미가 없다고 적습니다.

제가 보기에 기준은 이렇게 정리됩니다.

자리 경계를 둘까
앱 최상단 둔다. 최소한 흰 화면 대신 「문제가 발생했습니다」와 새로고침 안내를 보여 준다
독립적인 영역 (대시보드 위젯, 사이드바, 댓글) 둔다. 한 영역의 실패가 나머지를 가리지 않게 한다
목록의 각 항목 항목이 외부 데이터에 따라 실패할 수 있으면 둔다
작은 장식 요소 (아바타, 아이콘) 두지 않는다. 부모 경계에 맡긴다

그리고 대체 UI 에는 다시 시도할 방법을 함께 두는 편이 좋다고 봅니다. 일시적인 네트워크 문제였다면 경계의 state 를 초기화하고 다시 렌더링하는 것만으로 복구되는 경우가 있습니다. react-error-boundary 나 Next.js 의 error.tsx 가 재시도 함수를 넘겨주는 이유입니다. Next.js 16 에서는 그 함수 이름이 unstable_retry 입니다.

경계는 기록하는 자리이기도 하다

경계가 오류를 잡아 대체 UI 로 바꾸면 사용자 화면은 조용해지지만, 개발자는 오류가 났다는 사실을 모르게 됩니다. componentDidCatch 가 따로 있는 이유가 이것입니다. 대체 UI 를 보여 주는 일과 별개로, 오류와 컴포넌트 스택을 오류 수집 도구로 보내는 자리입니다. 경계를 추가할 때 기록도 함께 붙이지 않으면, 사용자에게는 안내 문구가 보이고 운영 로그에는 아무것도 남지 않는 상태가 됩니다.

여기까지가 확실한 부분

렌더링 오류 시 전체 UI 가 제거되는 기본 동작, 함수 컴포넌트로 Error Boundary 를 만들 수 없다는 점, 잡지 못하는 네 가지 경우와 startTransition 예외, 경계 위치에 대한 채팅 앱 예는 react.dev 문서 기준입니다. try/catch 가 자식 컴포넌트의 렌더링 오류를 잡지 못하는 이유는 렌더링이 React 의 컴포넌트 호출이라는 문서의 정의에서 따라 나오는 설명입니다.

마지막 수정:

공유하실 때는 출처(Duolabs)와 원문 주소를 표시해 주세요.