기본 콘텐츠로 건너뛰기

React Error Boundary로 화면 전체가 무너지는 오류 막기

React Error Boundary로 화면 전체가 무너지는 오류 막기

빠른 답

  • Error Boundary는 하위 컴포넌트의 렌더링, 생명주기, 생성자에서 발생한 오류를 잡아 fallback UI로 바꾼다.
  • 이벤트 핸들러, 서버 사이드 렌더링, 일반 비동기 콜백의 오류는 자동으로 잡지 않는다.
  • React에서 직접 Error Boundary를 만들 때는 현재도 클래스 API인 static getDerivedStateFromErrorcomponentDidCatch를 사용한다.
  • 데이터 요청 실패는 요청 상태가 소유하고, Error Boundary는 렌더링 실패를 격리하는 역할로 두는 편이 읽기 쉽다.

흐름으로 보기

흐름 다이어그램
React Error Boundary로 화면 전체가 무너지는 오류 막기 흐름 다이어그램

React는 부모에서 자식 방향으로 데이터를 내려보내며 컴포넌트를 렌더링한다. 이때 하위 컴포넌트가 렌더링 중 예외를 던지면 React는 컴포넌트 트리의 부모 방향으로 올라가며 가장 가까운 Error Boundary를 찾는다.

경계를 찾으면 해당 하위 트리를 정상 UI로 계속 유지하지 않고 fallback UI로 교체한다. 이후 componentDidCatch에서 오류 객체와 React 컴포넌트 스택을 로깅할 수 있다. 경계가 없다면 React는 깨진 UI를 남겨두지 않기 위해 루트 UI를 제거할 수 있고, 사용자는 흔히 말하는 하얀 화면을 보게 된다.

데이터 흐름과 상태 소유권

Error Boundary를 이해할 때 먼저 나눠야 할 것은 “예상 가능한 상태”와 “렌더링이 깨진 상태”다. API 요청이 실패했거나 데이터가 아직 로딩 중인 상태는 애플리케이션의 정상적인 데이터 흐름 안에 있다. 이 상태는 데이터를 요청한 컴포넌트나 데이터 패칭 계층이 소유하는 편이 자연스럽다.

반면 렌더링 중 undefined 값을 배열처럼 다루거나, 외부 위젯이 잘못된 설정값 때문에 예외를 던지는 상황은 정상적인 UI 분기로 표현하기 어렵다. Error Boundary는 이런 예외가 화면 전체로 퍼지지 않도록 하위 트리의 렌더링 실패 상태를 소유한다.

function UserPanel({status, user, errorMessage}) {
  if (status === "loading") {
    return <p>사용자 정보를 불러오는 중입니다.</p>;
  }

  if (status === "error") {
    return <p>{errorMessage ?? "사용자 정보를 불러오지 못했습니다."}</p>;
  }

  if (!user) {
    return <p>표시할 사용자 정보가 없습니다.</p>;
  }

  return <p>{user.name}님, 안녕하세요.</p>;
}

위 코드는 Error Boundary에 맡길 문제가 아니다. 로딩, 요청 실패, 빈 데이터는 화면이 다룰 수 있는 상태다. 이런 상태까지 예외로 던지면 데이터 흐름이 흐려지고, 사용자가 재시도할 수 있는 UI를 만들기도 어려워진다.

흔한 안티패턴은 서버 요청 실패, 입력 검증 실패, 렌더링 실패를 모두 Error Boundary로 보내는 방식이다. Error Boundary는 데이터 복구 도구라기보다 렌더링 격리 도구에 가깝다. 데이터 소유자는 loading, error, data를 관리하고, Error Boundary는 “이 하위 트리가 렌더링 가능한가”만 다루도록 역할을 나누면 리렌더링 이유가 더 분명해진다.

잡는 오류와 잡지 못하는 오류

Error Boundary가 잡는 오류는 React가 렌더링 흐름 안에서 만나는 하위 컴포넌트 오류다. 렌더링 함수, 클래스 생명주기 메서드, 생성자에서 발생한 오류가 여기에 해당한다. 예를 들어 items.map(...)을 호출했는데 items가 배열이 아니라면 렌더링 도중 예외가 발생하고, 가까운 Error Boundary가 그 영역을 fallback UI로 바꿀 수 있다.

