
지난번 "React lazy, Suspense 동작 방식 알아보기" 글 작성 중 ErrorBoundary와 Suspense의 차이에 대해 작성한 부분이 있습니다.
Error Boundary는 자식에게 error객체를 catch하기 때문에 에러 처리를 세부적으로 처리할 수 있지만 Suspense는 자식에게 Promise를 catch 하기 때문에 성공 유무에 따른 fallback UI 처리에 목적을 두고 있다고 합니다.
요약하면
Error Boundary는 오류를, Suspense는 비동기 데이터의 로딩 상태를 처리하는데에 사용되는 것입니다.
해당 부분을 작성하며 ErrorBoundary가 포착할 수 없는 에러에 대해 기술되어있는 것을 보았는데, 공식 문서를 기반으로 제가 공부한 것을 정리합니다.

목차
- 이벤트 핸들러
- 비동기적 로직
- 서버 사이드 렌더링
- 자식에서가 아닌 경계 자체에서 발생하는 에러
- 마무리
이벤트 핸들러
이벤트 핸들러 내에서 발생한 에러는 ErrorBoundary가 포착하지 않습니다.
export default function App() {
return (
<ErrorBoundary>
<Button />
</ErrorBoundary>
);
}
function Button() {
return (
<button
onClick={() => {
throw new Error('error');
}}
>
Click me
</button>
);
}
간단한 예시입니다. 이를 이해하기 위해서는 React 이벤트 핸들러가 바인딩 되는 과정을 이해할 필요가 있습니다.
Vanilla JS에서 이벤트를 처리하는 방식은 다음과 같습니다.
document.getElementById("button").addEventListner("click", (e) => {
// 콜백 함수
});
- JS를 사용해서 이벤트 핸들러를 할당한 DOM node를 선택한다.(getElementById, querySelector 등)
- 선택한 DOM node에 addEventListner()를 사용해 콜백 함수를 등록한다.
특정 노드를 찾아 이벤트를 등록하는 방식인 Vanlia JS와 달리 React는 모든 이벤트를 root element에서 핸들링 합니다.

사진에서 button, input, div등 모든 이벤트 핸들러는 root DOM Element에 할당 되고 있음을 알 수 있습니다.
이러한 이벤트 핸들러의 동작은 실제 코드를 통해 확인해볼 수 있습니다.
export default function App() {
return (
<div className="App">
<button
onClick={(e) => {
console.log(e.nativeEvent.currentTarget);
}}
>
test
</button>
</div>
);
}

이렇게 모든 이벤트 핸들러들이 root node에 등록되기 때문에 브라우저 API인 getEventListeners()를 통해 확인해보면 root node에 모든 이벤트 핸들러가 붙어 있는 것을 알 수 있습니다.

