Front-end/React

[React] Suspense의 동작 방식 및 비동기 처리 톺아보기 🎭

xeunnie 2025. 1. 30. 01:00
728x90
반응형

React Suspense의 동작 방식 및 비동기 처리 톺아보기 🎭

React에서 Suspense는 비동기 작업을 효과적으로 처리해 사용자 경험(UX)을 한 단계 끌어올리는 데에 중요한 역할을 합니다.

 

비동기 데이터를 처리하는 방식이 로딩 상태를 관리하고, 컴포넌트를 적절히 렌더링하는 데 있어 복잡성을 동반하는데요,

이 과정에서 상태 관리, 에러 핸들링, 로딩 UI 표시 등이 프론트엔드 개발에 필수적으로 요구됩니다.

 

Suspense

1. 비동기 컴포넌트를 쉽게 정의하고, 2. 데이터가 로드되는 동안 사용자에게 적절한 로딩 상태를 제공함으로써

매끄러운 사용자 경험을 유지할 수 있도록 합니다.

 

React의 Suspense를 활용하면,

1. 비동기 작업의 진행 상태를 시각적으로 표현하고, 2.사용자가 기다리는 동안 제공할 수 있는 피드백을 효과적으로 관리할 수 있습니다.

 

SuspenseReact의 Concurrent Mode와 함께 사용됨으로써,

애플리케이션의 전반적인 성능을 개선할 수 있는 가능성을 열어줍니다. 


1️⃣ Suspense란?

🎯 정의

Suspense는,

React 컴포넌트에서 비동기 데이터 로딩을 기다리며 화면에 로딩 상태를 보여줄 수 있도록 도와주는 컴포넌트입니다.

현재는 React.lazy비동기 데이터 Fetching에서 주로 사용되며, 개발자가 비동기 작업을 보다 쉽게 관리할 수 있도록 설계되었습니다.

 

Suspense를 사용하면,

  1. 데이터가 로드되는 동안 사용자에게 로딩 스피너와 같은 시각적 피드백을 제공합니다.
  2. 이를 통해 비동기 작업이 완료될 때까지 컴포넌트의 렌더링을 지연시킬 수 있습니다.

📌 왜 필요한가?

기존의 React 비동기 데이터 처리는 다음과 같은 문제를 동반했습니다:

  • 복잡한 로직
    • 비동기 데이터 로딩을 처리하기 위해 여러 상태 변수를 관리해야 했습니다.
    • 발생하는 문제: 복잡한 코드 & 낮은 가독성(로딩 상태, 오류 상태, 데이터 상태 등을 별도로 관리 필요)
  • 불필요한 리렌더링
    • 데이터를 로드할 때마다 새롭게 렌더링 해야 했습니다.
    • 발생하는 문제: 성능 저하 발생, 불필요한 깜빡임 & 불안정한 경험을 제공
  • 일관성 부족
    • 데이터가 다 로드되기 전 중간 상태로 인해 사용자 경험이 흔들림.

🏂 Suspense의 등장 후

개발자는 복잡한 비동기 로직을 덜 신경 쓰고도 직관적이고 깔끔한 코드를 작성할 수 있게 됐습니다.

또한, 비동기 데이터 로딩을 보다 일관되고 효율적으로 처리할 수 있게 됐습니다.

결과적으로 애플리케이션의 유지 보수성과 성능이 향상됐답니다.


2️⃣ Suspense의 기본 구조

import React, { Suspense } from 'react';

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

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

export default App;

🔍 어떻게 동작하나?

비동기 컴포넌트를 효율적으로 로드하기 위해 다음과 같은 단계를 거쳐 동작합니다:

1️⃣ LazyComponent 로드 시작

React.lazy는 동적으로 컴포넌트를 가져오는 기능으로, 이 과정에서 Promise를 반환합니다.

Promise는 컴포넌트가 준비될 때까지 대기하게 되며, 비동기적으로 모듈을 가져오는 과정을 자동으로 처리합니다.

👉 필요한 컴포넌트를 필요할 때만 로드할 수 있으며, 초기 번들 크기를 줄이는 효과를 얻을 수 있습니다.

2️⃣ Suspense 대기

Promise가 완료될 때까지 Suspense는 fallback에 지정된 컴포넌트를 화면에 표시합니다.

