• 2021. 10. 22.

    by. 문익점

    반응형

    앞선 포스팅에서 Suspense와 ErrorBoundary를 이용 했을 때의 이점에 대해서 포스팅 했습니다. 이제는 이 두 컴포넌트를 하나로 합쳐서 더욱 사용하기 쉽게 만드는 방법에 대한 포스팅입니다.

    현재는 두 가지 컴포넌트로 감싸야 한다.

    function Main() {
      return (
        <ErrorBoundary fallback={<div>에러 발생!</div>}>
          <Suspense fallback={<div>로딩 중...</div>}>
            <UserInfo />
          </Suspense>
        </ErrorBoundary>
      )
    }

    UserInfo 컴포넌트안에서는 API 패칭이 일어나고 있고 데이터를 가져오는 동안에 랜더링 처리는 Suspense에 위임하고 있고 발생하는 오류(에러)처리는 ErrorBoundary로 위임 하는 모습입니다. 이 방식을 사용할려면 항상 두 가지의 컴포넌트로 감싸 주고 있는 모습을 볼 수 있습니다. 매번 데이터를 가져와서 랜더링 하는 컴포넌트를 사용 할 때마다 이 두 가지로 감싸줘야 한다면 그것도 매우 피곤한 일 입니다. 이는 두 가지 컴포넌트를 하나로 합쳐서 더욱 깔금하게 사용 할 수 있도록 개선되어야 합니다.

    합치기 전 좀 더 유용한 ErrorBoundary로.

    기존 React 공식 홈페이지에 있는 ErrorBoundary는 오류가 난 뒤 다시 오류를 리셋하는 기능을 제공하고 있지 않습니다(https://ko.reactjs.org/docs/error-boundaries.html). 아래는 공식 문서에서 제공하는 ErrorBoundary입니다.

    class ErrorBoundary extends React.Component {
      constructor(props) {
        super(props);
        this.state = { hasError: false };
      }
    
      static getDerivedStateFromError(error) {
        // 다음 렌더링에서 폴백 UI가 보이도록 상태를 업데이트 합니다.
        return { hasError: true };
      }
    
      componentDidCatch(error, errorInfo) {
        // 에러 리포팅 서비스에 에러를 기록할 수도 있습니다.
        logErrorToMyService(error, errorInfo);
      }
    
      render() {
        if (this.state.hasError) {
          // 폴백 UI를 커스텀하여 렌더링할 수 있습니다.
          return <h1>Something went wrong.</h1>;
        }
        return this.props.children;
      }
    }

    내부 구현체를 살펴보면 componentDidCatch 에서 에러 객체와 에러에 대한 정보를 받을 수 있지만 이 에러 상태를 다시 초기 상태로 초기화 하는 기능은 제공하고 있지 않습니다. 즉 한번 에러가 발생하면 <h1>Something went wrong.</h1> 가 랜더링이 된 후 다시 처음으로 돌아갈 수 없습니다. 즉 데이터 패칭을 다시 시도 할 수 없습니다. 이는 사용자에게 다시 불러오기와 같은 버튼을 제공 할 수 없게 됩니다.

    그러므로 공식으로 제공하는 Errorboundary보다 좀 더 확장성있는 버전을 사용하게 됩니다. 그것이 바로 react-error-boundary (https://www.npmjs.com/package/react-error-boundary)입니다. 이 라이브러리는 공식 문서에서 제공된 Errorboundary에서 여러가지 기능들이 추가되어 있습니다. 해당 라이브러리의 컴포넌트의 Props로 여러가지 옵션을 더 설정 할 수 있습니다.

    • resetKeys → 특정 값이 변경 되었을 때 초기화 함(의존성 배열).
    • onReset → 리셋이 일어난 후에 바로 실행되므로 후처리가 가능 함.
    • onError → 에러가 난 후에 바로 실행되므로 후처리가 가능 함.
    • FallbackProps → 리셋 콜백 함수를 넘길 수 있음.

    위와 같이 추가적인 기능으로 에러처리를 더욱 유용하게 가능하게 되었습니다.

     

    두 가지 컴포넌트를 합치자: AsyncBoundary

    react-error-boundary의 ErrorBoundary와 React의 Suspense를 합친 구현체인 AsyncBoundary의 모습입니다. 아래에서 어떤 방식으로 구현하였는지 하나하나 설명합니다.

    import React, { ReactNode, Suspense, SuspenseProps } from "react";
    import {
      ErrorBoundary,
      ErrorBoundaryPropsWithRender,
    } from "react-error-boundary";
    
    type ExceptFallbackErrorBoundaryAttributes = Omit<
      ErrorBoundaryPropsWithRender,
      "fallbackRender" | "fallback" | "FallbackComponent"
    >;
    
    type AsyncBoundaryProps = {
      children: ReactNode;
      ErrorFallback: ErrorBoundaryPropsWithRender["fallbackRender"];
      SuspenseFallback: SuspenseProps["fallback"];
    } & ExceptFallbackErrorBoundaryAttributes;
    
    function AsyncBoundary({
      children,
      ErrorFallback,
      SuspenseFallback,
      ...restErrorBoundaryAttributes
    }: AsyncBoundaryProps) {
      return (
        <ErrorBoundary
          fallbackRender={ErrorFallback}
          {...restErrorBoundaryAttributes}
        >
          <Suspense fallback={SuspenseFallback}>{children}</Suspense>
        </ErrorBoundary>
      );
    }
    
    export default AsyncBoundary;

    ErrorBoundarySuspense로 받아야 할 Props를 AsyncBoundary의 Props로 모두 받게 해야합니다.그 다음 AsyncBoundary의 chidren는 Suspense의 chidren으로 넘겨 줌으로 써 에러와 팬딩 처리를 외부로 위임 할 수 있는 형태로 만들어야 합니다.

     

    AsyncBoundary의 Props 설정하기

    일단 ErrorBoundary의 Props와 Susense의 Props를 합치기 위해서 ErrorBoundary의 Props를 추출해야 합니다.

    type ExceptFallbackErrorBoundaryAttributes = Omit<
      ErrorBoundaryPropsWithRender,
      "fallbackRender" | "fallback" | "FallbackComponent"
    >;

    ErrorBoundary의 fallback에는 에러 발생 시 랜더링 할 컴포넌트가 들어가게 됩니다. 이 컴포넌트는 각 3가지 방식으로 넣어 줄 수 있습니다. 각각의 차이점은 공식문서를 참조하면 됩니다. 우선 AsyncBoundaryfallbackRender를 사용하게 됩니다. ExceptFallbackErrorBoundaryAttributes 타입에선  ErrorBoundary가 에러 발생 시 랜더링 할 컴포넌트를 넣는 Props( fallback 관련)를 제외하고 나머진 옵션들만 넣기 위해서 Typescript의 유틸리티 타입인 Omit을 이용하여 fallback 관련 타입들은 제거합니다.

    type AsyncBoundaryProps = {
      children: ReactNode;
      ErrorFallback: ErrorBoundaryPropsWithRender["fallbackRender"];
      SuspenseFallback: SuspenseProps["fallback"];
    } & ExceptFallbackErrorBoundaryAttributes;

    이제 최종적으로 AsyncBoundaryProps를 정의해야합니다. chidren에는 감싸줄 컴포넌트를 받기 위해서 ReactNode로 정의해줍니다. ErrorFallback는 에러 발생 시 랜더링 할 컴포넌트를 넣어주기 위해 ErrorBoundaryPropsWithRender 타입에서 fallbackRender 옵션만 가져옵니다. SuspenseFallback도 마찬가지로 SuspenseProps에서 fallback옵션만 가져옵니다. 그 다음 Errorboundary의 나머지 옵션들을 받기 위해서 위에서 만든 ExceptFallbackErrorBoundaryAttributes를 & 연산자로 확장합니다.

    function AsyncBoundary({
      children,
      ErrorFallback,
      SuspenseFallback,
      ...restErrorBoundaryAttributes
    }: AsyncBoundaryProps) {
      return (
        <ErrorBoundary
          fallbackRender={ErrorFallback}
          {...restErrorBoundaryAttributes}
        >
          <Suspense fallback={SuspenseFallback}>{children}</Suspense>
        </ErrorBoundary>
      );
    }
    
    export default AsyncBoundary;

    자. 이제 AsyncBoundaryProps는 Errorboundary의 Props와 Suspense의 Props를 모두 받을수 있게 되었습니다.이제 이제 각각 Props로 받은 값들을 제자리로 넣어줍니다. Errorboundary의 나머지 옵션들은 스프레드 연산자를 이용하여 ErrorBoundary의 Props로 넣어줍니다.

     

    AsyncBoundary 사용하기

    앞선 포스팅에서 언급 하였듯이 React Suspense 컴포넌트에서 데이터를 가져오는 시점을 컨트롤 할려면 감싸준 컴포넌트가 React-query를 이용하여 suspense 모드이거나 Recoil의 비동기 셀렉터를 사용해야합니다. 이 예제에서는 비동기 셀렉터를 이용합니다.

    function RejectTest({ ms }: RejectTestProps) {
      const result = useTestReject(ms);
      return <div>{result.success}</div>;
    }
    
    export default RejectTest;

    RejectTest 컴포넌트를 이용하여 테스트 하고자 합니다. useTestReject hook을 이용하여 데이터를 가져오고 성공 유무를 랜더링하는 컴포넌트입니다.

    비동기 셀렉터의 모습 (useTestReject)

    class TestingService {
        static reject(ms: number) {
        return new Promise<TestEntity>((_, reject) => {
          setTimeout(
            () =>
              reject({
                success: false,
                status: 400,
              }),
            ms
          );
        });
      }
    }
    
    export default TestingService;
    
    const testRejectSelector = selectorFamily<TestEntity, number>({
      key: "testRejectState",
      get:
        (ms) =>
        async ({ get }) => {
          const response = await TestingService.reject(ms);
          return response;
        },
    });
    
    export function useTestReject(ms: number) {
      return useRecoilValue(testRejectSelector(ms));
    }

    Recoil의 비동기 셀렉터를 이용하는 모습입니다. TestingService는 파라메터로 전달받은 millisec 만큼 기다리다가 reject를 시킵니다. 이 요청을 testRejectSelector에서 비동기 셀렉터로 연결합니다. 마지막으로 만든 셀렉터를 useTestReject라는 hook으로 따로 빼어낸 모습입니다.

     

    실패하는 예시를 보며 최종적으로 사용해보기

    const [millisecond, setMillisecond] = useState(3000);
    ...
    
    <AsyncBoundary
      ErrorFallback={(rest) => <ErrorTestNotice {...rest} />}
      SuspenseFallback={<div>...loading</div>}
      onReset={(ms) => setMillisecond(ms as number)}
    >
      <RejectTest ms={millisecond} />
    </AsyncBoundary>;

    AsyncBoundary의 각각의 Props로 값들을 넣어준다음 RejectTest를 감싸줍니다. 이렇게 된다면 당연히 3초 후에 ErrorTestNotice 컴포넌트가 랜더링 될 것 입니다.

    import { FallbackProps } from "react-error-boundary";
    
    type ErrorTestNoticeProps = {} & FallbackProps;
    
    function ErrorTestNotice({ ...errorProps }: ErrorTestNoticeProps) {
      return (
        <div role="alert">
          <p>Something went wrong:</p>
          <pre>{errorProps.error.message}</pre>
    
          <button
            onClick={() => errorProps.resetErrorBoundary(Math.random() * 3000)}
          >
            Try again
          </button>
        </div>
      );
    }
    
    export default ErrorTestNotice;

    ErrorTestNotice는 props로 FallbackProps를 받게 됩니다. 이 Props는 에러 객체와 에러를 리셋 할 수 있는 콜백 함수를 받게 됩니다. 최종적으로 버튼을 클릭 할 때마다 millisec가 랜덤으로 배정되며 onReset에 정의 된 대로 밖에 있는 종속 변수인 millisecond가 변경되면서 계속해서 에러 상황을 리셋 할 수 있습니다.

    오류 상황이 아닐 떈?

    오류 상황이 아닐 때는 데이터를 가져오는 동안에는 <div>...loading</div>가 랜더링 되가 최종적으로는

    AsyncBoundary으로 감싼 컴포넌트가 잘 가져온 데이터로 아름답게 랜더링하게 될 것 입니다.

    Errorboundary와 Suspense를 같이 사용하는 법에 대해 포스팅 했습니다. 다음 포스팅은 에러 핸들링에 대해서 더 자세하게 포스트 하겠습니다.

    3탄:

    Refs.

    https://ko.reactjs.org/docs/error-boundaries.html

    https://www.npmjs.com/package/react-error-boundary

    https://jbee.io/react/error-declarative-handling-1/

    https://react-query.tanstack.com/guides/suspense#_top

    https://recoiljs.org/ko/docs/guides/asynchronous-data-queries/

    반응형