React에서 선언적 비동기처리 다루기

KyungminLee·2024년 5월 16일
0

Next

목록 보기
4/5
post-thumbnail

1. 프론트엔드 성능 최적화

항상 프론트엔드 개발을 시작하기에 앞서 사용자경험(UX) 을 어떻게 하면 높일 수 있을지 고민하면서 개발을 시작한다.

사용자 경험을 향상시키기 위한 방법으로는 페이지 로드 시간 개선, API 응답 속도 향상, 웹 성능 최적화 등 여러 가지가 있지만, 이번 글에서는 비동기 작업이나 에러로 인해 화면이 제대로 표시되지 않는 상황을 효과적으로 처리하는 방법에 대해 집중적으로 다룰 예정이다.

다른 내용을 확인하고 싶다면 이전에 작성 된 아래 링크를 참고하자.

프론트엔드 성능최적화1

프론트엔드 성능최적화2

리액트에서 선언적으로 비동기처리를 하게 되면 개발자는 성공 상태와 비즈니스 로직에만 집중하여 컴포넌트를 개발할 수 있기 때문에 개발자 경험(DX) 또한 개선할 수 있다.

여기에서 가장 중요한 개념이 SuspenseErrorBoundary 이다.


2. 기존 방식의 문제

비동기 서버 통신을 React-query를 이용해서 구현한다면 다음과 같이 에러와 로딩 상태를 처리하게 될 것이다.

function UserProfile() {
  const { data, loading, error } = useShopping();

  if (loading) return <span>데이터를 불러오는 중입니다...</span>;
  if (error) return <span>문제가 발생했습니다</span>;

  return <div>...</div>;
}

위 코드는 기능적으로 문제가 없지만, 프로젝트 규모가 커지고 컴포넌트가 복잡해지면 문제가 발생한다.

문제점

  • 개발자 경험 저하: 생성되는 컴포넌트마다 매번 로딩 상태와 에러 상태를 확인하고 정의하는 반복 작업이 필요하다.

  • 사용자 경험 저하: 특정 컴포넌트에서 에러 핸들링이 되지 않는다면 전체 서비스가 멈출 수 있다.


3. Suspense와 선언적으로 Data Fetching 처리하기

Suspense: 로딩이 발생하는 부분에만 fallback을 Render 할 수 있다.
ErrorBoundary: 에러가 발생하는 부분에만 fallback을 Render 할 수 있다.

비동기 작업의 목표는 사용하는 애플리케이션이 멈추지 않고 다른 작업을 동시에 할 수 있도록 하기 위함이다.

만약 로딩이나 에러 상태가 발생했을 때 전체 어플리케이션이 멈추게 된다면 사용자 경험에 치명적인 영향을 줄 것이다. 따라서 부분적으로 로딩, 에러 상태를 보여주는 것이 좋다.

(1) Suspense 동작 원리

Suspense를 이용하면 Loading 상태를 선언적으로 관리할 수 있다.

  function ShoppingList(){
    const { data } = useShopping();
    return <div>{data.shoppingList}</div>
  }

  function Main() {
    return (
      <main>
        <Suspense fallback={<Loading />} />
          <ShoppingList />
        </Supsense>
      </main>
    )
  }

위 코드를 보면 컴포넌트의 loading, error 처리 상태가 없어지면서 개발자는 선언적으로 개발할 수 있게 되었다.
Suspense의 fallback 으로 로딩 컴포넌트를 넘겨 줘서 로딩 상태에 따른 렌더링 처리를 하였다.

(2) Suspense의 핵심 동작

  • 컴포넌트는 가장 가까운 Parent에 위치한 Suspense에게 Promise를 throw 한다.
  • Promise의 상태가 pending인 경우에는 fallback props에 전달된 컴포넌트를 렌더링하고 Promise의 상태가 resolve가 되면, 해당 컴포넌트를 렌더링한다.

(3) ErrorBoundary 동작 원리

ErrorBoundary가 도입된 배경은 UI에 존재하는 JS 에러가 전체 애플리케이션을 중단시켜서는 안 된다는 것이다.

따라서 ErrorBoundary라는 이름처럼 에러를 어떠한 경계 안에 가두고 기존 컴포넌트 대신 fallback UI를 보여주는 역할을 한다.

