Skip to content

React Suspense 도입 과정에서 발생한 성능 저하 개선하기

이지은 (오월) edited this page Dec 20, 2023 · 5 revisions

로딩 기능을 도입했을 때 발생한 문제점


알고션 페이지는 SPA (Single Page Application) 특성상
동적 데이터 fetching으로 인한 CLS (Cumulative Layout Shift)을 막기 위해 로딩 상태에서는 로딩 화면을 보여줍니다.


기존에 로딩 상태를 제어하는 로직은
아래 코드와 같이 조건문을 통한 명령형 프로그래밍 방식으로 작성되었습니다.

이는 고질적인 명령형 프로그래밍 방식의 문제로 이어졌습니다.
바로 상황이 다양해 질수록 코드가 점점 지저분해지고 복잡해 진다는 것이었습니다.


// 명령형 방식으로 로딩 상태를 제어하는 컴포넌틀 로직
const MyComponent = () => {
  const [isLoading, setIsLoading] = useState(false);
  const [data, setData] = useState(null);

  const updateData = async () => {
    if (isLoading) return;

    setIsLoading(() => true);
    const data = await getServerData();
    setData(() => data);
    setIsLoading(() => false);
  };

  useEffect(() => {
    updateData();
  }, []);

  return (
    <>
      {isLoading && <Loading />}
      {!isLoading && !!data && <ChildComponent data={data} />}
    </>
  );
};

당시 알고션 페이지는 실제로 동작하는 컴포넌트라는 특성상
예시보다 훨씬 더 지저분하고 복잡한 로직을 가지고 있었습니다. 🥲

앞으로 페이지 기능이 확장되는 것을 고려해 봤을 때
데이터 fetching 기능이 추가될수록 로딩 상태를 관리하는 로직은 점점 더 복잡해질 것이 확실했으며,
이는 결국 유지보수에 큰 문제가 되리라 판단했습니다.

따라서 로딩과 관련된 로직을 컴포넌트와 분리하여 선언적으로 다루기 위해 Suspense를 도입하기로 결정했습니다.



Suspense란?

Suspense는 Suspense 컴포넌트 내부에서 발생한 Promise를 catch하여
해당 Promise가 resolve 되기 전까지 컴포넌트의 렌더링을 잠시 멈추고
대체 컴포넌트를 보여줄 수 있게 해주는 기능을 제공합니다.

### Suspense란?

이러한 Suspense의 특성과 데이터를 fetching하는 동안 Promise를 throw하는 비동기 데이터 관리 라이브러리를 함께 이용하면
선언적으로 로딩 로직을 구현할 수 있게 됩니다.


Suspense 도입 방법

React SuspenseReact-Query를 이용하여 다음과 같이 로딩 로직을 분리했습니다.

React v.18 이상, React-Query v5 버전

const MySuspenseComponent = () => {
  const { data: data1 } = useSuspenseQuery({
    queryKey: ["DATA_1"],
    queryFn: getServerData,
  });

  const { data: data2 } = useSuspenseQuery({
    queryKey: ["DATA_2"],
    queryFn: getServerData,
  });

  return (
    **<Suspense fallback={<Loading />}>**
      <ChildComponent data={data1} />
      <ChildComponent data={data2} />
    **</Suspense>**
  );
};

React-Query에서 기본 제공되는 메서드로 데이터 fetching 로직을 구현한 후
로딩 처리가 필요한 컴포넌트 상단에 선언적으로 Suspense를 감싸주어
데이터를 fetching이 이루어지는 동안 Suspense가 알아서 로딩 화면을 보여주게 됩니다.


이렇게 Suspense에 로딩과 관련된 로직을 위임하면
컴포넌트는 아무리 데이터 fetching이 많아지더라도 더 이상 로딩 상태 로직이 복잡해 질까봐 걱정하지 않아도 됩니다.

완전한 관심사 분리가 이루어지면서 개발 효율성이 증가하게 됩니다.



Suspense로 인해 발생한 성능 저하 : Waterfall 해결하기

Suspense를 도입하자, 또다른 예상치 못한 문제가 발생했습니다.
Waterfall 현상이 발생한다는 것이었습니다.​

Waterfall 현상이란​

데이터 fetching이 폭포처럼 순차적으로 일어나는 것을 의미합니다.

Suspense 내부 데이터 fetching을 실행했을 때,​
동시에 실행될 것이라는 예측과 달리 아래 그림과 같이 순차적으로 실행되었습니다.​

이는 모든 데이터 로딩이 완료될 때까지의 시간이
극단적으로 길어지는 성능 저하 문제로 이어졌습니다.

Waterfall 현상 발생 원인

Waterfall 현상이 발생했던 원인은
Suspense가 Promise resolve 될 때 까지 모든 후순위 작업을 일시중지하기 때문이었습니다.

Suspense 특성으로 인해 해당 컴포넌트 내부의 API들이 순차적으로 실행되었고
곧 Waterfall 현상으로 이어졌습니다.

랜더링이 완료되기 까지의 시간을 극단적으로 늘어났습니다. 따라서 순차적 호출을 제거하여 랜더링 속도를 개선할 필요가 있었습니다.


Waterfall 현상을 제거하여 랜더링 속도 개선하기

Waterfall 현상을 제거하기 위한 방법으로는 2가지가 있었습니다.​

하나는 react query에서 기본 제공되는 useSuspenseQueries 메서드를 이용하는 것이며,​
다른 하나는 Suspense당 하나의 비동기만 담당하도록 API를 각 다른 Suspense로 감싸주는 것이었습니다.​

이 중 useSuspenseQueries를 이용하여 문제를 해결하는 방법이 가독성 측면에서 우수하다고 판단하여​ 해당 방법으로 Waterfall 현상을 해결했습니다.

개선 결과

useSuspenseQueries를 적용한 결과​
모든 데이터 fetching이 비동기적으로 실행되면서
사진과 같이 랜더링 시간을 단축할 수 있었습니다.​

(✅ 애니메이션)​

이처럼 트러블 슈팅을 통해 Suspense 내부 동작 방식에 이해 이해하고,​

이를 바탕으로 성능을 개선할 수 있었던 경험을 공유드리며 마치도록 하겠습니다. 감사합니다.

🌊 ALGOCEAN

TEAM : 강서(대문)구

기획

아키텍처

스프린트 계획회의

데일리스크럼

팀 회고

개발 일지

태호

more

지호

more

지은

more

승규

more

멘토링 일지

Clone this wiki locally