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 ...└── RecommendationsLikeButton이 죽으면?
- 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를 확인한다:
- 에러가 해당 영역에만 격리되는가? — 다른 부분이 멀쩡한지
- fallback UI가 의미 있는가? — 사용자가 상황을 이해할 수 있는지
- 부분 실패된 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로 테스트한다