Error Boundary
React 컴포넌트에서 에러가 발생하면 어떻게 될까? 기본적으로는 전체 컴포넌트 트리가 언마운트되면서 빈 화면이 보인다. 사용자 입장에서는 앱이 죽은 것이다.
Error Boundary는 이 문제를 해결한다. 자식 컴포넌트 트리에서 발생한 JavaScript 에러를 잡아서, 전체 앱을 날리는 대신 대체 UI(fallback)를 보여준다.
왜 try-catch로 안 되는가
일반적인 JavaScript 에러 처리는 try-catch다. 그런데 React에서는 이게 잘 안 먹는다.
// 이건 렌더링 중 발생하는 에러를 못 잡는다
try {
return <BrokenComponent />;
} catch (e) {
return <p>에러!</p>;
}
왜냐하면 JSX에서 <BrokenComponent />는 즉시 실행되는 게 아니라, React 엘리먼트 객체를 생성하는 것이기 때문이다. 실제 렌더링은 React가 나중에 하고, 그때 발생하는 에러는 이 try-catch 스코프 밖이다.
Error Boundary는 React의 렌더링 파이프라인 안에서 동작하기 때문에 렌더링 중 에러를 잡을 수 있다.
동작 원리
Error Boundary는 두 가지 생명주기 메서드를 사용하는 클래스 컴포넌트다.
class ErrorBoundary extends React.Component {
state = { hasError: false };
// 에러 발생 시 state 업데이트 → fallback 렌더링
static getDerivedStateFromError(error) {
return { hasError: true };
}
// 에러 로깅 등 부수 효과 처리
componentDidCatch(error, errorInfo) {
console.error('Caught by boundary:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return <h1>문제가 발생했습니다.</h1>;
}
return this.props.children;
}
}
getDerivedStateFromError: 정적 메서드다. 에러가 발생하면 호출되고, 반환값으로 state를 업데이트한다. 렌더 단계에서 실행되므로 부수 효과(side effect)를 넣으면 안 된다.
componentDidCatch: 커밋 단계에서 실행된다. 에러 로깅 서비스에 보고하거나 추가 정보를 수집하는 데 쓴다. errorInfo.componentStack에 어떤 컴포넌트에서 에러가 터졌는지 스택 정보가 담겨 있다.
잡을 수 있는 것 vs 없는 것
Error Boundary가 잡는 에러:
- 자식 컴포넌트의 렌더링 중 에러
- 생명주기 메서드 안에서의 에러
- 생성자(constructor) 안에서의 에러
잡지 못하는 에러:
- 이벤트 핸들러 — 렌더링 밖에서 실행되므로 일반 try-catch로 처리
- 비동기 코드 — setTimeout, Promise 등
- 서버 사이드 렌더링 — 서버에는 DOM 라이프사이클이 없음
- Error Boundary 자기 자신의 에러
이벤트 핸들러 에러를 못 잡는 건 의도적이다. 이벤트 핸들러에서 에러가 나도 React는 무엇을 렌더링해야 하는지 이미 알고 있으므로, 화면이 깨지지 않는다. 렌더링 중 에러와는 상황이 다르다.
배치 전략: 어디에 두느냐가 중요하다
Error Boundary의 위치가 에러 발생 시 사용자 경험을 결정한다.
최상위에 하나만: 앱 전체가 fallback으로 대체된다. "뭔가 잘못됐습니다" 페이지. 최후의 방어선으로는 좋지만, 작은 에러에도 앱 전체가 날아간다.
기능 단위로 감싸기: 특정 섹션만 fallback을 보여주고, 나머지는 정상 동작. 예를 들어 채팅 위젯에서 에러가 나도 메인 콘텐츠는 살아있다.
<Layout>
<ErrorBoundary fallback={<p>피드를 불러올 수 없습니다.</p>}>
<Feed />
</ErrorBoundary>
<ErrorBoundary fallback={<p>채팅 오류</p>}>
<Chat />
</ErrorBoundary>
</Layout>
실전에서는 두 전략을 함께 쓴다. 최상위 boundary로 전체를 감싸고, 중요한 기능 단위마다 추가 boundary를 둔다.
함수 컴포넌트에서 쓰기: react-error-boundary
Error Boundary는 클래스 컴포넌트로만 만들 수 있다. getDerivedStateFromError에 대응하는 Hook이 아직 없기 때문이다. 그래서 react-error-boundary 같은 라이브러리가 나왔다.
import { ErrorBoundary } from 'react-error-boundary';
function ErrorFallback({ error, resetErrorBoundary }) {
return (
<div>
<p>오류: {error.message}</p>
<button onClick={resetErrorBoundary}>다시 시도</button>
</div>
);
}
<ErrorBoundary
FallbackComponent={ErrorFallback}
onReset={() => {
// 상태 초기화 등
}}
>
<App />
</ErrorBoundary>
핵심 이점은 에러 복구다. 기본 Error Boundary는 한번 에러가 발생하면 fallback 상태에 갇히는데, resetErrorBoundary로 정상 상태로 돌아갈 수 있다. resetKeys prop을 주면 특정 값이 변할 때 자동으로 리셋된다.
<ErrorBoundary
FallbackComponent={ErrorFallback}
resetKeys={[userId]} // userId가 바뀌면 자동 리셋
>
<UserProfile userId={userId} />
</ErrorBoundary>
핵심 정리
Error Boundary는 React의 선언적 에러 처리 방식이다. 명령적으로 에러를 잡아서 분기하는 게 아니라, "이 영역에서 에러가 나면 이걸 보여줘"라고 선언한다. React가 컴포넌트 트리를 관리하듯, 에러도 트리 구조에 맞게 관리하는 것이다.