react-error-boundary 사용해서 함수형 컴포넌트 에러 처리하기

    반응형

    react-error-boundary

     

    이 전의 글에서 React의 ErrorBoundary를 사용해서 에러 처리하는 방법을 알아보았다. 아쉬운 점은 함수형으로 만들 수 없다는 것이었는데 React 공식 문서에도 클래스 형 ErrorBoundary의 대안으로 react-error-boundary를 제안하고 있다. 이 글에서는 react-error-boundary의 사용방법에 대해 알아보고 프로젝트에서 어떻게 사용했는지에 대해 작성해 보려고 한다.

     

    react-error-boundary


    react-error-boundary는 클래스 형 ErrorBoundary의 함수형으로 사용할 수 있는 대안으로 함수형 컴포넌트를 사용해서 리액트 프로젝트를 작업하는 경우 클래스 형을 사용해서 ErrorBoundary를 사용하는 것보다 일관되게 함수형으로 사용할 수 있도록 해준다. 클래스 형 ErrorBoundary를 살펴보면 기본적으로 대부분 동일한 fallback UI를 렌더링 한다.

     

    class ErrorBoundary extends React.Component {
      constructor(props) {
        super(props);
        this.state = { hasError: false };
      }
    
      static getDerivedStateFromError(error) {
        return { hasError: true };
      }
    
      componentDidCatch(error, errorInfo) {
        logErrorToMyService(error, errorInfo);
      }
    
      render() {
        if (this.state.hasError) {
          return <h1>Something went wrong.</h1>;
        }
    
        return this.props.children; 
      }
    }

     

    위의 코드처럼 기본적인 형태는 항상 동일한 fallback UI를 렌더링 한다. 하지만 에러의 타입에 따라 다른 UI를 표시하고 싶다면 코드가 길어지고, 재사용성이 떨어진다. 에러의 타입별로 다른 fallback UI를 렌더링 하는 예시 코드를 보면 아래와 같다.

     

    class ErrorBoundary extends React.Component {
      constructor(props) {
        super(props)
        this.state = { hasError: false, error: null }
      }
    
      static getDerivedStateFromError(error) {
        return { hasError: true, error }
      }
    
      componentDidCatch(error, errorInfo) {
        console.log(error, errorInfo);
      }
    
      render() {
        if (this.state.hasError) {
          if (this.state.error instanceof TypeError) {
            return (
              <div>
                <h1>타입 에러가 발생했습니다.</h1>
                <p>{this.state.error.message}</p>
              </div>
            )
          } else if (this.state.error instanceof ReferenceError) {
            return (
              <div>
                <h1>참조 에러가 발생했습니다.</h1>
                <p>{this.state.error.message}</p>
              </div>
            )
          } else {
            return (
              <div>
                <h1>알 수 없는 에러가 발생했습니다.</h1>
                <p>{this.state.error.message}</p>
              </div>
            )
          }
        }
        return this.props.children;
      }
    }

     

    예시 코드와 같이 유연성, 재사용성 등이 떨어진다. 하지만 react-error-boundary는 props를 통해 다양한 에러 처리 로직을 사용할 수 있기 때문에 더 유연하고 재사용이 가능한 에러 처리가 가능하다.

     

    react-error-boundary 설치 및 사용방법


    설치

    # npm
    npm install react-error-boundary
    
    # pnpm
    pnpm add react-error-boundary
    
    # yarn
    yarn add react-error-boundary

     

    사용법

    import { ErrorBoundary } from "react-error-boundary"
    
    <ErrorBoundary fallback={<div>에러가 발생했습니다.</div>}>
      <App />
    </ErrorBoundary>

     

    기본적인 사용법은 ErrorBoundary 클래스 형과 매우 유사하다. react-error-boundary의 ErrorBounday 컴포넌트 기본적인 fallback prop의 사용방법이다. 일관적인 에러 fallback UI를 보여줄 때 사용할 수 있다.

     

    <ErrorBoundary
      fallbackRender={({ error, resetErrorBoundary }) => (
        <div>
          <h1>에러가 발생했습니다</h1>
          <pre>{error.message}</pre>
          <button onClick={resetErrorBoundary}>다시 시도</button>
        </div>
      )}
    >
      <MyComponent />
    </ErrorBoundary>

     

    fallbackRender prop은 함수를 받는다. 이 함수는 JSX 혹은 TSX를 리턴하는 함수를 작성할 수 있다. fallbackRender의 UI가 길어지면 ErrorBoundary의 fallbackRender의 선언부가 길어질 수 있다. 

     

    function ErrorFallback({ error, resetErrorBoundary }) {
      return (
        <div>
          <h1>에러가 발생했습니다</h1>
          <pre>{error.message}</pre>
          <button onClick={resetErrorBoundary}>다시 시도</button>
        </div>
      );
    }
    
    <ErrorBoundary FallbackComponent={ErrorFallback}>
      <MyComponent />
    </ErrorBoundary>

     

    FallbackComponent prop은 이름 그대로 이미 정의된 컴포넌트를 받는다. 이미 정의된 컴포넌트를 받기 때문에 재사용성이 높다. 에러의 UI에 관련된 로직을 따로 컴포넌트로 분리할 수 있기 때문에 최적화의 활용에도 좋다.

     

    onRest prop


    react-error-boundary의 ErrorBoundary는 에러가 발생하면 fallback UI에 에러 정보를 담은 error와 에러 상태를 초기화 하는 함수인 resetErrorBoundary를 받는다. resetErrorBoundary를 실행하면 에러 상태를 초기화하고 재렌더링 한다. 하지만 ErrorBoundary에 onRest prop을 전달하고 resetErrorBoundary가 호출되면 onReset 에 정의된 로직이 실행되고 에러 정보를 초기화한고 다시렌더링 한다.

     

    // 에러처리 로직 component
    function ErrorFallback({error, resetErrorBoundary}) {
      return (
        <div>
          <h1>에러가 발생했습니다</h1>
          <pre>{error.message}</pre>
          <button onClick={resetErrorBoundary}>다시 시도</button>
        </div>
      );
    }
    
    // onRest을 사용해서 resetErrorBoundary가 실행되면 데이터를 다시 가져와 상태에 저장
    <ErrorBoundary
      FallbackComponent={ErrorFallback}
      onReset={() => {
        // 데이터를 다시 가져오는 로직
        fetchData().then(setData);
      }}
    >
      {/* 랩핑된 자식 컴포넌트 */}
    </ErrorBoundary>

     

    react-error-boundary에서 제공하는 hook


    useErrorBoundary hook

    클래스 형 ErrorBoundary는 렌더링 혹은 생명주기 메서드 중에 발생한 자바스크립트 에러를 잡아 fallback UI를 렌더링 한다. 이벤트 핸들러, 비동기적 코드 등의 실행에서는 에러를 잡을 수 없는데 useErrorBoundary hook은 이러한 에러들을 잡아 가장 가까운 ErrorBoundary로 에러를 전달한다. 이 hook은 showBoundary, resetBounday 함수를 반환하다. 필자는 useErrorBoundary hook을 사용해서 에러 처리를 했다.

     

    // index.tsx
    
    const ErrorFallback = ({ error }: { error: any }) => {
      return <StateMessage type="error" error={error.message} />
    }
    
    const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement)
    root.render(
      <ErrorBoundary fallbackRender={ErrorFallback}>
        <App />,
      </ErrorBoundary>,
    )

     

    firestore에서 데이터를 가져오는 부분에 에러 처리를 하기 위해 사용했다. fallbackRender Prop을 사용했고 최상위 컴포넌트인 App 컴포넌트를 랩핑했다.

     

    // useAppState.tsx
    
    import { useState } from 'react'
    
    import { useErrorBoundary } from 'react-error-boundary'
    
    import { doc, getDoc } from 'firebase/firestore'
    import { db } from '@firebaseApp'
    
    import { Wedding } from 'models/wedding'
    
    const useAppState = () => {
      const { showBoundary } = useErrorBoundary()
    
      const [loading, setLoading] = useState<Boolean>(false)
    
      const [wedding, setWedding] = useState<Wedding | null>()
    
      const getWeddingData = async () => {
        try {
          setLoading(true)
    
          const docRef = doc(db, 'DOC', '문서ID')
          const docSnap = await getDoc(docRef)
    
          if (!docSnap.exists()) {
            throw new Error('데이터를 가져올 수 없습니다.')
          }
    
          setWedding(docSnap.data() as Wedding)
          setLoading(false)
        } catch (error: any) {
          showBoundary(error)
        }
      }
    
      return { loading, wedding, getWeddingData }
    }
    
    export default useAppState

     

    만약 문서에 데이터가 하나도 없다면 새로운 에러를 생성해서 catch 블록에서 잡아내고 이러한 에러를 useErrorBoundary를 사용해서 가까운 ErrorBoundary로 전달한다.

     

    // stateMessage.tsx
    
    import { useErrorBoundary } from 'react-error-boundary'
    
    interface StateMessageProps {
      type: 'loading' | 'error'
      error?: string
    }
    
    const StateMessage = ({ type, error }: StateMessageProps) => {
      const { resetBoundary } = useErrorBoundary()
    
      return (
        <div className="message__container">
          {type === 'loading' ? (
            <>
              <Heart />
              <span className="message__text">청첩장</span>
            </>
          ) : (
            <>
              <Error />
              <span className="message__text-error">
                청첩장을 가져오지 못했어요
              </span>
              <span className="message__text-error-desc">{error}</span>
              <button className="message__text-error-btn" onClick={resetBoundary}>
                다시 시도하기
              </button>
            </>
          )}
        </div>
      )
    }
    
    export default StateMessage

     

    에러가 발생하면 렌더링되는 fallBack UI에서 resetBoundary를 사용해서 다시 시도할 수 있도록 사용자 경험을 증가시키는 방법을 선택해서 코드를 작성했다. 이 외에도 더 자세한 내용은 아래의 링크에서 확인할 수 있다.

     

     

    GitHub - bvaughn/react-error-boundary: Simple reusable React error boundary component

    Simple reusable React error boundary component. Contribute to bvaughn/react-error-boundary development by creating an account on GitHub.

    github.com

     

    * 공부한 내용을 정리하는 것이기 때문에 틀린 부분이 있을 수 있습니다. 지적해 주시면 확인 후 수정하겠습니다.

    반응형

    댓글