[React & RN] Error Boundary 를 사용하여 에러 처리하기 (+ Suspense)

Chloe K·2023년 9월 11일
3

React-native

목록 보기
13/15
post-thumbnail
post-custom-banner

리액트로 작업하면서 Error Boundary를 아주 간단하게 에러를 처리할때 쓰는 기능으로만 알고 사용해본 적이 없었다. 리액트 네이티브로 프로젝트를 하면서 데이터를 fetching하는 과정에서 에러가 발생할 때 사용자에게 fallback으로 설정한 컴포넌트를 보여주면서 refetching을 할 수 있는 버튼을 만들면 좋겠다라는 생각에서 시작하여 Error Boundary를 도입하기로 했다.

Error Boundary란❓

공식문서에 따르면, 하위 컴포넌트 트리의 어디에서든 자바스크립트 에러를 기록하며 깨진 컴포넌트 트리 대신 폴백 UI를 보여주는 React 컴포넌트이다.

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;
  }
}

공식문서에서 가져온 에러 바운더리 컴포넌트이다. 함수형이 아닌 클래스형 컴포넌트만을 지원하는 것 같다. (현재 버전 18 이상) 이 클래스를 export해서 사용하면 된다.

import ErrorBoundary from '@components/ErrorBoundary'

<ErrorBoundary>
  <MyWidget />
</ErrorBoundary>

❗️오직 하위에 존재하는 컴포넌트의 에러만을 포착한다. 즉 렌더링하는 과정 중 <MyWidget/> 에서 에러가 발생한다면 이 에러를 잡아내는 것이다.


  render() {
    if (this.state.hasError) {
      // 폴백 UI를 커스텀하여 렌더링할 수 있습니다.
      return <h1>Something went wrong.</h1>;
    }

    return this.props.children;
  }

에러가 발생했을 때 보여주고 싶은 컴포넌트를 커스텀해서 보여주면 된다.

react-error-boundary & react-native-error-boundary

getDerivedStateFromErrorcomponentDidCatch등을 직접 구현해야하는 Error Boundary를 좀 더 쉽게 사용하기 위해서는 라이브러를 사용한다.

(필자는 리액트 네이티브에 적용했기 때문에 react-native-error-boundary를 사용했다.)

설치하기

npm i react-native-error-boundary
cd ios
pod install 
../

사용하기

import ErrorBoundary from "react-native-error-boundary";
import CustomFallback from "components/fallback/CustomErrorFallback";

(...)

  return (
    <Layout>
      <ErrorBoundary FallbackComponent={CustomFallback}>
          <Sample />
      </ErrorBoundary>
    </Layout>
  );

<ErrorBoundary/>는 아래와 같은 props를 전달한다.

  • Children
  • FallbackComponent
  • onError

<CustomFallback.tsx/>

import React from "react";
import { Text, Button } from "react-native";
import { styled } from "styled-components/native";

export type Props = { error: Error; resetError: () => void };

const CustomErrorFallback = ({ error, resetError }: Props) => {
  return (
    <Container>
      <ErrorMsg>Something happened!</ErrorMsg>
      <Text>{error.message}</Text>
      <RetryCopy>Please try again.</RetryCopy>
      <Button title="Refresh" onPress={resetError} />
    </Container>
  );
};

<FallbackComponent/>의 props

  • error: 발생한 에러
  • resetError: error state를 reset한다.

결과

error boundary를 사용하기 전에는 사용자에게 빈 화면만 보여줄 수밖에 없었지만 사용 후, 이렇게 fallback으로 에러 상황을 보여줄 수 있게 되었다.

✨ Suspense와 함께 사용하여 사용자경험 향상시키기 (with React-query)

Suspense는 컴포넌트가 렌더링되기 전에 다른 작업이 먼저 이루어지도록 해주는 컴포넌트이다. Suspense를 사용하면 로딩화면 구성, 컴포넌트 lazy loading등을 사용할 수 있다.

import React, { Suspense } from 'react';

const OtherComponent = React.lazy(() => import('./OtherComponent'));

function MyComponent() {
  return (
    <div>
      <Suspense fallback={<div>Loading...</div>}>
        <OtherComponent />
      </Suspense>
    </div>
  );
}

HugeComponent는 lazy를 사용해서 lazy loading이 이루어지는 동안 Suspense 는 fallback을 보여준다.

여기서 lazy component는 Suspense 컴포넌트의 하위에서 렌더링되어야 한다.


(...)
 
     <Layout>
      <ErrorBoundary FallbackComponent={CustomFallback}>
        <Suspense fallback={<Spinner />}>
          <Sample />
        </Suspense>
      </ErrorBoundary>
    </Layout>

위 코드처럼 Error Boundary 하위에 Suspense와 하위 컴포넌트를 배치하면 된다. Suspense는 <Sample/> 컴포넌트를 렌더링을 잠시 멈추고 fallback인 <Spinner/>를 먼저 보여준다. 이때 <Sample/>를 렌더링 하는 과정에서 에러가 발생하면 <ErrorBoundary/>에서 에러를 캐치하여 fallback을 보여준다.

react-query와 사용하기

const useNotesListQuery = (user: TUser) => {
  const filter = useRecoilValue(notesFilterState);
  const {
    isLoading,
    error,
    refetch,
    data: notesState,
  } = useQuery(["notes"], () => getNotes(user), {
    select: (data) => data.filter((note) => note.folder === filter),
    // Error Boundary 사용을 위한 옵션
    // suspense 옵션이 true인 경우에는 기본값이 true로 설정된다.
    useErrorBoundary: true,
    // 데이터 불러오기를 위한 Suspense를 활성화하는 옵션
    // useQuery Hook을 "Suspense를 지원하는 특별한 객체"로 사용
    suspense: true,
  });

  return { isLoading, error, refetch, notesState };
};

커스텀 훅으로 만든 useQuery를 가져왔다. suspense를 true로 설정해야 <Suspense/>가 잘 작동한다.

Suspense를 통해 Skeleton UI 노출하기

(...)

  return (
    <Layout>
      <ErrorBoundary FallbackComponent={CustomFallback}>
        <Suspense fallback={<NoteSkeleton />}>
          <NoteSection
            user={currentUser}
            handleScroll={handleScroll}
            moveToNote={moveToNote}
          />
        </Suspense>
      </ErrorBoundary>
    </Layout>
  );

따로 만들어뒀던 커스텀 Skeleton 컴포넌트를 Suspense의 폴백으로 사용하여 로딩이 이루어지는 과정 중에 Skeleton UI를 노출시켰다. 이로써 사용자는 데이터를 로딩하고 있는 중이라는 것을 인지하게 된다.

(skeleton UI 만드는데도 꽤 애먹었다.. 리액트 네이티브에서는 css의 애니메이션인 keyframe을 사용하지 못해서 RN에서 제공하는 Animated를 사용해야해서 많이 헤맸다. 😭 이 부분은 까먹지 않기 위해 따로 작성할 예정이다.)

Reference

공식문서

react-native-error-boundary

ref

profile
Frontend Developer
post-custom-banner

1개의 댓글

comment-user-thumbnail
2024년 2월 6일

고맙습니다 정리가 너무 깔끔하네요

답글 달기