반대로 버튼 클릭 핸들러 안에서 발생한 오류는 렌더링 중 오류가 아니다. 이 경우에는 일반 JavaScript 코드처럼 try...catch로 처리하거나, 로컬 상태에 실패 메시지를 저장해 화면에 보여주는 편이 맞다. setTimeout, requestAnimationFrame, 일반적인 fetch 콜백에서 발생한 오류도 Error Boundary가 자동으로 대신 처리하지 않는다.

서버 사이드 렌더링 중 발생한 오류도 클라이언트 Error Boundary의 대상이 아니다. Next.js 같은 프레임워크의 라우트 오류 처리 기능은 React Error Boundary와 맞닿아 있지만, 서버 요청 흐름과 라우팅 계층의 오류 처리까지 같은 개념으로 보면 디버깅 범위가 흐려진다.

한 가지 현재 문서 기준의 예외도 있다. React의 useTransition에서 반환된 startTransition 함수 안에서 던져진 오류는 Error Boundary가 처리할 수 있다. 오래된 설명에서 “비동기는 전부 잡지 못한다”라고만 적혀 있다면, 이 차이를 함께 기억하는 편이 좋다.

현재 React 기준과 마이그레이션 포인트

2026년 4월 기준, React 공식 문서는 최신 문서 버전을 React 19.2로 안내한다. Error Boundary의 기본 모델은 React 16에서 도입된 뒤 계속 유지되고 있지만, 설명 방식과 권장 구현에는 몇 가지 차이가 있다.

React에서 함수 컴포넌트가 일반적인 작성 방식이 되었지만, 직접 Error Boundary를 구현하는 API는 아직 클래스 컴포넌트에 남아 있다. static getDerivedStateFromError는 오류 후 어떤 상태로 렌더링할지 계산하고, componentDidCatch는 로깅 같은 부수 효과를 처리한다.

오래된 예제에서는 componentDidCatch 안에서 setState를 호출해 fallback UI로 전환하는 코드가 자주 보인다. 현재 React 문서는 이 방식을 deprecated로 설명하고, fallback 상태 전환은 static getDerivedStateFromError에 두는 쪽을 안내한다. componentDidCatch는 반환값이 없고, 오류 리포팅 서비스로 로그를 보내는 작업에 두는 것이 역할이 더 분명하다.

React 15 시절의 unstable_handleError 같은 이름은 현재 코드에서 사용할 수 없다. 오래된 블로그나 레거시 코드에서 이 이름을 만난다면 componentDidCatchstatic getDerivedStateFromError 조합으로 옮겨야 한다.

공식 문서와 레퍼런스는 아래에서 확인할 수 있다.

기본 Error Boundary 구현

다음은 직접 Error Boundary를 만들 때의 기본 형태다. getDerivedStateFromError는 fallback UI로 전환하기 위한 상태만 반환하고, componentDidCatch는 오류와 컴포넌트 스택을 기록한다.

import React from "react";

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

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

  componentDidCatch(error, info) {
    console.error("Render error:", error);
    console.error("Component stack:", info.componentStack);
  }

  render() {
    if (this.state.hasError) {
      return this.props.fallback;
    }

    return this.props.children;
  }
}

여기서 Error Boundary가 소유하는 상태는 hasError뿐이다. 오류가 난 자식의 데이터를 수정하거나 재요청하지 않는다. 하위 트리가 렌더링 불가능하다는 사실을 기억하고, 다음 렌더링에서 children 대신 fallback을 반환한다.

이 차이가 리렌더링에도 영향을 준다. 한 번 hasErrortrue가 되면, 기본 구현에서는 props가 조금 바뀌어도 fallback UI가 유지된다. 다시 시도해야 하는 조건이 있다면 경계를 새로 마운트하거나 reset 로직을 별도로 설계해야 한다.

경계 배치와 리렌더링 설계

Error Boundary를 앱 루트에 하나만 두면 전체 화면이 사라지는 상황은 줄일 수 있다. 하지만 어떤 영역이 실패했는지 사용자에게 세밀하게 보여주기는 어렵다. 반대로 모든 작은 컴포넌트를 각각 감싸면 fallback UI가 화면 곳곳에 흩어지고 코드의 잡음이 늘어난다.

경계는 라우트, 탭, 독립 위젯처럼 사용자가 “이 영역만 실패했다”고 이해할 수 있는 단위에 두는 편이 좋다. 외부 차트, 추천 목록, 광고, 실험 기능처럼 데이터 모양이나 외부 의존성이 자주 바뀌는 영역은 별도 경계를 둘 만하다.