이 내용을 통해서 ErrorBoundary가 이벤트 핸들러에서 발생한 에러를 잡을 수 없는지 이해할 수 있습니다.
정리하면,
리액트의 이벤트 핸들링은 최상위 node인 root에서 이뤄집니다. 각 이벤트들은 컴포넌트에 선언되어있지만, 렌더링 과정에서 root node에 모입니다.
따라서 특정 이벤트 핸들러에서 에러가 발생했을 때, 그것의 실행 컨텍스트는 ErrorBoundary 내부에 있지 않습니다. 따라서 ErrorBoundary가 이벤트 핸들러에서 발생한 에러를 포착할 수 없습니다.
비동기적 코드
비동기적 코드도 ErrorBoundary가 포착할 수 없습니다.
export default function App() {
return (
<ErrorBoundary>
<Children />
</ErrorBoundary>
);
}
function Children() {
function throwError() {
throw new Error('Error');
}
return (
<button
onClick={() => {
setTimeout(throwError, 3000);
}}
>
error!
</button>
);
}
이 케이스 또한 실행 컨텍스트와 연관이 있습니다.
setTimeout의 callback은 3초 후에 실행 컨텍스트에 들어와서 실행됩니다. 이는 ErrorBoundary의 실행 컨텍스트가 끝난 시점입니다.
비동기의 대표 예시인 데이터 통신 예시를 들어보면,
export default function App() {
return (
<ErrorBoundary>
<Children />
</ErrorBoundary>
);
}
function Children() {
const [todos, setTodos] = useState([]);
useEffect(() => {
(async () => {
try {
const res = await fetch('https://jsonplaceholder.typicode.com/todos');
const data = await res.json();
setTodos(data);
} catch (e) {
console.error('Errors: ', error);
}
})();
}, []);
return <button>click</button>;
}
catch 문에서 error를 반환해주고 있지만 fetch하고 있는 data에 대한 에러가 잡히지 않습니다.
ErrorBoundary가 error를 포착하게 하려면 어떻게 해야할까요?
export default function App() {
return (
<ErrorBoundary>
<Children />
</ErrorBoundary>
);
}
function Children() {
const [todos, setTodos] = useState([]);
const [error, setError] = useState(null);
useEffect(() => {
(async () => {
try {
const res = await fetch('https://jsonplaceholder.typicode.com/todos');
const data = await res.json();
setTodos(data);
} catch (e) {
setError(e);
}
})();
}, []);
if (error) {
throw error;
}
return <button>click</button>;
}
ErrorBoundary와 같은 컨텍스트에서 error를 반환하면 됩니다.
즉, error 상태를 분리하고 실행 컨텍스트 내에서 동기적으로 전달하는 것이죠.
서버 사이드 렌더링
서버 사이드에서 발생한 에러는 ErrorBoundary가 포착할 수 없습니다.
class ErrorBoundary extends React.Component {
state: {hasError: boolean} = {hasError: false};
static getDerivedStateFromError(error: any): {hasError: boolean} {
return {hasError: true};
}
render(): any {
const {hasError} = this.state;
if (hasError) {
return (
<div
style={{
color: 'red',
border: '1px solid red',
borderRadius: '0.25rem',
margin: '0.5rem',
padding: '0.5rem',
}}>
An error was thrown.
</div>
);
}
const {children} = this.props;
return (
<div
style={{
border: '1px solid gray',
borderRadius: '0.25rem',
margin: '0.5rem',
padding: '0.5rem',
}}>
{children}
</div>
);
}
}
ErrorBoundary는 getDerivedStateFromError() 기반으로 hasError의 변경을 통해 동작합니다. hasError는 클라이언트 사이드에서만 실행되는 state입니다.
getDerivedStateFromError()는 서버사이드가 동작하는 서버 환경이 아닌, 상태가 존재하는 브라우저에서 실행되므로 동작 주체가 다릅니다. 따라서 Errorboundary는 서버 사이드에서의 에러를 포착할 수 없습니다.
자식에서가 아닌 에러 경계 자체에서 발생하는 에러
자식에서가 아닌 에러 경계 차제에서 발생하는 에러는 ErrorBoundary가 포착하지 않습니다.
위의 3가지 예시는 실행 컨텍스트 범위를 벗어난 곳에서 생긴 에러라는 공통점이 있습니다. 이번에는 컴포넌트 범위를 벗어난 경우입니다.
ErrorBoundary는 자신 하위에서 발생하는 Error를 감지합니다. 만약 ErrorBoundary에서 error가 다시 던져지면, 에러를 상위 컴포넌트로 던지게 되고, 해당 에러는 또 다른 상위 ErrorBoundary를 필요로 하게 됩니다.
마무리
React 공식문서에 서술되어있는 ErrorBoundary가 포착할 수 없는 에러들에 대해 알아보았습니다. 이 경우들이 처음에는 다소 생소하다고 생각했지만, JS 핵심 개념인 실행 컨텍스트, React의 이벤트 핸들러 방식, 서버사이드 렌더링의 개념을 알고 있다면 전혀 어렵지 않은 경우라고 생각합니다.
에러 처리는 안정적인 서비스 운영을 위해 정말 중요한 과제라고 생각합니다. 앞으로도 더 나은 에러 관리 방식을 고민하고, 실제 프로젝트에서 적용 가능한 전략을 모색하여 실력을 키워나가고자 합니다.
ErrorBoundary의 동작을 위주로 기술하며 축약한 개념들은 자세히 공부하고 별도 포스팅을 해보려 합니다.
'Tech > SW Development' 카테고리의 다른 글
| Webpack, Esbuild, Rollup 빌드 속도 측정 & 비교 (0) | 2025.04.15 |
|---|---|
| 번들러에 대해 알아보기 (1) | 2025.04.15 |
| React-router-dom 뜯어보기 (2) | 2024.01.10 |
| 디자인 패턴 도입기 (0) | 2023.12.12 |
| Next13에서 Sass 사용하기 (0) | 2023.12.11 |