React 비동기 처리와 성능 향상(feat. lazy, Suspense, ErrorBoundary)

dk.han·2023년 1월 9일
3
post-thumbnail

BilliG 프로젝트에서 API 통신으로 데이터를 fetching 할 때, 리액트 쿼리의 useQuery를 통해 isLoading, isError를 받아와 컴포넌트 내에서 비동기 처리 로직을 작성했었다. 이렇게 작성한 코드가 올바른 방법인지에 대해 코치님들께 질문을 하였고 Suspense를 통해 고급스럽게 처리할 수 있다는 피드백을 받았다. 때문에 리팩토링 기간 중 React.lazy, Suspense, ErrorBoundary를 이용해서 API 비동기 처리, 에러 처리 로직을 컴포넌트에서 삭제하고 관심사 분리를 할 수 있었다. 이에 대한 기록을 남기고자 한다.

React.lazy

컴포넌트를 dynamic import를 통해 동적으로 불러오기 위해서는 React.lazy()를 사용해야 한다.
컴포넌트를 동적으로 불러온다는 것은 컴포넌트를 필요한 순간에 불러와서 사용함을 의미한다.
일반적인 import를 사용할 경우 사용자가 첫 페이지를 로드할 때, 페이지에서 사용되지 않는 컴포넌트들(모든 JS 파일)까지도 모두 번들로 만들어 사용자에게 전송된다. 이는 번들의 크기가 커져 다운받는 시간이 오래 걸리게 되고, 웹 페이지의 성능 저하에 큰 영향을 끼친다.
때문에 React.lazy를 통한 Code Splitting(코드 분할)으로 첫 렌더링에 필요한 파일의 크기를 줄여 렌더링 속도를 향상시킬 수 있다.

React.lazy() 문법.

const Main = lazy(() => import('./pages/Main'));
const MyPage = lazy(() => import('./pages/MyPage'));
const SubmainPage = lazy(() => import('./pages/SubmainPage'));
const ProductsList = lazy(() => import('./pages/ProductsList'));
const Writing = lazy(() => import('./pages/Writing'));

lazy 컴포넌트는 반드시 Suspense 컴포넌트 하위에서 렌더링 되어야한다. 그럼 Suspense는 뭔데?

Suspense

Suspense는 컴포넌트 내부에서 데이터를 가져오는 API 통신과 같은 비동기적 로직이 실행될 때, 해당 작업이 끝날 때까지 컴포넌트의 렌더링을 멈추는 역할을 한다. 또한 비동기 로딩 시 fallback으로 받은 컴포넌트를 사용자에게 보여줌으로써 비동기 처리 상태에 따라 사용자에게 보여줄 화면을 결정할 수 있다.

SubmainPage

const SubmainCategory = lazy(
  () => import('../components/category-submain/SubmainCategory'),
);
export default function SubmainPage() {
  return(
          <Suspense fallback={<Loading />}>
          	<Routes>
            	<Route path="/" element={<SubmainCategory type="borrow" />} />
            	<Route path="/borrow" element={<SubmainCategory type="lend" />} />
          	</Routes>
          </Suspense>
  )
}

컴포넌트 내에서 비동기 처리 로직을 Suspense로 관심사 분리를 하게 되면 컴포넌트 내에서 코드를 동기적으로 작성할 수 있다.
컴포넌트 내에서 isLoading을 통해 화면을 컨트롤하고, optional chaining(?.)을 통해 코드를 작성하지 않아도 된다는 의미이다.
Suspense를 적용하기 위해서는 useQuery의 옵션 suspense: true를 설정해야 한다.

  const { data: categories } = useQuery(['categories'], getCategories, {
    refetchOnWindowFocus: false,
    staleTime: 60 * 1000 * 60,
    useErrorBoundary: true,
    suspense: true,
  });

ErrorBoundary

컴포넌트 내에서 useQuery를 통해 isError를 받아와 API 통신 중 에러가 발생하게 되면 에러 페이지 화면을 사용자에게 보여주는 식으로 에러 처리를 하였다. 하지만 에러 처리도 ErrorBoundary를 통해 관심사 분리를 할 수 있었다.

      <ErrorBoundary key={pathname}>
        <Suspense fallback={<Loading />}>
          <Routes>
            <Route path="/" element={<SubmainCategory type="borrow" />} />
            <Route path="/borrow" element={<SubmainCategory type="lend" />} />
          </Routes>
        </Suspense>
      </ErrorBoundary>

리액트 공식 사이트에서 제공하는 ErrorBoundary 컴포넌트에 타입을 추가하여 사용하였다. getDerivedStateFromError, componentDidCatch 두 가지 라이프 사이클을 사용하는데 이는 hook에서 핸들링할 수 있는 방법이 없기 때문에 클래스형 컴포넌트로 이루어져 있다.

import React, { ErrorInfo, ReactNode } from 'react';
import ErrorPage from 'components/ErrorPage';

interface Props {
  children?: ReactNode;
}

interface State {
  hasError: boolean;
  error?: Error;
}

class ErrorBoundary extends React.Component<Props, State> {
  constructor(props: Props) {
    super(props);
    this.state = {
      hasError: false,
    };
  }

  static getDerivedStateFromError(error: Error): State {
    return { hasError: true, error };
  }

  componentDidCatch(error: Error, errorInfo: ErrorInfo) {
    console.log('error:', error);
    console.log('errorInfo:', errorInfo);
  }

  public render() {
    if (this.state.hasError) {
      return <ErrorPage />;
    }

    return this.props.children;
  }
}

export default ErrorBoundary;

아래 4가지에 대해서는 ErrorBoundary가 처리하지 않는다.

  • 이벤트 핸들러 (이벤트 핸들러는 렌더링 중에 발생하지 않기 때문, try-catch로 처리)
  • setTimeout과 같은 비동기적 코드
  • 서버사이드 렌더링
  • 자식이 아닌 ErrorBoundary 자체에서 발생한 예외

나의 생각

lazy와 suspense는 성능 향상과 비동기 처리를 고급스럽게 할 수 있기 때문에 좋은 기술이라고 생각한다.
하지만 ErrorBoundary는 꼭 사용해야 하나?라고 의문을 가지게 되었다. 각 페이지마다 에러를 처리하는 UI가 다를 수 있기 때문에 컴포넌트 내에서 해결하는 게 더 효율적이지 않을까라고 생각했다. 또한 리액트에서 제공하는 ErrorBoundary는 단지 UI만 표시할 수 있어, 커스텀 해서 사용하거나 react-error-boundary 라이브러리를 사용해야 하는 번거로움이 존재한다.
공부하는 입장이기 때문에 lazy, Suspense, ErrorBoundary에 대해 공부하고 적용시켜보았지만, ErrorBoundary는 필수적인 요소는 아니라 판단하게 되었다.

Reference

React Query와 함께 Concurrent UI Pattern을 도입하는 방법
React.lazy
React.Suspense
Error Boundary

1개의 댓글

comment-user-thumbnail
2023년 1월 17일

훌륭한 글입니다.

답글 달기