프론트엔드/React

React - Suspnese

학습하는 청년 2024. 8. 1. 16:09

최종 수정일 : 2024-08-01

Suspense

비동기 로직을 더 선언적이고 관리하기 쉽게 만들어, 개발자 경험을 크게 향상시킨다. 이는 복잡한 UI와 데이터 흐름을 가진 현대적인 웹 애플리케이션 개발에 매우 유용한 도구이다.

 

기본 설명

1. 기존 비동기 처리의 문제점

React 18 이전 버전에서는 주로 코드 스플리팅에만 사용 가능했다(16.6ver에서 등장).

React에서 비동기 데이터 로딩은 컴포넌트 내부에서 처리됐다. 이로 인해 많은 조건부 렌더링 로직과 로딩 상태 관리를 필요로 했다. 이는 복잡한 애플리케이션에서는 유지보수를 어렵게 만들었다.

 

2. Suspense의 해결책

Suspense는 비동기 작업을 컴포넌트 외부로 추상화시켜 로딩 상태를 선언적으로 정의할 수 있게 해준다. 또한, 조건부 렌더링을 사용하지 않고, 컴포넌트는 데이터가 준비됐을 때만 렌더링 된다.

 

3. 작동 방식

  • Suspense는 하위 컴포넌트가 던진 Promise를 캐치한다.
  • Promise가 해결될 때까지 fallback UI를 보여준다.
  • Pomise가 해결되면 실제 컴포넌트를 렌더링한다.
import React, { Suspense } from 'react';
import { fetchData } from './api';

const DataComponent = () => {
  const data = fetchData(); // 이 함수가 Promise를 throw할 수 있음
  return <div>{data}</div>;
};

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

 

4. 성능 이점

여러 비동기 작업을 병렬로 처리할 수 있어 워터폴 효과를 방지한다.

필요한 데이터가 모두 준비될 때까지 렌더링을 지연시켜 불필요한 리렌더링을 줄인다.

 

5. 서버 사이드 렌더링(SSR)

React 18부터 지원하고 있다.

서버에서 일부 컴포넌트의 렌더링을 지연시키고, 클라이언트에서 나머지를 완성할 수 있다.

 

6. Error Boundary

Suspense만으로는 에러 처리가 불충분하므로 Error Boundary와 함께 사용해야 한다. 함께 사용하면, 로딩 중 발생하는 에러를 우아하게 처리할 수 있다.

 

7. 미래 전망

데이터 fetching, 라우팅 등 다양한 비동기 작업을 처리할 수 있게 만들 계획이다.

 

8. 한계 및 개선 사항

 


사용 예제

1. 기본적인 코드 스플리팅

import React, { Suspense } from 'react';

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

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

export default App;
  • React.lazy()를 사용하여 LazyComponent를 동적으로 import한다.
  • Suspense 컴포넌트로 LazyComponent를 감싼다.
  • LazyComponent가 로드되는 동안 "Loading..." 메시지가 표시된다.

이를 통해 초기 번들 크기를 줄이고, 필요한 때만 컴포넌트를 로드할 수 있다.

 

2. 데이터 페칭

import React, { Suspense } from 'react';
import { fetchUser } from './api';

const UserData = ({ id }) => {
  const user = fetchUser(id);
  return <div>{user.name}</div>;
};

function App() {
  return (
    <div>
      <h1>User Profile</h1>
      <Suspense fallback={<div>Loading user data...</div>}>
        <UserData id={1} />
      </Suspense>
    </div>
  );
}

export default App;
  • fetchUser 함수로 사용자 데이터를 비동기적으로 가져온다.
  • UserData 컴포넌트는 fetchUser를 호출하고 그 결과를 바로 사용한다.
  • Suspense는 데이터가 로드되는 동안 fallback UI를 보여준다.

React 18 이상에서 권장되는 데이터 페칭 방식이다.

 

