콘텐츠로 이동

React 에러 바운더리 — 어디에, 얼마나 배치할 것인가

에러 바운더리는 React에서 런타임 에러를 잡아내는 유일한 선언적 방법이다. 하지만 “어디에” 배치할지에 대한 가이드는 부족하다. 너무 적으면 앱 전체가 터지고, 너무 많으면 반쯤 깨진 UI가 오히려 사용자를 혼란스럽게 한다.

에러 바운더리가 하나도 없으면, 하위 컴포넌트 하나의 에러가 전체 앱을 날린다.

function App() {
return (
<Layout>
<Header />
<Sidebar />
<Feed /> {/* 여기서 에러 → 앱 전체 화이트스크린 */}
<TrendingTopics />
</Layout>
);
}

사용자 입장에서는 피드 하나의 문제 때문에 사이트 전체를 잃는 셈이다.

앱 전체를 감싸는 하나의 바운더리

섹션 제목: “앱 전체를 감싸는 하나의 바운더리”

가장 흔한 접근법이지만, 결과는 “에러 바운더리가 없을 때”와 크게 다르지 않다.

function App() {
return (
<ErrorBoundary fallback={<ErrorPage />}>
<Layout>
<Header />
<Sidebar />
<Feed />
<TrendingTopics />
</Layout>
</ErrorBoundary>
);
}

화이트스크린 대신 에러 페이지를 보여주지만, 사용자가 할 수 있는 건 여전히 새로고침뿐이다.

너무 많은 바운더리 — 반쯤 깨진 UI

섹션 제목: “너무 많은 바운더리 — 반쯤 깨진 UI”

반대로 모든 컴포넌트를 각각 감싸면 어떨까?

function Feed() {
return (
<div>
{tweets.map((tweet) => (
<ErrorBoundary key={tweet.id} fallback={<TweetError />}>
<Tweet data={tweet} />
</ErrorBoundary>
))}
</div>
);
}

개별 트윗이 실패해도 다른 트윗은 보인다. 언뜻 좋아 보이지만, 반쯤 깨진 UI가 정상 UI보다 더 혼란스러울 수 있다.

예를 들어, 좋아요 버튼에 에러 바운더리를 걸면:

// 트윗은 보이는데 좋아요 버튼만 에러 fallback으로 대체
<Tweet>
<TweetContent />
<ErrorBoundary fallback={<span>오류</span>}>
<LikeButton /> {/* 이게 터지면 */}
</ErrorBoundary>
<RetweetButton />
<ShareButton />
</Tweet>

사용자는 “좋아요를 누를 수 없는 트윗”을 보게 된다. 이건 기능 장애가 아니라 혼란이다.

올바른 배치를 찾기 위한 재귀적 질문이 있다:

“이 컴포넌트가 죽으면, 형제 컴포넌트도 함께 숨겨야 하는가?”

이 질문을 컴포넌트 트리를 거슬러 올라가며 반복한다.

App
├── Header
├── Sidebar
│ ├── Profile
│ ├── Navigation
│ └── TrendingTopics
├── Feed
│ ├── TweetComposer
│ └── TweetList
│ ├── Tweet
│ │ ├── TweetContent
│ │ ├── LikeButton
│ │ ├── RetweetButton
│ │ └── ShareButton
│ └── Tweet ...
└── Recommendations

LikeButton이 죽으면?

  • RetweetButton, ShareButton도 의미가 퇴색 → Tweet 전체를 숨기는 게 낫다
  • 하지만 다른 Tweet까지 숨길 필요는 없다

Tweet 하나가 죽으면?

  • 다른 Tweet은 독립적 → 개별 Tweet만 숨기면 된다
  • TweetComposer는 영향 없음

Sidebar가 죽으면?

  • Header는 독립적 → Sidebar만 숨기면 된다
  • Feed는 Sidebar 없이도 동작 가능

결론적으로 다음과 같은 배치가 된다:

function App() {
return (
<Layout>
<Header />
<ErrorBoundary fallback={<SidebarError />}>
<Sidebar />
</ErrorBoundary>
<ErrorBoundary fallback={<FeedError />}>
<Feed />
</ErrorBoundary>
<ErrorBoundary fallback={<RecommendationsError />}>
<Recommendations />
</ErrorBoundary>
</Layout>
);
}
function Feed() {
return (
<div>
<TweetComposer />
{tweets.map((tweet) => (
<ErrorBoundary key={tweet.id} fallback={<TweetError />}>
<Tweet data={tweet} />
</ErrorBoundary>
))}
</div>
);
}

에러 바운더리는 독립적으로 동작할 수 있는 UI 단위를 기준으로 배치한다.

에러 바운더리를 배치했으면 실제로 테스트해봐야 한다. 가장 간단한 방법은 의도적으로 에러를 던져보는 것이다.

// 테스트하고 싶은 컴포넌트에 임시로 추가
function LikeButton() {
throw new Error('테스트용 에러');
// ... 원래 코드
}

이 상태에서 UI를 확인한다:

  1. 에러가 해당 영역에만 격리되는가? — 다른 부분이 멀쩡한지
  2. fallback UI가 의미 있는가? — 사용자가 상황을 이해할 수 있는지
  3. 부분 실패된 UI가 혼란스럽지 않은가? — 차라리 더 넓은 범위를 숨기는 게 나은지
// 개발 환경에서만 동작하는 에러 트리거
function DevErrorTrigger({ children }: { children: React.ReactNode }) {
const [shouldThrow, setShouldThrow] = useState(false);
if (shouldThrow) {
throw new Error('DevErrorTrigger: 의도적 에러');
}
if (process.env.NODE_ENV !== 'production') {
return (
<div>
<button onClick={() => setShouldThrow(true)} style={{ fontSize: 10 }}>
💥
</button>
{children}
</div>
);
}
return <>{children}</>;
}

이 컴포넌트를 에러 바운더리 안에 넣으면 클릭 한 번으로 에러 시나리오를 테스트할 수 있다.

  • 에러 바운더리가 없으면 한 곳의 에러가 앱 전체를 죽인다
  • 에러 바운더리가 너무 많으면 반쯤 깨진 UI가 사용자를 혼란스럽게 한다
  • “형제도 함께 숨겨야 하나?” 질문을 재귀적으로 반복해서 최적의 경계를 찾는다
  • 에러 바운더리는 독립적 UI 단위를 기준으로 배치한다
  • 배치 후 반드시 throw new Error테스트한다

출처: developerway.com — “Fault Tolerance in React”