본문 바로가기

이것저것 스터디📚/React - 공식 문서

React 공식문서 -<Suspense>

- <Suspnese>를 사용하면 자식이 로딩을 완료할 때까지 폴백을 표시할 수 있다.

<Suspense fallback={<Loading />}>
  <SomeComponent />
</Suspense>

Reference

Props

- children : 렌더링하려는 실제 UI이다. 렌더링하는 동안 children이 일시 중단되면 Suspense 경계가 fallback 렌더링으로 전환된다.

- fallback : 로딩이 완료되지 않은 경우에 실제 UI 대신 렌더링할 대체 UI이다. 유효한 어떤 React 노드든 상관없다.

- Suspense는 children이 일시 중단되면 자동으로 fallback으로 전환되고, 데이터가 다시 준비되면 다시 children으로 전환된다.

- 렌더링 중에 fallback이 일시 중단되면 가장 가까운 상위 Suspense 경계가 활성화된다.

   -> "가장 가까운(부모) Suspense 경계를 활성화시킬 수 있다"는 말은 Suspense 경계가 중첩되어 있을 때, 하위 경계에서 발생한 `fallback` 상태가 상위 경계에 영향을 미칠 수 있다는 것을 의미한다.

다시 말해, 자식 컴포넌트에서 발생한 `fallback` 상태가 상위 컴포넌트로 전파될 수 있고, 상위 컴포넌트도 `fallback` 상태로 전환될 수 있다는 것을 의미합니다. 

 

즉, 만약 하위 컴포넌트에서 데이터를 로딩하고 그 로딩이 실패하거나 또는 다른 이유로 인해 `fallback` 상태로 전환된다면, 상위 컴포넌트도 이를 감지하고 자체적으로 `fallback` 상태로 전환할 수 있다. 이런 방식으로 사용자에게 더 나은 로딩 및 에러 처리를 제공할 수 있다.

 

Caveats(주의사항)

- React는 처음 마운트하기 전에 일시 중단되 렌더링의 state를 보존하지 않는다. 즉, 컴포넌트가 로드되면 React는 일시 중단된 트리의 렌더링을 처음부터 다시 시도한다.


Revealing content together at once(콘텐츠를 한 번에 드러내기)

- Suspense 내부의 전체 트리는 기본적으로 단일 단위로 취급된다.

- 내부 트리 구조의 컴포넌트 중 하나만 데이터 대기를 위해 일시 중단하더라도 모든 컴포넌트가 함께 로딩 표시기로 대체된다.

- 데이터를 로드하는 컴포넌트가 Suspense 경계의 직접적인 자식일 필요는 없다.

<Suspense fallback={<Loading />}>
  <Details artistId={artist.id} />
</Suspense>

function Details({ artistId }) {
  return (
    <>
      <Biography artistId={artistId} />
      <Panel>
        <Albums artistId={artistId} />
      </Panel>
    </>
  );
}

- 위 예제에서, Biography와 Albums 컴포넌트가 데이터를 로딩하는 컴포넌트라고 가정했을 때 Detail 컴포넌트의 자식, 즉 Suspense의 경계의 직접적인 자식이 아니더라도, Suspense의 fallback은 동작한다.


Revealing nested content as it loads(중첩된 콘텐츠가 로드될 때 표시하기)

- Suspense의 경우, 트리 구조내의 컴포넌트가 일시 중단되면 가장 가까운 Suspense 컴포넌트의 fallback이 표시된다.

- 따라서, 중첩 Suspense 컴포넌트를 사용하면 로딩 시퀀스를 만들수 있다.

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

- 위 예제 코드의 실행 순서를 보자.

- 1. Biography 컴포넌트가 아직 로드되지 않은 경우, 전체 콘텐츠의 영역을 대신해서 BigSpinner 컴포넌트가 표시된다.

- 2. Biography 컴포넌트가 로드되면, 화면에 표시되고, 만약 Album 컴포넌트가 로드되지 않았다면, 전체 콘텐츠 영역에서 Biography 영역을 제외하고 Panel, Albums 컴포넌트 대신 AlbumsGlimmer 컴포넌트가 표시된다.

- 3. Albums 컴포넌트가 로드되면, 전체 컨텐츠 영역이 표시된다.

 

- Suspense의 경계를 활용하면, UI의 어떤 부분이 항상 동시에 표시되는지, 로딩 상태의 시퀀스에서 점진적으로 더 많은 콘텐츠를 표시해야 하는지 조정할 수 있다.

- 다만, Suspense 경계는 사용자가 경험하게 될 로딩 시퀀스에 따라 사용해야 하는 것을 지향하며, 모든 컴포넌트에서 Suspense 경계를 설정하는 것은 지양하자.


Showing stale content while fresh content is loading(새 콘텐츠가 로드되는 동안 오래된 콘텐츠 표시하기)

- 일반적인 대체 UI 패턴에서는 새로운 목록을 업데이트 하는 경우, 새로운 목록이 준비가 될때까지 이전 결과를 계속 표시한다.

- 이때, useDeferredValue 훅을 사용하면 쿼리의 연기된(?) 버전을 전달할 수 있다.

- useDeferredValue 훅 정리(추후 업데이트 예정)

export default function App() {
  const [query, setQuery] = useState('');
  const deferredQuery = useDeferredValue(query);
  return (
    <>
      <label>
        Search albums:
        <input value={query} onChange={e => setQuery(e.target.value)} />
      </label>
      <Suspense fallback={<h2>Loading...</h2>}>
        <SearchResults query={deferredQuery} />
      </Suspense>
    </>
  );
}

