Concurrent UI 패턴 사용하기

Jemin·2023년 11월 20일
0

개발 지식

목록 보기
39/51
post-thumbnail

서론

현재 진행 중인 토이 프로젝트에서 React18버전을 사용하며 React Query를 통해 비동기 데이터를 관리하고 있다.

최근에 ErrorBoundary를 통해 렌더링시 발생하는 에러를 처리하는 방법을 사용했는데, 선언형 프로그래밍 방식이 마음에 들기도 하고 React 자체가 선언형 프로그래밍을 지향하는 것 같아서 점진적으로 명령형 컴포넌트들을 선언형 컴포넌트로 변경하는 작업을 해보려 한다.

아래는 React Query를 사용해 공공데이터를 받아와 List로 렌더링하는 컴포넌트다. 해당 컴포넌트를 선언형 컴포넌트로 변경해보자.

import { useGetCenters } from "../../hooks/queries/centerAPI";
import CenterItem from "../common/CenterItem";

const CenterList = () => {
    const { data, isLoading } = useGetCenters();

  	// 이 부분을 선언적으로 변경할 예정이다.
    if (isLoading) {
        return (
            <div className="flex justify-center">
                <p>데이터를 불러오는 중 입니다.</p>
            </div>
        );
    }

    if (data && data.data.length === 0) {
        return (
            <div className="flex justify-center">
                <p>저장된 데이터가 없습니다.</p>
            </div>
        );
    }

    return (
        <>
            {data &&
                data.data.map((center) => (
                    <CenterItem key={center.id} center={center} />
                ))}
        </>
    );
};

export default CenterList;

Concurrent UI 패턴이란

React 공식 문서를 참고해서 설명하자면 Concurrent란 우리말로 동시의 라는 뜻을 가지고 있다. 자바스크립트는 다중 코어를 이용해서 병렬적으로 실행시키는 언어가 아니다. 이벤트 루프라는 방식을 이용해 이 한계점을 최대한 극복할 수 있는 구조로 되어있으며, React 또한 마찬가지다. Concurrent 모드는 이런 환경 속에서 최대한 동시성을 추구할 수 있는 방법을 도입하겠다는 의미를 담고 있다고 한다.

Concurrent 모드를 사용하면 우선순위에 따른 화면 렌더, 컴포넌트의 지연 렌더, 로딩 화면의 유연한 구성 등을 쉽게 구성할 수 있도록 특성화된 기능들을 제공하고 있다. 이러한 기능들을 사용한 UI 개발 패턴을 React팀에서는 Concurrent UI Pattern 이라고 부르고 있다.

선언형 컴포넌트

참고한 카카오페이팀의 글에서는 이렇게 만들어진 컴포넌트를 선언형 컴포넌트라고 부르고 있다.

기존의 React Query의 isLoading이나 isError 같은 builtin 메서드를 사용해 어떻게 화면을 처리할지에 대해서 집중했지만, 선언형 컴포넌트를 사용하면 화면을 어떻게(HOW) 그릴지에 집중하는 것이 아니라 무엇을(WHAT) 보여줄 것인지에 집중할 수 있다.

Suspense

React의 Suspense는 비동기 데이터 로딩을 처리하기 위한 기능이다. Suspense를 사용하면 컴포넌트가 비동기적으로 로딩되거나 데이터를 불러올 때 로딩 상태를 처리할 수 있다.

기본적으로 Suspense는 React의 코드 스플리팅과 함께 사용되며, 아래와 같이 선언적으로 아주 가독성 좋은 코드를 만들 수 있다.

import React, { lazy, Suspense } from 'react';

const MyComponent = lazy(() => import('./MyComponent'));

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

export default App;

위 코드에서 MyComponentlazy 함수를 사용해 코드 스플리팅되었다. Suspense 컴포넌트는 비동기적으로 로딩되는 컴포넌트를 감싸고 있으며, fallback prop은 해당 컴포넌트가 로딩되는 동안에 표시될 로딩 상태를 나타낸다.