(4) ErrorBoundary의 핵심 동작

  • 핵심 원리는 Suspense와 마찬가지로 하위 컴포넌트에서 throw된 에러를 catch 한다.
  • 에러 catch가 가능한 이유는 class 컴포넌트의 생명주기 중 에러와 관련된 메서드 때문이다.

getDerivedStateFromError
자식 컴포넌트에서 오류가 발생했을 때 호출된다. 이때 주의할 점은 에러를 throw 받은 시점인 render 단계에서 호출되기 때문에 side effects를 발생시키면 안 된다.

throw된 에러를 catch하고 return 한 값을 기반으로 setState를 실행한다.

componentDidCatch
render 이후의 side effects를 다루는 메서드이다. 에러 로그를 기록하는 용도로 사용될 수 있다.

(5) React 생명주기(ErrorBoundary)

getDerivedStateFromError -> render -> componentDidCatch 순서에 따라 동작된다.

앞선 동작 원리에서 유추할 수 있듯이 class 컴포넌트의 생명주기 메서드를 이용하여 에러를 catch하기 때문에 ErrorBoundary는 class 컴포넌트로만 구현할 수 있다.

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

  static getDerivedStateFromError(error) {
    // 다음 렌더링에서 Fallback UI가 보이도록 상태를 업데이트 합니다.
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    // 에러를 기록합니다.
    logErrorToMyService(error, errorInfo);
  }

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

    return this.props.children;
  }
}

4. Suspense와 ErrorBoundary를 조합해서 사용하기

Suspense와 ErrorBoundary를 조합해서 하나의 컴포넌트로 로딩, 에러 상태의 작업을 모두 처리하여 개발자 경험(DX)을 향상 시킬 수 있습니다.

"use client";

import { ComponentType, PropsWithChildren, ReactNode } from "react";
import { ErrorBoundary, FallbackProps } from "react-error-boundary";

import Spinner from "@/components/spinner/Spinner";
import { Suspense } from "./Suspense";
import ErrorFallback from "./ErrorFallback";

interface Props {
  errorFallback?: ComponentType<FallbackProps>;
  suspenseFallback?: ReactNode;
}

export default function SearchDataErrorBoundary({
  errorFallback,
  suspenseFallback,
  children,
}: PropsWithChildren<Props>) {
  return (
    <ErrorBoundary FallbackComponent={errorFallback ?? ErrorFallback}>
      <Suspense fallback={suspenseFallback ?? <Spinner />}>{children}</Suspense>
    </ErrorBoundary>
  );
}

위와 같은 컴포넌트를 아래와 같이 적용할 수 있다.

interface SearchSectionProps {}

const SearchSection: React.FC<SearchSectionProps> = ({}) => {
  return (
    <section>
      <SearchDataErrorBoundary
        suspenseFallback={<ImageSkeleton variant="searchList" />}
      >
        {/* 보여주고 싶은 API 호출 컴포넌트 */}
      </SearchDataErrorBoundary>
    </section>
  );
};

export default SearchSection;

보여주고 싶은 API 호출 컴포넌트를 공통 Suspense + ErrorBoundary로 감싸주기만 하면 된다. 핵심은 API 호출을 Boundary 안에 가두는 것이다.

상태에 따른 UI 렌더링

  • 데이터 로드 전 Pending 상태
    suspenseFallback으로 전달한 ImageSkeleton 컴포넌트 렌더링

  • 비동기 작업 중 Error 발생
    errorFallback으로 전달한 ErrorFallback 컴포넌트 렌더링

  • 비동기 작업 완료 Fulfilled 상태
    '보여주고 싶은 API 호출 컴포넌트' 컴포넌트 렌더링


5. 정리

화면이 정상적으로 노출되지 않아서 사용자 경험이 저하되는 사례는 ErrorBoundary를 이용하여 API를 재호출하거나 특정 페이지로 redirect 하도록 처리하여 해결할 수 있다.

더 나아가, 비동기 로딩과 에러 처리 로직을 담당하는 Suspense와 Errorboundary를 결합한 컴포넌트를 도입함으로써 개발자는 성공 상태와 비즈니스 로직에만 집중할 수 있게 되어 개발 생산성을 높일 수 있다.

6. 참고자료

React에서 선언적 비동기 다루기

React 선언적 비동기 처리

profile
끊임없이 발전해가는 개발자.

0개의 댓글

관련 채용 정보