- 위 예제 코드에서, input에 새로운 값이 표시되기 전까지 fallback의 <h2>태그가 아닌 deferredQuery가 이전 값을 유지하기 때문에 SearchResults 컴포넌트는 잠시동안 이전 결과를 표시한다.


Preventing already revealed content from hiding(이미 표시된 콘텐츠가 숨겨지지 않도록 방지하기)

- 컴포넌트가 일시중지 되어 가장 가까운 상위 Suspense의 fallback으로 전환될 때, 이미 일부 콘텐츠가 표시되는 경우에는 사용자 경험이 끊길 수가 있다.

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>;
}

- 위 예제에서는 버튼 클릭시에 Router 컴포넌트가 IndexPage 대신 ArtistPage를 렌더링하면서, ArtistPage 내부 컴포넌트가 일시중지 되고 가장 가까운 Suspense 경계가 fallback을 표시한다. 이때, Layout 컴포넌트 역시 BigSpinner로 대체된다.

function Router() {
  const [page, setPage] = useState('/');

  function navigate(url) {
    startTransition(() => {
      setPage(url);      
    });
  }
  // ...

- 이때 위와 같이 startTransition을 사용하여 탐색 state 업데이트를 트랜지션으로 표시할 수 있다.

- startTransition : UI를 차단하지 않고 state를 업데이트할 수 있다(비동기 작업을 수행할 때 사용)(추후 정리 업데이트 예정)

- 즉, React에게 state의 전환(setPage)가 긴급하지 않으며 이미 표시된 콘텐츠를 숨기는 대신 이전 페이지를 계속 표시하는 것이 낫다고 알려주는 방법이다.

- 트랜지션은 모든 콘텐츠가 로드될 때까지 기다리는 것이 아닌, 이미 표시된 콘텐츠가 숨겨지지 않을 만큼만 기다린다.

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>
  );
}

- 즉, 위 예제에서 버튼이 눌리고 Albums 컴포넌트가 로드되기 전까지 화면에 표시되는 것은 ArtistPage 컴포넌트 내부의 Suspense의 fallback이 되는 것이다.(BigSpinner가 아니다)


Indicating that a transition is happening(트랜지션이 발생하고 있음을 나타내기)

- useTransition 훅의 isPending이라는 불리언 값을 사용하면, 트랜지션이 진행되는 동안 "진행중"이라는 것을 사용자에게 시각적으로 보여줄 수 있다.

import { Suspense, useState, useTransition } 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('/');
  const [isPending, startTransition] = useTransition();
  // useTransition 사용

  function navigate(url) {
    startTransition(() => {
      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 (
  // isPending 사용
    <Layout isPending={isPending}>
      {content}
    </Layout>
  );
}

function BigSpinner() {
  return <h2>🌀 Loading...</h2>;
}

// Layout.js
export default function Layout({ children, isPending }) {
  return (
    <div className="layout">
      <section className="header" style={{
      // isPending 값에 따른 시각적 표시
        opacity: isPending ? 0.7 : 1
      }}>
        Music Browser
      </section>
      <main>
        {children}
      </main>
    </div>
  );
}

 


Resetting Suspense boundaries on navigation(탐색시 Suspense 경계 재설정하기)

- key를 사용하면, React가 서로 다른 컴포넌트로 취급하고 탐색 중에 Suspense 경계를 재설정하도록 할수 있다.

- 예를들어, 어떤 사용자의 프로필 페이지를 둘러보던 중 무언가가 일시 중단되었을 때, 해당 업데이트가 트랜지션으로 감싸져 있으면 이미 표시된 콘텐츠의 fallback은 촉발되지 않을 것이다.

- 하지만, 두 개의 서로 다른 사용자 프로필 사이를 탐색하고 있을 때, 다른 사용자의 프로필을 탐색한다면 fallback을 표시해야하는데, 이때 key를 사용할 수 있다.

<ProfilePage key={queryParams.id} />

Providing a fallback for server errors and server-only content(서버 오류 및 서버 전용 콘텐츠에 대한 폴백 제공하기)

- 스트리밍 서버 렌더링 API 중하나를 사용하는 경우, React는 서버에서 발생하는 오류를 처리하기 위해 Suspense 바운더리를 사용한다.

- 이때, React는 서버 렌더링을 중지시키지 않고 Suspense 컴포넌트의 fallback을 렌더링한다.

- 클라이언트에서는 React의 동일한 컴포넌트를 다시 렌더링하려고 시도하고, 만약 클라이언트에서도 에러가 발생하면 React는 에러를 던지고 가장 가까운 에러 경계를 표시한다.


How do I prevent the UI from being replaced by a fallback during an update?(업데이트 중에 UI가 폴백으로 대체되는 것을 방지하려면 어떻게 해야 할까요?)

- 표시되는 UI를 fallback으로 대체하면 사용자 환경이 불안정하기 때문에, startTransition을 활용하면 충분한 데이터가 로드될 때까지 기다릴 수 있다.


* 참고 : React 공식문서(https://react-ko.dev/learn)

'이것저것 스터디📚 > React - 공식 문서' 카테고리의 다른 글

React 공식문서 -useRef  (0) 2023.09.20
React 공식문서 -lazy  (0) 2023.09.14
React 공식문서 -memo  (0) 2023.09.07
React 공식문서 -useContext  (0) 2023.09.06
React 공식문서 -useReducer  (0) 2023.09.06