Frontend/React

[React] Suspense

khakhalog 2023. 10. 31. 09:54

Suspense

React.lazy와 함께 사용하면 가져오기가 로드되는 동안 폴백 로딩 상태를 렌더링하도록 리액트에 지시할 수 있다.

props

- children : 렌더링하려는 실제 UI

- fallback : loading 상태일 때, 실제 UI 대신 렌더링할 대체 UI. 보통 로딩 스피너나 스켈레톤 UI로 대체한다.

 

children 컴포넌트가 로딩이 완료될 때까지 fallback 컴포넌트를 보여준다. 

아래와 같이 여러 Suspense 구성 요소를 중첩하여 로딩 시퀀스를 만들 수도 있다.

<Suspense fallback={<BigSpinner />}>
  <Biography />
  <Suspense fallback={<AlbumsGlimmer />}>
    <Panel>
      <Albums />
    </Panel>
  </Suspense>
</Suspense>

1. <Biography /> 가 loading 상태이면, 전체 콘텐츠 영역이 <BigSpinner /> 로 대체된다.

2. <Biography /> loading이 완료되면 <BigSpinner /> 는 사라지고, 로딩이 완료된 컨텐츠가 렌더링 된다.

3. <Albums />가 loading 상태이면, <Albums /> 와 <Panel /> 영역이 <AlbumsGlimmer /> 로 대체된다.

4. <Albums /> loading이 완료되면, <AlbumsGlimmer/> 는 사라지고, 로딩이 완료된 컨텐츠가 렌더링 된다.

startTransition(scope)

startTranstion으로 래핑된 상태 업데이트는 전환 업데이트로 처리되며, 긴급한 업데이트가 들어오면 중단된다.

- urgent updates (긴급한 업데이트) : 입력, 클릭 등과 같은 직접적인 상호작용 반영

- transition updates (전환 업데이트) : UI의 전환

usage

사용법은 간단. setState 함수를 래핑하여 사용한다.

import { startTransition } from 'react';
 
function TabContainer() {
  const [tab, setTab] = useState('about');
  const [isPending, startTransition] = useTransition(); // pending 상태인지 여부를 알려주는 boolean 타입의 isPending 값으로 스타일을 적용할 수도 있다.
 
  function selectTab(nextTab) {
    startTransition(() => {
      setTab(nextTab);
    });
  }
  // ...
}

with Suspense

사용자의 어떠한 동작에 의해 구성요소가 로딩되면 가장 가까운 Suspense 경계가 fallback 컴포넌트로 대체되면서 이미 일부 표시되던 컨텐츠가 안보이게 되는 현상이 발생하기도 한다.

이러한 현상을 startTransition으로 래핑하여 방지할 수 있다.

  • App.js
import { Suspense, useState } from 'react';
import IndexPage from './IndexPage.js';
import ArtistPage from './ArtistPage.js';
import Layout from './Layout.js';
 
export default function App() {
  return (
    <Suspense fallback={<BigSpinner />}>
      <Router />
    </Suspense>
  );
}
 
function Router() {
  const [page, setPage] = useState('/');
 
  function navigate(url) {
    setPage(url);
  }
 
  let content;
  if (page === '/') {
    content = (
      <IndexPage navigate={navigate} />
    );
  } else if (page === '/the-beatles') {
    content = (
      <ArtistPage
        artist={{
          id: 'the-beatles',
          name: 'The Beatles',
        }}
      />
    );
  }
  return (
    <Layout>
      {content}
    </Layout>
  );
}
 
function BigSpinner() {
  return <h2>🌀 Loading...</h2>;
}
  • IndexPage.js
export default function IndexPage({ navigate }) {
  return (
    <button onClick={() => navigate('/the-beatles')}>
      Open The Beatles artist page
    </button>
  );
}
  • ArtistPage.js
import { Suspense } from 'react';
import Albums from './Albums.js';
import Biography from './Biography.js';
import Panel from './Panel.js';
 
export default function ArtistPage({ artist }) {
  return (
    <>
      <h1>{artist.name}</h1>
      <Biography artistId={artist.id} />
      <Suspense fallback={<AlbumsGlimmer />}>
        <Panel>
          <Albums artistId={artist.id} />
        </Panel>
      </Suspense>
    </>
  );
}
 
function AlbumsGlimmer() {
  return (
    <div className="glimmer-panel">
      <div className="glimmer-line" />
      <div className="glimmer-line" />
      <div className="glimmer-line" />
    </div>
  );
}

1. /the-beatles 로 이동하면 <Router /> 렌더링

2. <Biography /> 가 loading 상태이면 가장 상위 Suspense 경계인 <BigSpinner /> 로 먼저 대체된다.

3. <Biography /> loading 완료, <Albums /> 가 loading 중일 때, <AlbumsGlimmer /> 로 대체된다.

4. <Album /> loading 완료되면, <Albums /> 표시

위 과정에서 2번 과정은 이미 보였던 컨텐츠가 스피너로 대체되기 때문에 사용자에게 불편함을 야기할 수 있다.

 

startTranstion으로 해결해보자.

// startTransition 적용한 App.js
function Router() {
  const [page, setPage] = useState('/');
 
  function navigate(url) {
    startTransition(() => {
      setPage(url);     
    });
  }
  // ...

위와 같이 setPage를 startTranstion으로 래핑해주면, <Biography /> 가 로딩중일 때, <BigSpinner />로 컨텐츠를 가려버리지 않고, 로딩완료 될때까지 기다린다.

<Albums />를 감싸고 있는 Suspense 경계는 새로운 컨텐츠이기 때문에 transition이 기다리지 않고, <AlbumsGlimmer/>를 보여준다.

 

💡 startTransition은 모든 컨텐츠가 로드될 때까지 기다리는 것이 아니고,이미 보여진 컨텐츠를 숨기지 않을만큼 기다린다.