import {ErrorBoundary} from "./ErrorBoundary";
import {ActivityChart} from "./ActivityChart";
import {ProfileCard} from "./ProfileCard";
import {RecommendationWidget} from "./RecommendationWidget";

export function DashboardPage({userId}) {
  return (
    <>
      <ErrorBoundary
        key={`profile-${userId}`}
        fallback={<p>프로필을 표시하지 못했습니다.</p>}
      >
        <ProfileCard userId={userId} />
      </ErrorBoundary>

      <ErrorBoundary fallback={<p>활동 차트를 표시하지 못했습니다.</p>}>
        <ActivityChart userId={userId} />
      </ErrorBoundary>

      <ErrorBoundary fallback={<p>추천 영역을 표시하지 못했습니다.</p>}>
        <RecommendationWidget userId={userId} />
      </ErrorBoundary>
    </>
  );
}

위 구성에서는 프로필, 차트, 추천 영역이 서로 다른 렌더링 실패 상태를 가진다. 차트가 깨져도 프로필은 남고, 추천 영역이 실패해도 나머지 화면은 유지된다.

key를 바꿔 Error Boundary를 새로 마운트하는 방식은 간단한 reset 전략이다. 예를 들어 userId가 바뀌면 이전 사용자의 오류 상태를 다음 사용자 화면에 남기지 않을 수 있다. 다만 기준을 너무 자주 바뀌는 값으로 잡으면 하위 트리가 불필요하게 다시 마운트된다. reset 기준은 “이전 오류 상태를 새 화면에도 유지해도 되는가”를 기준으로 정하는 편이 좋다.

라이브러리 패턴

함수 컴포넌트 중심 코드베이스에서는 react-error-boundary를 사용하는 경우가 많다. React 공식 문서도 함수 컴포넌트로 직접 Error Boundary를 작성하는 API가 아직 없다고 설명하면서, 이 라이브러리를 대안으로 언급한다.

2026년 4월 기준 react-error-boundary는 6.x 릴리스를 제공한다. 패키지 README는 ES Modules를 지원하지 않는 프레임워크나 런타임에서는 v5 사용을 안내한다. React Server Components 환경에서는 이 컴포넌트가 클라이언트 컴포넌트라는 점도 고려해야 하며, 필요한 위치에 "use client"; 지시문을 둬야 한다.

"use client";

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

function Fallback({error, resetErrorBoundary}) {
  return (
    <section>
      <p>위젯을 표시하지 못했습니다.</p>
      <pre>{error.message}</pre>
      <button onClick={resetErrorBoundary}>다시 시도</button>
    </section>
  );
}

export function ProductRecommendations({productId}) {
  return (
    <ErrorBoundary
      FallbackComponent={Fallback}
      resetKeys={[productId]}
      onError={(error, info) => {
        reportRenderError({
          message: error.message,
          stack: error.stack,
          componentStack: info.componentStack,
          area: "product-recommendations"
        });
      }}
    >
      <RecommendationWidget productId={productId} />
    </ErrorBoundary>
  );
}

resetKeys는 특정 값이 바뀌었을 때 Error Boundary의 오류 상태를 초기화한다. 위 예시에서는 productId가 바뀌면 이전 상품에서 발생한 렌더링 실패 상태를 새 상품 추천 영역에 그대로 남기지 않는다.

onError에서는 오류 메시지와 JavaScript 스택뿐 아니라 componentStack을 함께 남기는 것이 좋다. JavaScript 스택이 함수 호출 위치를 보여준다면, componentStack은 React 컴포넌트 트리에서 어떤 조합의 화면이 깨졌는지를 보여준다.

디버깅 출력과 로그 설계

렌더링 중 의도적으로 throw new Error("BrokenWidget render failed")가 발생하면 개발 콘솔에는 대략 다음과 같은 로그를 남길 수 있다. 실제 파일명, 줄 번호, 컴포넌트 이름은 번들러와 소스맵 설정에 따라 달라진다.

Render error: Error: BrokenWidget render failed
    at BrokenWidget (BrokenWidget.jsx:4:11)

Component stack:
    at BrokenWidget (BrokenWidget.jsx:2:25)
    at ErrorBoundary (ErrorBoundary.jsx:5:5)
    at DashboardPage (DashboardPage.jsx:8:33)
    at App

Render context:
    route=/dashboard
    userId=42
    fallback=activity-chart