3. 다중 Suspense 사용

import React, { Suspense } from 'react';

const Header = React.lazy(() => import('./Header'));
const Content = React.lazy(() => import('./Content'));
const Footer = React.lazy(() => import('./Footer'));

function App() {
  return (
    <div>
      <Suspense fallback={<div>Loading header...</div>}>
        <Header />
      </Suspense>
      
      <Suspense fallback={<div>Loading content...</div>}>
        <Content />
      </Suspense>
      
      <Suspense fallback={<div>Loading footer...</div>}>
        <Footer />
      </Suspense>
    </div>
  );
}

export default App;
  • 여러 개의 컴포넌트를 각각 lazy 로딩한다.
  • 각 컴포넌트마다 별도의 Suspense로 감싼다.

이렇게 하면 각 부분이 독립적으로 로드되고, 각각 다른 로딩 상태를 보여줄 수 있다. 다시 말해, 페이지의 일부가 로드되는 동안 다른 부분을 먼저 볼 수 있다.

 

4. ErrorBoundary와 함께 사용

import React, { Suspense } from 'react';
import ErrorBoundary from './ErrorBoundary';

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

function App() {
  return (
    <div>
      <h1>My App</h1>
      <ErrorBoundary fallback={<div>Error loading component!</div>}>
        <Suspense fallback={<div>Loading...</div>}>
          <LazyComponent />
        </Suspense>
      </ErrorBoundary>
    </div>
  );
}

export default App;
  • ErrorBoundary로 Suspnese와 LazyComponent를 감싼다.
  • 컴포넌트 로딩 중 오류가 발생하면 ErrorBoundary가 이를 잡아 오류 UI를 나타낸다.
  • 정상적인 로딩 중에는 Suspense의 fallback UI가 나타난다.

이 방식을 통해 로딩 상태와 오류 상태를 모두 우아하게 처리할 수 있다.


Suspense와 상태관리 라이브러리(Zustand, Jotai)

두 라이브러리들은 Suspense와의 호환성을 고려하여 설계되었다. 하지만 주의해야 할 점이 있다.

  • 성능 : Suspnese와 상태 관리를 함께 사용할 때 불필요한 리렌더링이 발생하지 않도록 주의해야 한다.
  • 에러 처리 : Suspense와 함께 사용할 때는 적절한 에러 바운더리를 설정하는 것이 중요하다.
  • SSR : 서버 사이드 렌더링을 사용할 때는 Suspense와 상태 관리의 상호작용을 신중히 고려해야 한다.

 

1. Zustand와 Suspense

Zustand는 기본적으로 Suspense를 지원하지 않는다. 하지만 둘을 함께 사용하는 패턴을 구현할 수 있다.

  • ex) Zustand 스토어 내에서 Promise를 관리하고, 이를 컴포넌트에서 사용할 때는 Suspense로 감싼다.
import create from 'zustand';
import { Suspense } from 'react';

const useStore = create((set) => ({
  data: null,
  fetchData: async () => {
    const response = await fetch('https://api.example.com/data');
    const data = await response.json();
    set({ data });
  },
}));

function DataComponent() {
  const data = useStore((state) => state.data);
  if (!data) throw useStore.getState().fetchData();
  return <div>{data}</div>;
}

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

 

2. Jotai와 Suspnese

Jotai는 Suspense를 네이티브하게 지원한다. Jotai의 atom을 사용하여 비동기 데이터를 관리하고, 이를 Suspense와 함께 사용할 수 있다. Jotail의 useAtom과 같은 훅이 있어 Suspense와 쉽게 통합할 수 있다.

import { atom, useAtom } from 'jotai';
import { Suspense } from 'react';

const dataAtom = atom(async () => {
  const response = await fetch('https://api.example.com/data');
  return response.json();
});

function DataComponent() {
  const [data] = useAtom(dataAtom);
  return <div>{data}</div>;
}

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