에러 바운더리는 안쪽 컴포넌트가 렌더링 중에 던진 오류를 잡아서, 앱 전체가 멈추는 대신 대체 UI를 보여 주는 React 컴포넌트입니다. 이를 위한 훅이 없기 때문에 static getDerivedStateFromError를 정의한 클래스 컴포넌트여야 합니다. 한 번 작성해 두면 트리의 어느 부분이든 감쌀 수 있습니다.
"Break the profile"을 눌러 자식이 렌더링 중에 오류를 던지게 해 보세요.
프로필이 대체 UI로 바뀌고, 페이지의 나머지는 계속 동작합니다. 콘솔에는 오류가 두 번 표시됩니다. React가 잡힌 모든 오류를 console.error로 기록하고, componentDidCatch가 한 번 더 기록하기 때문입니다. "Try again"은 원인(broken)과 바운더리의 오류를 지우므로 프로필이 다시 렌더링됩니다. reset에서 this.props.onReset();을 지우면, 원인을 고친 것이 없으므로 Try again이 곧바로 다시 대체 UI로 돌아갑니다.
클래스가 동작하는 방식
두 메서드가 클래스를 에러 바운더리로 만듭니다.
static getDerivedStateFromError(error)는 자식이 오류를 던진 직후 렌더링 중에 실행됩니다. 바운더리를 대체 UI로 전환하는 state 업데이트를 반환합니다. 순수해야 하며, 로깅도 요청도 하면 안 됩니다.componentDidCatch(error, info)는 대체 UI가 화면에 나온 뒤 실행됩니다. 오류를 보고 서비스로 보내는 것 같은 부수 효과에 사용하세요.info.componentStack은 오류를 던진 컴포넌트와 그 위의 컴포넌트들을 나열합니다.
class ErrorBoundary extends Component {
state = { error: null };
static getDerivedStateFromError(error) {
return { error }; // switch to the fallback
}
componentDidCatch(error, info) {
reportError(error, info.componentStack); // your logging service
}
render() {
if (this.state.error) return this.props.fallback;
return this.props.children;
}
}
대응하는 훅이 없으므로, 함수 컴포넌트로만 작성한 앱에도 이 클래스 하나는 필요합니다. 클래스 문법이 처음이라면 클래스 컴포넌트 페이지에서 설명합니다.
바운더리가 잡는 것과 잡지 못하는 것
바운더리는 React가 그 안의 컴포넌트를 처리하는 동안 일어난 오류를 잡습니다.
- 렌더링(함수 컴포넌트의 본문, 클래스의
render), - 자식 클래스 컴포넌트의 생명주기 메서드와 생성자,
- 이펙트(
useEffect,useLayoutEffect)와 ref 콜백, - use로 읽은 거부된 프로미스, 그리고 React 19에서는 폼 액션이나
useTransition의startTransition에 넘긴 함수가 던진 오류.
다음은 잡지 못합니다.
- 이벤트 핸들러의 오류,
- 직접 시작한 비동기 코드:
setTimeout,.then()콜백, 이벤트 핸들러 안의await, - 바운더리 자신의
render에서 난 오류(그 위의 바운더리가 잡습니다), - 서버 렌더링 중의 오류.
이벤트 핸들러가 빠지는 이유는 렌더링을 깨뜨리지 않기 때문입니다. 화면은 여전히 올바른 상태이므로 핸들러 안의 try/catch로 충분합니다. 그래도 바운더리를 보여 주고 싶다면 오류를 state에 저장하고 다음 렌더링에서 던지세요.
첫 번째 버튼은 버튼들을 화면에 그대로 두고 콘솔에 잡히지 않은 오류만 출력합니다. 두 번째 버튼은 거부를 잡아서 저장하고, 바운더리가 넘겨받습니다.
바운더리를 어디에 둘까
앱 전체를 감싼 바운더리 하나는 흰 화면을 막아 주지만, 그러면 작은 오류 하나가 모든 것을 대체합니다. 사이드바 위젯, 차트, 각 라우트처럼 독립적으로 실패할 수 있는 부분에 바운더리를 두세요. 그러면 실패해도 사용자는 페이지가 아니라 패널 하나만 잃습니다.
Stocks 패널만 대체 UI를 보여 줍니다. 두 위젯을 하나의 <ErrorBoundary>로 옮기면, 잘 동작하던 Weather 패널도 함께 사라집니다.
바운더리가 하나도 없으면, 렌더링 오류가 잡히지 않을 때 React는 트리 전체를 화면에서 제거합니다. 잘못된 데이터를 보여 주는 깨진 페이지보다는 빈 페이지가 안전하지만, 사용자는 그것을 충돌로 받아들입니다.
react-error-boundary 라이브러리
react-error-boundary 패키지는 클래스를 대신 감싸 주고 초기화 도우미를 더합니다. 이 에디터에서는 쓸 수 없으므로 정적 블록으로 보여 줍니다.
import { ErrorBoundary, useErrorBoundary } from 'react-error-boundary';
function Fallback({ error, resetErrorBoundary }) {
return (
<div role="alert">
<p>Something went wrong: {error.message}</p>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
);
}
<ErrorBoundary FallbackComponent={Fallback} onReset={() => refetch()} resetKeys={[userId]}>
<Profile userId={userId} />
</ErrorBoundary>;
// Inside a child: hand an async error to the boundary
const { showBoundary } = useErrorBoundary();
fetchData().catch(showBoundary);
resetKeys는 나열한 값 중 하나라도 바뀌면 바운더리를 초기화하고, showBoundary는 앞의 예제에서 본 "저장하고 던지기" 기법을 대신해 줍니다.
React 19에서 오류 보고하기
React 19는 오류를 새로운 방식으로 보고합니다. 바운더리가 잡은 오류는 console.error로 한 번 기록되고, 잡히지 않은 오류는 window.reportError로 갑니다. 직접 운영하는 서비스로 보내려면 createRoot에 옵션을 넘기세요. 각 옵션은 해당하는 기본 로깅을 대체합니다.
import { createRoot } from 'react-dom/client';
const root = createRoot(document.getElementById('root'), {
onCaughtError(error, errorInfo) {
// an error boundary caught it
logToService('caught', error, errorInfo.componentStack);
},
onUncaughtError(error, errorInfo) {
// no boundary caught it, and React removed the tree
logToService('uncaught', error, errorInfo.componentStack);
},
onRecoverableError(error, errorInfo) {
// React recovered by itself, for example after a hydration mismatch
logToService('recovered', error, errorInfo.componentStack);
},
});
root.render(<App />);
onRecoverableError는 하이드레이션 불일치나 React가 다시 시도했을 때 사라진 렌더링 오류처럼, React가 도움 없이 고친 오류를 위한 것입니다. hydrateRoot도 같은 세 옵션을 받습니다.
자주 하는 실수
오류를 던지는 컴포넌트 안에 바운더리 두기. 바운더리는 자식의 오류만 잡습니다. Profile이 자기 JSX를 <ErrorBoundary>로 감싸고 자기 본문에서 오류를 던지면, 그 오류는 바운더리를 지나쳐 그 위의 바운더리로 갑니다. 컴포넌트는 부모에서 감싸세요.
getDerivedStateFromError 안의 부수 효과. 렌더링 중에 실행되고 여러 번 실행될 수 있으므로 state 반환만 하세요. 로깅은 componentDidCatch나 루트의 onCaughtError에 속합니다.
아무것도 고치지 않는 초기화. 바운더리의 오류를 지우면 같은 자식을 같은 props로 렌더링합니다. 다시 오류를 던지면 사용자는 대체 UI로 돌아갑니다. 첫 번째 예제가 onReset으로 하듯이, 오류의 원인을 먼저 바꾸고(다시 가져오기, 잘못된 입력 지우기, key 바꾸기) 그다음 오류를 지우세요.
앱 전체에 바운더리 하나만 두기. 빈 페이지는 막지만, 모든 작은 실패를 전체 페이지 오류로 바꿉니다. 기능 단위로 바운더리를 추가하고, 최상위 바운더리는 최후의 수단으로 남겨 두세요.
개발자에게서 오류를 숨기는 대체 UI. 친절한 메시지는 사용자에게 맞지만, 오류가 여전히 로그에 도달하는지 확인하세요. React 19는 기본적으로 잡힌 오류를 콘솔에 기록하지만, 프로덕션에서 그 콘솔은 사용자의 것이므로 componentDidCatch나 onCaughtError에서 서비스로 오류를 보내세요.
에러 바운더리는 <Suspense>와 자연스럽게 짝을 이룹니다. Suspense는 "아직 준비되지 않음"을, 바운더리는 "실패함"을 처리합니다. use 예제들처럼 바운더리를 Suspense 바깥에 두면, 컴포넌트 하나가 로딩, 성공, 실패를 모두 다룹니다.
자주 묻는 질문
React에서 에러 바운더리란 무엇인가요?
안쪽의 어떤 컴포넌트든 렌더링 중에 던진 자바스크립트 오류를 잡아서 대신 대체 UI를 렌더링하는 컴포넌트입니다. 에러 바운더리가 없으면 렌더링 중 오류 하나가 앱 전체를 화면에서 없앱니다.
에러 바운더리를 위한 훅이 있나요?
아니요. 에러 바운더리는 static getDerivedStateFromError(와 선택적으로 componentDidCatch)를 가진 클래스 컴포넌트여야 합니다. 한 번 작성해 두면 함수 컴포넌트에서 다른 컴포넌트처럼 쓸 수 있고, 클래스를 대신 감싸 주는 react-error-boundary를 설치해도 됩니다.
에러 바운더리는 이벤트 핸들러의 오류도 잡나요?
아니요. 이벤트 핸들러는 렌더링 바깥에서 실행되므로 React가 회복하는 데 바운더리가 필요 없습니다. 핸들러에서 try/catch를 쓰고, 바운더리를 보여 주고 싶다면 오류를 state에 저장한 뒤 렌더링 중에 던지세요.
에러 바운더리는 비동기 오류도 잡나요?
직접 시작한 setTimeout, 프로미스 콜백, fetch().then()의 오류는 잡지 않습니다. use로 읽은 거부된 프로미스, 그리고 React 19에서는 폼 액션이나 useTransition의 startTransition에 넘긴 함수 안에서 던진 오류는 잡습니다.
에러 바운더리는 어떻게 초기화하나요?
오류의 원인을 고친 뒤, 보통 대체 UI의 Try again 버튼에서 바운더리의 오류 state를 지우세요. 바운더리의 key를 바꿔도 React가 새 인스턴스를 마운트하므로 초기화됩니다.