운영 로그에는 최소한 error.message, error.stack, componentStack, 현재 라우트, fallback 영역 이름을 함께 남기는 편이 원인 추적에 도움이 된다. 프로덕션 빌드에서는 컴포넌트 이름이 축약될 수 있으므로, 일반 JavaScript 오류 스택과 마찬가지로 소스맵을 이용해 복원할 수 있는 로깅 파이프라인을 준비해야 한다.

개발 빌드와 프로덕션 빌드의 동작 차이도 있다. React 문서는 개발 환경에서 componentDidCatch가 잡은 오류가 window.onerrorwindow.addEventListener("error", ...)까지 버블링될 수 있지만, 프로덕션에서는 명시적으로 잡힌 오류가 다시 버블링되지 않는다고 설명한다. 개발 콘솔에서 전역 핸들러 로그가 함께 보인다고 해서 운영에서도 같은 경로로 기록된다고 가정하면 로그 누락이 생길 수 있다.

흔한 안티패턴

첫 번째 안티패턴은 렌더링 오류를 일반 try...catch로 감싸려는 방식이다. React 공식 lint 문서도 부모 컴포넌트의 반환문 주변에 try...catch를 두는 방식은 자식 컴포넌트 렌더링 오류를 잡지 못한다고 설명한다. React 렌더링 오류는 명령형 함수 호출처럼 그 자리에서 처리되는 것이 아니라 컴포넌트 트리를 따라 Error Boundary로 전파된다.

두 번째 안티패턴은 이벤트 핸들러 오류까지 Error Boundary가 처리한다고 기대하는 것이다. 저장 버튼 클릭 중 실패한 요청, 폼 검증 실패, 권한 오류는 핸들러나 데이터 요청 계층에서 직접 상태로 표현하는 편이 좋다. 사용자는 “다시 시도”나 “입력값 확인” 같은 다음 행동이 필요하고, 이 흐름은 fallback UI 하나로 덮기 어렵다.

세 번째 안티패턴은 fallback UI를 너무 복잡하게 만드는 것이다. Error Boundary는 자기 자신이나 자신의 fallback에서 발생한 오류를 잡지 못한다. fallback에서 다시 같은 깨진 데이터를 깊게 읽거나 추가 요청을 많이 만들면, 오류를 격리하려던 영역이 다시 불안정해질 수 있다. fallback은 오류 메시지, 재시도 버튼, 최소한의 이동 경로처럼 단순한 구성으로 두는 편이 다루기 쉽다.

네 번째 안티패턴은 reset 기준 없이 Error Boundary를 오래 유지하는 것이다. 한 번 오류 상태가 된 경계는 별도 reset이 없다면 계속 fallback을 보여준다. 사용자가 다른 문서, 다른 상품, 다른 사용자 화면으로 이동했는데도 이전 오류 fallback이 남아 있다면 상태 소유권이 맞지 않는 신호다. 이때는 라우트 파라미터나 주요 엔티티 ID를 기준으로 keyresetKeys를 검토할 수 있다.

원문 참고

https://www.maeil-mail.kr/question/75

댓글

이 블로그의 인기 게시물

아이콘 폰트 (icomoon 사용법)

 장난감 프로젝트를 만들다 보면, 아이콘이 필요한 경우가 있다. 간단하게 아이콘을 인터넷에서 검색하여, 이미지로 넣어두고 이미지 태그를 이용하여, 사용하는 경우가 일반적이였지만...  요즘에는 대부분 폰트를 이용하여 아이콘을 노출 한다. 나 같은 경우에도 기본적으로  https://material.io/resources/icons 를 참고하여 아이콘 폰트를 이용할 수 있도록 처리하고, 추가적으로 필요한 아이콘이고, 일상적으로 사용 되지 않는 아이콘의 경우에는  https://icomoon.io 에서 제작하여, 아이콘 폰트로 이용 하곤 한다.  그래서 이번에는 아이콘  https://icomoon.io 의 사용법을 간단히 공유하고자 한다.   들어가자 마자 위의 icoMoonApp버튼을 누르면 아래와 같은 화면이 나타난다.  icomoon에서 무료로 제공하는 아이콘들이 보이면 위에 파란색으로 표시 되어있는 집 모양 세가지를 선택한 후, 아래의 빨간색으로 표시되어있는 Generate Font를 눌러보자.  그리고 나서 바로 다운로드를 요청해보자. icomoon.zip이 다운로드가 될텐데, 압축을 해제해 보면, 아래의 폴더 및 파일들이 있다. 아래에서 중요한 것은 font 폴더와 style.css이다. demo-files fonts demo.html Read Me.txt selection.json style.css <!doctype html > <html> <head> <link rel ="stylesheet" href ="style.css" ></head> </head> <body> <span class ="icon-home" ></span> <span class ="icon-home2" ></span> <span class ="icon-hom...