fallback 컴포넌트란? 현재 데이터가 로드 중임을 알리는 역할을 하는 컴포넌트로, 로딩 스피너, 메시지 등이 있습니다.

3️⃣ 완료 후 렌더링

Promise가 성공적으로 완료되면, 실제 컴포넌트가 렌더링됩니다.

이 과정에서 Suspense는 로딩 중 표시된 fallback 컴포넌트를 제거하고, 준비된 LazyComponent를 화면에 표시합니다.

사용자는 즉각적으로 새로운 UI를 확인할 수 있고, 비동기 데이터 로딩이 완료된 후의 자연스러운 전환이 보여집니다.


3️⃣ Suspense의 동작 방식: 비동기 데이터 처리

💡 React가 비동기 상태를 처리하는 과정

React의 Suspense는 “throwing a Promise”라는 독특한 메커니즘을 활용하여 비동기 데이터 처리를 간소화합니다.

1️⃣ 비동기 작업 감지:

Suspense 안의 컴포넌트가 비동기 데이터를 요청할 때, 해당 작업은 Promisethrow합니다.

이 throw는 React의 렌더링 사이클에 통합되어, 컴포넌트가 비동기 작업을 수행 중임을 나타냅니다.

즉, 컴포넌트가 비동기 데이터를 필요로 할 때 React는 이를 감지하여 다음 단계로 넘어갑니다.

2️⃣ React가 Promise를 캐치:

React는 이 Promise가 해결될 때까지 렌더링을 중단합니다.

이때, React는 대신 Suspensefallback 컴포넌트를 표시합니다.

fallback은 사용자에게 불필요한 지연이나 빈 화면을 보여주지 않는 역할을 합니다.

3️⃣ Promise 해결 시 재시도:

Promise가 성공적으로 해결되면, React는 렌더링을 재시도하여 데이터를 포함한 실제 UI를 표시합니다. 이 단계에서 Suspense는 이전의 fallback 컴포넌트를 제거하고, 이제 로드된 데이터를 기반으로 한 컴포넌트를 화면에 렌더링합니다. 이로 인해 사용자는 즉시 새로운 정보를 확인할 수 있으며, 비동기 작업의 완료가 사용자 경험에 자연스럽게 통합됩니다.

 

✨ Suspense의 내부 동작을 살펴보는 코드 예제

import React, { Suspense } from 'react';

// 데이터를 불러오는 비동기 함수
const fetchData = () => {
  return new Promise((resolve) =>
    setTimeout(() => resolve('Loaded data!'), 2000)
  );
};

// 데이터 로더 함수
const createResource = (asyncFn) => {
  let status = 'pending';
  let result;

  const suspender = asyncFn().then(
    (res) => {
      status = 'success';
      result = res;
    },
    (err) => {
      status = 'error';
      result = err;
    }
  );

  return {
    read() {
      if (status === 'pending') {
        throw suspender; // Suspense가 처리
      } else if (status === 'error') {
        throw result;
      } else if (status === 'success') {
        return result;
      }
    },
  };
};

// Suspense 리소스 생성
const resource = createResource(fetchData);

// 컴포넌트
const DataDisplay = () => {
  const data = resource.read(); // 데이터 읽기 (Suspense가 대기 처리)
  return <p>{data}</p>;
};

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

export default App;

🔍 이 코드의 흐름

  1. fetchData 함수
    • 비동기 데이터를 요청 👉 2초 뒤에 데이터를 반환
    • Promise를 반환 & 데이터 로드의 시뮬레이션을 담당
  2. createResource
    • 비동기 함수의 결과를 관리하는 객체를 생성
    • 데이터 읽기를 시도할 때, read() 메서드에서 Promise를 throw 👉 Suspense에 의해 처리
  3. React의 Suspense
    • throw된 Promise를 캐치
    • fallback으로 지정된 로딩 상태 컴포넌트를 표시 👉 사용자에겐 "Loading..." 메시지를 보여줌
  4. Promise가 해결되면
    1. DataDisplay 컴포넌트를 다시 렌더링 👉 실제 데이터를 포함한 UI를 표시
    2. 최종적으로 즉각적으로 로드된 데이터가 표시

4️⃣ Suspense의 UX 개선 효과

