ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Portfolly] ErrorBoundary로 컴포넌트 단위 에러 화면 띄우기
    Studying/Proj 과정 2024. 1. 11. 21:14

    문제 상황

    기존 Portfolly 홈페이지는 클라이언트 화면에서의 에러 처리가 상황 별로 이루어지지 않았습니다.

     

    멘토링 시간 때 멘토님께서 현업에서는 ErrorBoudary 형식으로 에러 처리를 많이 한다고 알려주셔서,

    저도 그 방법을 공부한 뒤 리팩터링에 적용해보기로 했습니다.

     

    결론적으로 다음과 같은 컴포넌트 단위의 api 에러 화면을 만들 예정입니다.

     

     

    PortfolioList.tsx를 불러오지 못 했을 때

     

     

     

    구현 과정

    Error Boundary에 대해 처음 접근하게 된 건 아래 카카오 기술 블로그를 통해서였습니다.

     

    React의 Error Boundary를 이용하여 효과적으로 에러 처리하기 | 카카오엔터테인먼트 FE 기술블로그

    정호일(harry) 카카오페이지에서 웹 프론트엔드를 개발하고 있습니다. 집보다 밖에 돌아다니는 걸 좋아합니다.

    fe-developers.kakaoent.com

     

    Error Boundary란 다음과 같이 에러를 발생시킬 수 있는 컴포넌트를 감싼 뒤, throw한 에러를 받아 처리하는 방법입니다.

    <ErrorBoundary>
    	<Compoents />
    </ErrorBoundary>

     

    서버 점검 등 홈페이지 전체적인 접근을 막는 건 전역을 감싸는 GlobalErrorBoundary가 처리하고,

     

    api 통신 에러는 비동기 데이터를 요청하는 컴포넌트를 감싼  ApiErrorBoundary에서 처리합니다. 그러면 컴포넌트 단위로 에러 화면을 띄울 수 있습니다.

     

    사용자는 특정 데이터를 불러오지 못하더라도 해당 데이터 외 다른 부분들을 확인할 수 있고,

     

    불러오지 못한 데이터에 대해서만 재요청 할 수 있다는 점에서 훨씬 사용자 친화적인 UI를 제공하게 됩니다.

     

     

    위 기술 블로그에서는 클래스형 컴포넌트로 Error boundary를 구현했으나,

     

    저는 react-error-boundary 라이브러리를 사용해 일관된 함수형 컴포넌트를 유지했습니다.

     

    이는 클래스형 Error boundary와 달리 props로 에러 발생 시 보여주는 fallbackComponent를 지정할 수 있어, 별도의 추가적인 코드 작성 없이 props만 변경하며 다양한 에러 처리를 할 수 있습니다.

     

     

    다음과 같이 App.tsx 가장 바깥에 GlobalErrorBoundary를 감싸줍니다.

    import {
    	ErrorBoundary as GlobalErrorBoundary,
    } from "react-error-boundary";
    
    import { GlobalErrorFallback } from '@/components';
    
    export default function App() {
    	const { reset } = useQueryErrorResetBoundary();
    
      return (
    		<GlobalErrorBoundary
    			FallbackComponent={GlobalErrorFallback}
    			onReset={reset}
    		>
                <BrowserRouter>
                    <Routes>
                   		...
                    </Routes>
                </BrowserRouter>
    		</GlobalErrorBoundary>
      )
    }

     

     

    반면 ApiErrorFallback은 비동기 데이터를 불러오는 컴포넌트를 감쌉니다.

    import { useQueryErrorResetBoundary } from "@tanstack/react-query";
    import { ErrorBoundary as ApiErrorBoundary } from "react-error-boundary";
    
    import { ApiErrorFallback } from "@/components";
    
    export default function MainPage(){
    	...
    	const { reset } = useQueryErrorResetBoundary();
    
    	useDispatchSectionParameter();
    
    	return(
    		<S.Wrapper>
    			...
    			<S.PortfolioSection>
    				<ApiErrorBoundary FallbackComponent={ApiErrorFallback} onReset={reset}>
    					<Suspense fallback={<PortfolioListSkeleton type='portfolio-card' />}>
    						<PortfolioCardList filter={filter} />
    					</Suspense>
    				</ApiErrorBoundary>
    			</S.PortfolioSection>
    		</S.Wrapper>
    	)
    }

     

    여기서 useQueryErrorResetBoundary가 눈에 띄는데,

     

    이는 react-query가 제공하는 훅으로, 가장 가까운 컴포넌트에서 발생한 error를 reset하는 함수를 제공합니다.

     

    이를 활용해 데이터를 다시 재요청하는 버튼을 만들 수 있습니다.

     

    reset 메서드를 onResect이라는 매개변수로 넘겨주면 다음과 같이 fallbackComponent에서 resetErrorBoundary를 받아 호출했을 때 가장 가까운 컴포넌트(현재는 에러가 발생한 PortfolioCardList)의 비동기 데이터를 재요청할 수 있습니다.

    import { useNavigate } from 'react-router-dom';
    
    import type { FallbackProps } from 'react-error-boundary';
    
    import { getErrorMessage } from '@/utils';
    
    import { Error } from '@/components';
    
    export default function ApiErrorFallback({error, resetErrorBoundary}: FallbackProps) {
    	const navigate = useNavigate();
    
    	const { status } = error.response;
    	const message = getErrorMessage(status);
    
    	if(status === 401 || status === 402) {
    		const errorHandler = () => {
    			navigate(`/login`);
    		};
    		return (
    			<Error
    				type='component'
    				reset={errorHandler}
    				message={message}
    			/>
    		)
    	}
    
    	return(
    		<Error
    			type='component'
    			reset={resetErrorBoundary}
    			message={message}
    		/>
    	)
    }

     

     

    상태 코드에 따라 다음과 같이 다른 에러 메세지가 나타나도록 했습니다.

    export const getErrorMessage = (status: number) => {
    	switch (status) {
    		case 401:
    		case 403:
    			return {
    				title: '접근 권한이 없습니다.',
    				content: '로그인이 필요한 서비스입니다.',
    			};
    		case 404:
    			return {
    				title: '잠시 후 다시 시도해주세요.',
    				content: '요청사항을 처리하는데 실패했습니다.',
    			};
    		case 503:
    			return {
    				title: '서버 점검 중입니다.',
    				content: '이용에 불편을 드려 죄송합니다.',
    			};
    		case 409:
    		case 500:
    		default:
    			return {
    				title: '서비스에 접속할 수 없습니다.',
    				content: '새로고침 하거나 잠시 후 다시 접속해 주시기 바랍니다.',
    			};
    	}
    };

     

     

     

    그리고 결과는 다음과 같습니다!

     

    PortfolioList.tsx를 불러오지 못 했을 때

    비동기 데이터 PortfolioList를 불러오지 못 해도 사용자는 해당 부분만 에러가 났음을 인지하고, 데이터를 재요청 할 수 있습니다.

     

     

     

     

    두 번째 문제점

    그런데 아직 해결하지 못 한 두 번째 문제가 있었습니다.

     

    PortfolioList의 경우 PortfolioList.tsx 컴포넌트에서 비동기 데이터를 불러오지만,

    페이지 컴포넌트에서 비동기 데이터를 불러올 경우 결국 페이지 전체를 확인하지 못합니다.

     

    대표적으로 포트폴리오 상세 보기 페이지(PortfolioDetailPage)의 경우, 

     

    포트폴리오 게시물 내용과 사용자 프로필 등 다양한 정보를 페이지 컴포넌트에서 한 번에 불러옵니다.

     

    포트폴리오 데이터를 가져오지 못하면 페이지 전체를 보여줄 수가 없는 겁니다.

     

    따라서 비동기 데이터를 호출하는 페이지 컴포넌트를 위한 별도의 에러 처리가 필요했습니다.

     

     

    Error Boundary 대신 직접 Alert 띄우기

    제가 생각한 방법은 이렇습니다.

     

    비동기 데이터를 가져오는 페이지 컴포넌트의 경우, ApiErrorBoundary로 감싸지 말고 직접 에러를 감지해 에러 alert를 띄우는 겁니다.

     

    alert은 전역 컴포넌트로 등록되어 있으며, 전역 상태 alert에 경고 내용이 담긴 객체가 저장되면 alert 모달을 띄웁니다.

     

    저는 usePageErrorAlert 라는 재사용 커스텀 훅을 만들었습니다.

    (지금 다시 생각해보니 굳이 에러 Alert가 아니어도 사용하게 useAlert 정도로 리네이밍 하는 게 좋겠습니다)

    import { useEffect } from "react";
    import { useDispatch } from "react-redux";
    import { useNavigate } from "react-router-dom";
    
    import { setAlert } from "@/redux";
    
    export default function usePageErrorAlert(isError: boolean) {
    	const dispatch = useDispatch();
    	const navigate = useNavigate();
    
    	useEffect(() => {
    		if(!isError) return;
    		dispatch(setAlert({
    			type: 'error',
    			onConfirm: () => navigate(-1),
    		}));
    	}, [isError]);
    }

     

     

    이 커스텀 훅을 PortfolioDetailPage.tsx에서 다음과 같이 호출하기만 하면 됩니다.

    const { data: portfolio, isError } = usePortfolioDetailQuery(portfolioId);
    usePageErrorAlert(isError);

     

     

    전역 컴포넌트 Alert 모달에 대해선 다른 게시물에서 추가 설명하겠습니다 :)

     

     

     

    개선 결과

    결과적으로 다음과 같은 사용자 친화적 에러 화면을 구현했습니다!

     

    대표적으로 메세지 페이지에 들어갔을 때, 필요한 데이터를 아무것도 불러오지 못 해도 사용자가 대강 페이지 형태를 확인한 뒤, 무엇이 문제인지 인지하고 페이지에서 벗어날 수 있습니다.

     

     

    PortfolioDetailPage는 다음과 같습니다.

     

     

Designed by Tistory.