프로젝트에서 Suspense와 Error Boundary를 이용한 선언적 Error, Loading을 처리한 부분과
React.lazy를 사용해 초기 페이지 로드시간을 감소시킨 글을 작성하려고 한다.
이번글에서는 Errorboundary에 대해 중점적으로 다루고자 한다.
선언형 프로그래밍이란, 프로그램이 어떤 방법으로 해야 하는지를 나타내기보다 무엇과 같은지를 설명하는 경우에 맞춰 코드를 작성하는 방법이다. (위키백과 출처)
React는 기본적으로 선언형 프로그래밍 방식을 사용한다. 즉 컴포넌트별 코드의 재사용, UI를 화면에 그리는(render)로직을 분리함으로써, 코드가 간결해지고 한눈에 컴포넌트 구성을 알아볼 수 있다.
const FoodDetail = () => {
const scrollRef = useRef<HTMLElement>(null);
const shopId = Number(useParams().id);
const { isCommentLoading, isCommentError, commentState } = useCommentQuery(shopId);
const { isMenuLoading, isMenuError, menuState } = useMenuQuery(shopId);
const { isShopLoading, isShopError, shopState } = useShopQuery(shopId);
if (isCommentLoading || isMenuLoading || isShopLoading) {
return <S.CommentContainer>로딩중</S.CommentContainer>;
}
if (isCommentError || isMenuError || isShopError) {
return <S.CommentContainer>Error 발생</S.CommentContainer>;
}
return ...
이는 몇가지 단점이 존재했는데, 명령형으로 loading과 Error 상태를 처리하게 되면 컴포넌트별로 각각 모든 loading과 error처리를 해줘야했다.
또한 이는 React가 추구하는 선언형 프로그래밍에도 적합하지 않은 방식이다.
Loading과 Error 상태를 선언적으로 처리하는데 React에서는 Suspense와 Error Boundary라는 기능을 제공한다.
React 공식문서에 따르면, Error Boundary는 하위 컴포넌트 트리의 어디에서든 자바스크립트 에러를 기록하며 깨진 컴포넌트 트리 대신 폴백 UI를 보여주는 React 컴포넌트이다.
즉, Error Boundary로 감싼 하위 컴포넌트에서 try.. catch 처럼 error를 감지할 수 있으며, 만약 에러가 발생한다면 깨진 컴포넌트 대신
Error Boundary에서 fallback으로 넘겨준 UI를 보여주는 render 하는 방식이다.
여기서 ErrorBoundary 컴포넌트는 state에 error가 발생했는지를 식별 할 수 있는 값을 필요로 한다.
생명주기 메소드인 getDerivedStateFromError나 componentDidCatch 중 하나를 정의하면 클래스형 컴포넌트를 Error를 catch할 수 있는 Error Boundary로 사용 할 수 있다.
DerivedStateFromError가 Error를 catch하면, 컴포넌트 내 state의 hasError 프로퍼티가 true로 변한다.
Error가 존재한다면 fallback UI를, 아니라면 child component를 렌더링한다.
import React, { Component, ErrorInfo, ReactNode } from 'react';
import { FlexContainer } from '../styles/GlobalStyle';
interface Props {
children?: ReactNode;
}
interface State {
hasError: boolean;
}
class ErrorBoundary extends Component<Props, State> {
public state: State = {
hasError: false,
};
public static getDerivedStateFromError(_: Error): State {
return { hasError: true };
}
public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('Uncaught error:', error, errorInfo);
}
public render() {
if (this.state.hasError) {
return <FlexContainer>ErrorBoundary로 에러처리하기</FlexContainer>;
}
return this.props.children;
}
}
export default ErrorBoundary;
ErrorBoundary는 Suspense와 함께 선언적으로 컴포넌트 상태를 처리 할 수 있다. 컴포넌트별 loading과 Error 처리 로직을 한곳에 모을 수 있기 때문이다.
react Query를 사용할 때, Suspense, ErrorBoundary로 비동기처리 상태 처리에 대한 작업을 하려면 쿼리 옵션을 변경해주어야 한다.
//Router.tsx
const Router = () => {
return (
<BrowserRouter>
<ErrorBoundary>
<Suspense fallback={<Spinner />}>
<Routes>
<Route path="/" element={<MainPage />} />
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
<Route path="/foodList" element={<FoodList />} />
<Route path="/foodList/:id" element={<FoodDetail />} />
<Route path="/admin" element={<Admin />} />
<Route path="/mypage" element={<MyPage />} />
<Route path="*" element={<NotFound />} />
</Routes>
</Suspense>
</ErrorBoundary>
</BrowserRouter>
);
};
export default Router;
export const useCommentQuery = (shopId: number) => {
const {
isLoading: isCommentLoading,
isError: isCommentError,
data: commentState,
isSuccess,
} = useQuery<Tcomment[], AxiosError>(['comment', shopId], () => fetchComments(shopId), {
suspense: true,
useErrorBoundary: true,
});
return { isCommentLoading, isCommentError, commentState, isSuccess };
};
Error Boundary는 이벤트 핸들러 내에서 발생한 Error는 포착하지 못한다. 따라서 이벤트 핸들러 내에서는 try.. catch문을 통해 error를 따로 handling 해줘야 한다.
Error boundary 일시적인 api호출 관련 에러를 처리할때나 url의 path variable이나 query parameter를 찾을 수 없는 에러를 공통적으로 처리할 때 유용한 것 같다.
아직 에러 상태에 따른 상황별 에러처리나, Error가 발생했을때 reset과정을 거쳐서 api를 재호출하는 것 등 에러상태에 따른 에러핸들링은 구현하지 못했다.
ref) 공식문서 : https://ko.reactjs.org/docs/error-boundaries.html