Suspense는 데이터 로딩 중 발생할 수 있는 플래시(FOUC) 현상을 방지하고, 더 부드럽고 일관된 사용자 경험을 제공합니다.

 

FOUC란?

Flash of Unstyled Content의 줄임말로,

페이지가 로드될 때 스타일이 적용되기 전에 콘텐츠가 순간적으로 나타나는 현상입니다. (특히 CSS가 비동기적으로 로드될 때 발생할 수 있습니다.)

사용자가 스타일이 적용되지 않은 콘텐츠를 확인하게 될 수도 있어 페이지가 불완전하게 보일 수 있습니다.

 

🚀 UX 개선 팁

1️⃣ 여러 개의 Suspense 중첩:

각 컴포넌트에 맞는 로딩 상태를 분리하여 세분화하는 것이 중요합니다.

여러 개의 Suspense를 중첩하면, 각 컴포넌트가 로딩되는 동안 사용자에게 적절한 피드백을 제공할 수 있습니다.

예를 들어, 헤더와 콘텐츠를 별도로 로딩할 수 있습니다:

<Suspense fallback={<p>Loading Header...</p>}>
  <Header />
</Suspense>
<Suspense fallback={<p>Loading Content...</p>}>
  <Content />
</Suspense>

 

2️⃣ CSS 애니메이션 활용

많은 서비스 애플리케이션에서 spinner같은 fallback을 사용하는 것을 흔히 찾아볼 수 있습니다.

<Suspense fallback={<div className="spinner"></div>}>
  <MainContent />
</Suspense>

 

3️⃣ Placeholder 사용

로딩 중에도 UI가 덜 비어 보이도록 기본 템플릿 제공하고 있답니다.

<Suspense fallback={<p>Fetching user details...</p>}>
  <UserDetails />
</Suspense>

5️⃣ Suspense와 Concurrent Mode

SuspenseConcurrent Mode와 결합하면 더욱 강력해진답니다:

  • 비동기 렌더링을 통해 UI의 일관성을 유지
    • Concurrent Mode는 React 애플리케이션이 여러 작업을 동시에 처리할 수 있도록 해줍니다.(비동기 작업이 진행되는 동안 UI의 일관성을 유지할 수 있습니다.)
    • 예를 들어, 데이터가 로드되는 동안 사용자가 다른 작업을 계속할 수 있rh, React는 인터페이스를 매끄럽게 업데이트합니다.
  • 여러 데이터를 병렬로 로드하여 성능을 극대화
    • Concurrent Mode는 여러 비동기 작업을 병렬로 실행할 수 있도록 지원합니다. (각각의 컴포넌트가 독립적으로 데이터를 요청하고 로드할 수 있게 하여, 전체 애플리케이션의 성능을 극대화)
    • 예를 들어, 사용자가 웹 페이지를 탐색할 때 여러 컴포넌트가 동시에 데이터를 가져올 수 있어, 페이지 로딩 시간이 단축됩니다.

6️⃣ Suspense가 불완전한 점

  • 현재 서버 컴포넌트에서 제한적:
    • Suspense는 서버에서 데이터를 렌더링하는 데 있어 아직 제한적인 기능을 가지고 있습니다.
    • React의 Suspense for Data Fetching과 같은 실험적 기능을 통해 서버 사이드 렌더링과 통합할 수도 있겠지만, 아직은 완전하지 않기도 하고, 안정성이나 성능 측면에서 개선이 필요합니다.
  • 서드파티 라이브러리 의존:
    • Suspense는 데이터 페칭 라이브러리와 함께 사용할 때 가장 큰 효과를 발휘합니다.
      • 예를 들어, React Query와 같은 라이브러리를 사용하면, 데이터 로딩, 캐싱, 동기화 등의 복잡한 로직을 간편하게 처리 가능합니다.
    • 그러나 이러한 의존성은 러닝커브도 올리고 라이브러리에 종속성도 올라가게 됩니다.

큰 단점은 아니지만 서버 컴포넌트에서 사용하는 것이 아직까지는 완전하지 않다는 점을 인지하고 사용해주시면 됩니다. Next 프로젝트 에서의 Suspense는 다르게 동작하고 있는데 다음엔 그 부분을 다뤄보도록 할께요!

 

🌷전설의 개발자가 되어봅시당! 🌷

728x90
반응형