React Error Boundary로 화면 전체가 무너지는 오류 막기
빠른 답
- Error Boundary는 하위 컴포넌트의 렌더링, 생명주기, 생성자에서 발생한 오류를 잡아 fallback UI로 바꾼다.
- 이벤트 핸들러, 서버 사이드 렌더링, 일반 비동기 콜백의 오류는 자동으로 잡지 않는다.
- React에서 직접 Error Boundary를 만들 때는 현재도 클래스 API인
static getDerivedStateFromError와componentDidCatch를 사용한다. - 데이터 요청 실패는 요청 상태가 소유하고, 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 같은 이름은 현재 코드에서 사용할 수 없다. 오래된 블로그나 레거시 코드에서 이 이름을 만난다면 componentDidCatch와 static getDerivedStateFromError 조합으로 옮겨야 한다.
공식 문서와 레퍼런스는 아래에서 확인할 수 있다.
- React 버전 문서: React Versions
- Error Boundary API: React Component 문서
- 렌더링 오류를
try...catch로 잡지 않는 이유: error-boundaries lint 문서 - 함수 컴포넌트 코드베이스용 라이브러리: react-error-boundary
기본 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을 반환한다.
이 차이가 리렌더링에도 영향을 준다. 한 번 hasError가 true가 되면, 기본 구현에서는 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.onerror나 window.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를 기준으로 key나 resetKeys를 검토할 수 있다.
원문 참고
https://www.maeil-mail.kr/question/75
댓글
댓글 쓰기