ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [React] ErrorBuondary의 사용방법
    프로그래밍/React 2024. 5. 21. 23:23
    개요

    1. Error Boundary란?

     

     

    ✅ Error Boundary란?


     

    📦 Error Boundary

    React는 어플리케이션 렌더링 과정에서 에러가 발생하면 화면에서 해당 UI를 제거하게 되어 전체 앱이 중단되는 현상이 발생하게 된다. 이러한 현상을 막기위해 새롭게 React 16에서 도입된 기능이 Errorbuondary이다. Errorboundary는 하위요소의 렌더링 중에 throw된 error를 catch하도록 동작하며 오류가 발생한 요소 대신 대체 UI를 표시하게끔 해준다. 하지만 포착하지 못하는 오류도 있어 기본적으로 널리 사용되지는 않는다.
    • 포착하지 않는 오류: 이벤트 핸들러, 비동기 코드, 서버 측 렌더링, Errorboundary 자체에서 발생하는 오류(자식요소 X)
    • Life cycle을 사용하는 class 컴포넌트 기반으로 함수 컴포넌트의 통일성을 깨트리는 요소가 된다.

    🎉 Error Boundary 코드

    더보기
    class ErrorBoundary extends React.Component {
      constructor(props) {
        super(props);
        this.state = { hasError: false };
      }
    
      static getDerivedStateFromError(error) {
        return { hasError: true };
      }
    
      render() {
        if (this.state.hasError) {
          // 폴백 UI를 커스텀하여 렌더링할 수 있습니다.
          return <h1>Something went wrong.</h1>;
        }
    
        return this.props.children;
      }
    }

     

    📦 왜 필요한가?

    1. API 호출과 같은 비동기 동작들을 쉽게 제어하고 유지보수하기 위해 Fetcher를 사용하듯 선언적으로 에러를 처리할 수 있다.
    2. 공통으로 처리할 수 있는 에러를 한 곳에서 처리할 수 있다.
    3. 에러로 인해 사용자에게 빈 화면이 제공될 수 있는 경우를 방지할 수 있다.
    4. retry에 대한 처리도 할 수 있다.

     

    레퍼런스 이미지 차용

    위 이미지에서 볼 수 있듯이 특정 UI의 에러가 발생했을 경우에 대체 이미지를 띄우고 retry에 대한 처리 또한 Error Boundary에서 처리를 할 수 있다.

     

     

    📦 개선할 점

    우리가 주로 많이 처리하고자 하는 에러에 대한 부분은 비동기 코드, 이벤트 핸들러에서 발생한 에러에 대한 경우가 더 많을 것이다. 하지만 Errorboundary는 기본적으로 이러한 오류를 포착하지 못한다. 그렇기에 이러한 부분을 해결하여 Error Boundary의 활용은 더 나은 DX에 도움을 줄 수 있을 것이라고 생각한다.

     

     

    ✅ 개선 된 Error Boundary


     

    📦 레벨 단계로 나누어서 사용

    Errorboundary의 각 역할을 정해서 처리하면 좀 더 쉽게 에러 처리를 할 수 있다. 특히 네트워크 에러와 같이 페이지 전체에 에러를 보여줘야하는 경우에는 Global(root)에 위치시켜 에러 처리를 하고 API에러의 경우 하위 컴포넌트에서 감싸 각 컴포넌트 별 에러에 대한 처리를 한다.  
    • GlobalErrorBoundary: 페이지 전체 에러를 보여줘야 하는 경우
    • ApiErrorBoundary: 각 API의 에러로 하위 컴포넌트에 필요에 따라서 사용하는 경우
    <GlobalErrorBoundary>
      <ApiErrorBoundary>
        <ContentProductListFetcher>
          <ContentProductListContainer />
        </ContentProductListFetcher>
      </ApiErrorBoundary>
    </GlobalErrorBoundary>

    위와 같은 구조로 GlobalError와 ApiError에 대해서 처리를 할 수 있다.

     

    🎉 GlobalErrorBoundary

    더보기
    class GlobalErrorBoundary {
    	render() {
    		if (!this.state.shouldHandleError) {
    		  return this.props.children;
    		}
    		if(네트워크 에러) {
    			return (
    				<NetworkError onClickRetry={() => this.setState({ shouldHandleError: false})}/>
    			)
    		}
    		if(서버 점검 에러) {
    			return (
    				// 서버 점검 에러를 보여준다
    				<Maintenance />
    			)
    		}
    		return (
    			<UnknownError onClickRetry={() => this.setState({ shouldHandleError: false})} />
    		)
    	}
    }

    🎉 ApiErrorBoundary

    더보기
    class ApiErrorBoundary {
    	// 하위 트리에서 throw된 error를 받습니다.
    	// 이 에러는 카카오페이지 웹에서 사용되는 에러 코드 정보를 담고 있습니다.
    	static getDerivedStateFromError(error: Error): State {
    	  if (이 Error Boundary에서 처리할 수 없는 에러 코드) {
    			return {
    				shouldHandleError: false,
    				// 여기서 처리 할 수 없는 에러라면 render 단계에서 rethrow 하여 상위 에러 바운더리에서 처리하도록 합니다.
    				shouldRethrow: true,
    				error,
    			};
    	  }
    		return {
    			shouldHandleError: true,
    			shouldRethrow: false,
    			serializedPagewebError,
    		};
    	}
    
    	render() {
    		if(this.state.shouldRethrow) {
    			throw this.state.error;
    		}
    		if(!this.state.shouldHandleError) {
    			return this.props.children
    		}
    		if(미로그인 에러 코드) {
    			return (
    				<AuthError />
    			)
    		}
    		if(네트워크 에러 코드) {
    			// ApiErrorBoundary와 중복되는 에러 처리 코드입니다.
    			// Fetcher위에 ApiErrorBoundary가 누락 혹은 제외된 경우
    			return (
    				<NetworkError onClickRetry={() => this.setState({ shouldHandleError: false})} />
    			)
    		}
    		...
    		return (
    			<UnknownError onClickRetry={() => this.setState({ shouldHandleError: false})} />
    		)
    	}
    }

     

    📦 Error와 Loading에 대한 처리

    function CommentFetcher({ children }) {
      const { isLoading, error, data } = useGetData(); // data를 가져오는 hooks
    
      if (error) {
        // 실제로는 더 많은 정보(페이지 웹에서 사용하는 에러 코드)를 담고 있는 에러 객체입니다.
        throw error;
      }
    
      if (isLoading) {
        return <Loading />;
      }
    
      return children;
    }
    • 기본적으로 fetch에 대해서 data를 가져올 시에 loading과 error에 대한 처리를 해당 컴포넌트 내에서 처리를 하게 된다.
    • 이러한 부분을 Suspense와 ErrorBoundary를 통해서 해결한다면 해당 컴포넌트의 관심사를 더욱 명확하게 분리할 수 있다.

     

    변경 후

    const WithSuspenseNErrorBoundary = <Props extends object>(
      Component: ComponentType<Props>,
      options: {
        loadingFallback: ReactNode, 
        errorFallback: ReactNode  
      }) => {
      return function WrappedComponent(props: Props) {
      	return (
          <Suspense fallback={options.loadingFallback} >
          	<ApiErrorBoundary>
              <Component {...(props as Props)} />
            </ApiErrorBoundary>
          </Suspense>
        )
      }
    }
    
    export defalut WithSuspenseNErrorBoundary

    이러한 HOC pattern을 사용할 수 있다.

     

     

     

    📌 Reference

     

    댓글

Designed by Tistory.