Chart js와 amchart 비교

Chart js 특징은 위의 그림으로 대체 할 수 있을 듯 하다. 오픈 소스이고, 기본으로 제공하는 차트 종류가 8가지 Canv a s를 이용해서 차트를 그리고, 반응형을 지원한다. amchart amchart는 기본적으로 유료이며, 기본으로 제공하는 차트 종류가 기본적인 차트 + 주식 처럼 보이는 차트 + 지도에 관련된 차트(?) 까지 하면, 기본 제공 하는 종류가 20개 내외 이려나, 일일이 세기에는 양이 좀 많아 보인다. 렌더링은 svg를 통하여 그려지고, 당연 반응형도 지원이 된다. 그러면, 이 둘중에 어떤것이 내 프로젝트에 적합 하냐는 것이 문제이다. 일단, 주식 처럼 보이는 차트나 지도에 관련된 차트(?)가 필요하면, amchart를 선택해야 되는 것은 맞다. 그건 당연한 것이니 빼고 얘기 해보자! 여러 종류의 차트가 필요하다면, 일단은 amchart를 염두해 두는 것이 좋다. 돈 낸 만큼은 하는 듯 하다. 하지만, 기본적인 막대 그래프, 도넛 차트 등, 아주 기본적인 차트들인데, Chart js도 amchart도 그러한 차트가 없을 때가 문제가 된다. 그렇다면, 조금이라도 커스텀이 용이한 것을 찾는 것이 좋을 것이다.  일단 amchart에서 custom이라고 검색 하였을 때, 검색 결과가 61가지가 나온다. 차트의 종류도 많고, 각 차트마다 들어가는 속성이 매우 많기 때문에, 웬만한 내용들은 속성 값을 어떻게 주느냐에 따라서 변경이 가능 하게 된다. 커스텀의 예를 들면, 기본적으로 도넛 파이의 형태를 띄면서, 화살표로 목표를 표시해주는 차트가 필요하다고 생각 해보자. 이것은 amchart로 만든 그래프이고 이것은 chart js로 만든 그래프이다. 모양이 살짝 다르긴 하지만, 완벽하게 똑같이 구현 할 수도 있다. amchart로 만든 그래프의 경우, 저것은 도넛그래프가 아닌 guage 그래프이다. 원래 게이지 그래프는 이와 같...

javascript 압축 파일 다운로드

이번에는 전 게시글의 응용판? 이라고 해야하나....? 어쨋든! 우리는 각각의 파일들을 다운로드 해보았다. 그런데 생각보다 귀찮음?을 느꼇을 것이다. 파일을 각각 다운 받아야 한다는 현실때문에! 그래 파일 두개야 뭐 그렇다 치지... 하지만, 개발자도 사용자도 게으름뱅이이다. 자 결국, 우리가 해야 하는 것은 파일을 한 번에 둘다! 다운 받는 것이다. 물론, 클릭 한번에 여러개의 함수를 엮어서 다운받게 하면 되지만! 크롬에서 자주 봤듯이, 여러개의 파일을 다운로드를 시도하면 <- 여러개의 파일을 다운로드 합니다. 허용 합니까? 하고 물어보는 것을 볼 수 있다. 게다가 다운로드 한 파일들을 찾기도 귀찮다는 것. 자 해결책을 제시해보자면, https://github.com/Stuk/jszip 클라이언트 단에서 파일을 zip파일로 압축을 할 수가 있다! 필요한 작업은 아래와 같다. 0. 데이터 준비 1. BLOB(binary large object)를 만든다. 2. Blob을 URL.createObjectURL을 사용하여, 해당 binary의 주소를 생성. 3. 다운로드가 필요한 파일들을 Zip 객체에 셋팅! 4. a태그를 이용하여, 해당 url 셋팅 하고, 다운로드. 전 게시물과 별로 달라진게 없네... 자 그럼 샘플! 샘플을 보자! http://embed.plnkr.co/NMprnRxqYG0fkHa2J55D/ var util = {} function fixBinary(bin) { //binary to arrayBuffer var length = bin.length var buf = new ArrayBuffer(length) var arr = new Uint8Array(buf) for (var i = 0; i < length; i++) { arr[i] = bin.charCodeAt(i) } return buf } window.onload = function() { ...