기본적으로 React에서 에러 핸들링, 로딩 핸들링 시 상태를 관리하고 상태에 따라 UI를 렌더링하도록 로직을 작성하게된다.
이에 관련된 상태들이 많아질수록 정상적인 코드를 이해하는 것도 힘든데, 에러 로딩이 복잡하게 섞여 코드의 유지보수성이 저하될 것이다.
이러한 문제를 React에서의 Error boundary와 Suspense를 이용해 개선할 수 있다. 이 컴포넌트들을 통해 달성할 수 있는 건 에러와 로딩 처리를 조상 컴포넌트에게 위임하는 것이다. 선언적인 구현도 함께 달성된다.
이 중에서도 Error Boundary를 학습해보자.
크게 2단계 작업을 통해 화면에 Error를 그려낼 준비를 할 수 있다.
1. ErrorBoundary 컴포넌트 정의하기
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false }; // 에러 상태 지정
}
static getDerivedStateFromError(error) {
// 에러 발생 시, 상태를 변경하여 이에 따른 fallback UI를 렌더링한다
return { hasError: true };
}
componentDidCatch(error, info) {
// optinal한 구현사항이다, 에러 발생 시 원하는 작업을 작성하면된다
// 에러 로그 정보 출력 예시
// Example "componentStack":
// in ComponentThatThrows (created by App)
// in ErrorBoundary (created by App)
// in div (created by App)
// in App
logErrorToMyService(error, info.componentStack);
}
render() {
if (this.state.hasError) {
// You can render any custom fallback UI
return this.props.fallback;
}
return this.props.children;
}
}
2. ErrorBoundary로 에러 핸들링을 원하는 컴포넌트를 감싸기
ErrorBoundary의 fallback속성은 위에서 정의한 속성으로, 에러 발생시 보여줄 컴포넌트를 전달한다.
MyPage는 임의의 컴포넌트로, 선언한 ErrorBoundary가 화면에 렌더링되는 모습을 보기위해서는 에러를 throw하는 컴포넌트
도 준비되야한다.
<ErrorBoundary fallback={<p>somthing wrong</p>}>
<MyPage />
</ErrorBoundary />
getDerivedStateFromError(error)
componentDidCatch(error, info)
ErrorBoundary의 후손 컴포넌트가 에러를 던지면 그것을 잡아 getDerivedStateFromError
가 실행된다. 그리고 render함수가 실행되는데, getDerivedStateFromError
반환된 상태에 따라 fallback UI를 렌더링한다.
음... 그냥 새로고침하면 안되나요?
데이터를 다시 불러오려면 사용자가 새로고침 버튼을 누르거나 key 입력으로 발생시켜야한다. 또한 이때 앱이 다시 다운로드 받아와지기 때문에 비용이 비교적 더 발생하는 단점을 가진다. 추가적인 요구사항이 필요하더라도 UX 향상할 수 있다면 시도해볼만한 기능이라고 생각한다. 뿐만아니라 구현도 간단하다! 아래는 직접 실험하고 테스트를 진행한 코드다. 상태에 refresh
가 추가된 것 외에 큰 차이가 없어서, 설명은 더 하지 않겠다.// typescript로 작성한 ErrorBoundary 활용 예시
import React, { ErrorInfo, ReactNode } from 'react';
import { MdRefresh } from 'react-icons/md';
interface IProps {
fallback: ReactNode;
children?: ReactNode;
}
interface IState {
refresh: boolean;
hasError: boolean;
}
export default class ErrorBoundary extends React.Component<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {
refresh: false,
hasError: false
}; // 에러 상태 지정
}
static getDerivedStateFromError(error: unknown) {
// 에러 발생 시, 상태를 변경하여 이에 따른 fallback UI를 렌더링한다
// error는 컴포넌트에서 던진 에러
return { hasError: true, refresh: false };
}
render() {
const { fallback, children } = this.props;
if (this.state.refresh) {
return children;
}
if (this.state.hasError) {
return (
<div>
{fallback}
다시 시도할까요?
<div onClick={() => this.setState({ hasError: false, refresh: true })}>
<MdRefresh />
</div>
</div>
);
}
return children;
}
}
서비스가 더 커질수록 관리해야하는 상태는 더 많아지게되며, 에러핸들링의 복잡도가 높아질텐데 이에따라 에러 핸들링 관심사 분리, 선언적 사용을 통해 코드 품질을 향상하는 것이 복잡도를 줄이는데 도움이 될 것 같다고 생각했다. 또한 try-catch와 처리되는 꼴이 비슷하기 때문에 이를 이해하고있다면 동작하는 방식을 쉽게 이해할 수 있을 것이다!
Catching rendering errors with an error boundary - react
React의 Error Boundary를 이용하여 효과적으로 에러 처리하기 - kakaoent FE 기술 블로그