React Query와 함께 사용하기

React Query는 비동기 데이터 관리에 초점을 둔 라이브러리이기 때문에 당연히 Suspense관련 기능을 제공한다.

React Query v5 이전에는 useQuery 함수의 Option으로 suspense: ture를 해주었지만 v5 이후에 useSuspenseQuery라는 함수가 생겼다.

export const useGetCenters = () => {
    const { data, isSuccess, isLoading, isError } = useSuspenseQuery({
        queryKey: ["getCenters"],
        queryFn: () => centerAPI.getCenters(),
    });

    return {
        data,
        isSuccess,
        isLoading,
        isError,
    };
};

이제 이 Query를 호출한 컴포넌트의 로딩 상태를 Suspense 컴포넌트가 알 수 있다.

이제 React에서 제공하는 Suspense 컴포넌트로 감싸주기만 하면 된다.

import CenterList from "../components/list/CenterList";
import { QueryErrorResetBoundary } from "@tanstack/react-query";
import { ErrorBoundary } from "react-error-boundary";
import FallbackUI from "../components/common/FallbackUI";
import { Suspense } from "react";
import Loading from "../components/common/Loading";

const List = () => {
    return (
        <section>
            <QueryErrorResetBoundary>
                {({ reset }) => (
                    <ErrorBoundary onReset={reset} fallbackRender={FallbackUI}>
                        <Suspense fallback={<Loading />}>
                            <CenterList />
                        </Suspense>
                    </ErrorBoundary>
                )}
            </QueryErrorResetBoundary>
        </section>
    );
};

export default List;

CenterList컴포넌트에서 useSuspenseQuery를 호출하기 때문에 바깥을 Suspense컴포넌트로 감싸서 비동기 데이터를 받아오기 전에 Loading컴포넌트를 노출시킬 수 있다.

이제 Query함수에서 isLoading 메서드를 받을 필요가 없고 CenterList 컴포넌트의 역할은 데이터가 로드되었을 때 출력해주는 역할만 해주면 된다.

import { useGetCenters } from "../../hooks/queries/centerAPI";
import CenterItem from "../common/CenterItem";

const CenterList = () => {
    const { data } = useGetCenters();

    if (data.data.length === 0) {
        return (
            <div>
                <p>저장된 데이터가 없습니다.</p>
            </div>
        );
    }

    return (
        <>
            {data.data.map((center) => (
                <CenterItem key={center.id} center={center} />
            ))}
        </>
    );
};

export default CenterList;

기존의 && 연산자도 필요가 없는데 CenterList컴포넌트가 렌더링되는 시점에는 이미 로딩이 끝나고 데이터가 받아와진 이후에 렌더링되기 때문이다.

마무리

적용하다가 ErrorBoundary컴포넌트에 대한 궁금증이 생겼는데 fallbackRenderFallbackComponent라는 prop으로 fallbackUI를 받을 수 있다는 것이다. 둘의 차이가 무엇인가 살펴보았는데, 전혀 다른 점을 찾을 수 없었다. 그냥 사용자가 가독성이 더 좋다고 판단되는 prop으로 넘겨주면 될 것 같다.

지금은 Query로 데이터를 불러오는 부분이 1개이기 때문에 쉽게 적용할 수 있었지만, 다수의 Query를 호출해서 데이터를 받아온다면 어떻게 될지는 잘 모르겠다. 아마 ErrorBoundary컴포넌트를 각각에 감싸주고 Suspense 컴포넌트도 각각에 감싸주면 잘 해결될 것이라고 본다.

참고
React Query와 함께 Concurrent UI Pattern을 도입하는 방법
사용자 경험 개선을 위한Concurrent UI Pattern

profile
경험은 일어난 무엇이 아니라, 그 일어난 일로 무엇을 하느냐이다.

0개의 댓글