React 컴포넌트 렌더링 과정에서 발생한 오류를 잡아내고 처리한다. Javascript 에러가 나머지 애플리케이션을 중단시키는 것을 방지하고, 컴포넌트를 불러오지 못하게 되는 불상사에 대비하여 예비 UI (fallback UI라고 부름)를 표시하는 데에 사용되는 컴포넌트이다.
Javascript 예를 들어보자면
{ key1, key2 } = object
console.log(key1)
이런 코드가 있다고 가정해보자.
이 때 object가 undefined일 경우 undefined 값을 구조분해할당 하려고 했으니 type error가 발생한다.
이런 javascript 오류 때문에 컴포넌트 렌더링 자체가 안되어 버리면 유저가 흰 화면만 보게 되고.. 당황한 마음을 감추지 못한 채 앱을 이탈해버릴 것이다.
이 때 Error-boundary를 이용하면 차선책으로 화면에
"🌈Something went wrong🌸"
이런 거라도 띄워줄 수 있겠다. 물론 실제 서비스에서는 예비 UI 컴포넌트를 제작하고 띄워줘야 할 것이다.
아 물론 당연히 Error-boundary를 사용할 수 있다고 Javascript error를 대충 잡아내는 결례를 범하면 안된다.
개발 단계에서 개발자가 잡아낼 수 있는 모든 에러 + 화면 버그는 모조리 싹 다 잡아내고, 마지막에 "아 그래도 불안해. 운영 환경에 너란 녀석.. 정말 던질 수 있을까?"라는 마음과 함께, 예상치 못한 문제가 발생할 것에 대비하여 사용하는 게 좋다. 실제 환경에서는 늘 화면에 예상치 못한 문제가 발생할 수 있다. 사용자가 예상치 못한 방식으로 UI와 상호작용할 수도 있고, 외부 API에서 예상치 못한 데이터 형식이 반환될 수도 있기 때문이다. 이 모든 것은 "방어적 프로그래밍"의 일환으로 볼 수 있다. 가능한 한 모든 오류 상황에 대비하는 것이다. 안정적이고 예측가능하며, 사용자 친화적인 어플리케이션을 만드는 데에 도움이 될 것이다.
하지만 모든 에러를 Error-boundary 하나로 싹 다 잡을 수 있는 것은 아니다.
Error-boundary가 포착하지 못하는 오류
- 이벤트 핸들러 ex. onClick 함수
- 비동기 코드 ex. setTimeout, AJAX
- SSR (서버 사이드 렌더링)
- 에러 경계의 자식 컴포넌트들에서 발생하는 오류가 아니라, 에러 경계 자체에서 발생하는 오류
예를 들어서 버튼에 연결하는 onClick 함수나 axios로 HTTP 통신을 담당하는 코드가 있다면 error 처리는 Error-boundary에 의존하는 것이 아니라 try-catch문을 작성하여 직접 처리하는 것이 좋다.
서버 사이드 렌더링이라면 서버 측에서 에러 처리를 구현해야 한다.
즉, Error-boundary는 컴포넌트 렌더링 과정에서 발생할 가능성이 있는 오류들에 대한 차선책 정도로 생각하는 것이 좋은 듯 하다.
예를 들어, 내가 이번에 프로젝트를 하다가 마주쳤던 오류!
Uncaught Error: Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.
이런게 있었다. 카카오 로그인 버튼이 화면에 렌더링되지 않았었는데, 만약 당시에 Error-boundary를 사용했다면 에러 대신 fallback UI가 화면에 보였을 것이다.
방어적 프로그래밍을 위해서는 결국 Error-boundary와 try-catch문 등 에러 처리 전략들을 상호보완적으로 사용해야 하는 것이다.
npm i react-error-boundary
라이브러리 설치 후 common폴더 -components폴더 내부에 fallback UI 컴포넌트를 생성했다.
import { FallbackProps } from 'react-error-boundary';
function Fallback({ error, resetErrorBoundary }: FallbackProps) {
return (
<div role="alert">
<p>Something went wrong:</p>
<pre>{error.message}</pre>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
);
}
export default Fallback;
다음으로 App.tsx에 적용하여 전체 앱을 감싸는 방식을 사용했다.
물론 앱 전체가 아니라 앱 내 독립적인 부분들에 선택적으로 적용할 수도 있다.
하지만 Error-boundary를 다소 늦게 알게된 만큼 어느 부분에 적용하고 어느 부분에 적용하지 않을지를 판별하는 작업도 시간이 너무 오래 걸릴 것 같다는 생각이 들었다. 그래서 일단은 앱 전체에 적용해보는 경험에 의의를 두는 걸로!
// ...import 생략
import { ErrorBoundary } from 'react-error-boundary';
import Fallback from './common/components/Fallback';
function App() {
const queryClient = new QueryClient();
return (
// ErrorBoundary를 앱 전체에 적용
<ErrorBoundary FallbackComponent={Fallback}>
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<BrowserRouter>
<Header />
<Routes>
.
.
.
(Route 중략)
.
.
.
</Routes>
</BrowserRouter>
</Provider>
</QueryClientProvider>
</ErrorBoundary>
);
}
export default App;
게시판 만들기 프로젝트를 하면서 throwOnError 옵션에 대해 알게되었다. 원래 비동기 에러는 에러 바운더리가 포착할 수 없지만,
throwOnError 옵션을 사용하면 비동기 에러 발생 시 가장 가까운 에러 바운더리로 전파된다. 하지만 반드시 알아둬야 할점은 try catch와 같은 별도의 에러 처리를 소홀히 하면 안된다는 점이다. 데이터 요청과 같은 비동기 작업은 에러바운더리를 두더라도 항상 별도의 에러 처리 로직이 필요하다.
const { data } = useQuery('data', fetchData, {
throwOnError: true, // 비동기 에러를 throw하여 가장 가까운 ErrorBoundary로 전달
});
참고 자료:
https://www.npmjs.com/package/react-error-boundary
https://velog.io/@bbaa3218/React-%EC%97%90%EB%9F%AC-%EB%B0%94%EC%9A%B4%EB%8D%94%EB%A6%ACError-Boundary