준영이의 성장일기(FrontEnd)

React 공식문서 도장깨기(8) - Suspense 컴포넌트 본문

프론트엔드/React.js

React 공식문서 도장깨기(8) - Suspense 컴포넌트

Jay_Jung 2024. 9. 9. 15:25

 

꾸준하게 공부 겸 블로그를 작성하다보니 어느 덧 100번째 게시물을 쓰게 됐다. 이렇게 꾸준히 작성하다보면 언젠간 200개 달성도 가능할 것이라 생각한다. 이번에 정리할 내용은 사용자 경험 개선과 관련이 있는 Suspense 컴포넌트에 대해서 정리하고자 한다. Skeleton 컴포넌트를 활용해보면서 사용자 경험 개선에 관심이 생겨서 해당 개념도 공부를 하는데 술술 잘 읽혔다.

 

 

<참고 레퍼런스>

 

https://react-ko.dev/reference/react/Suspense

 

<Suspense> – React

The library for web and native user interfaces

react-ko.dev

 

 

<Suspense>

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

 

개념: Suspense는 자식 컴포넌트의 로딩이 완료될 때 까지 대체 UI를 보여주는 것을 의미한다. 렌더링하는 동안 children(SomeComponent)이 일시 중단되면 Suspense 경계가 fallback 렌더링으로 전환된다. 또한 렌더링 중에 fallback이 일시 중단되면 가장 가까운 상위 Suspense 경계가 활성화된다. 

 

 

<Suspense의 콘텐츠 한번에 보여주기>

기본적으로 Suspense는 내부 컴포넌트들을 하나의 단위로 생각한다. 코드를 보면 Biography, Panel 컴포넌트가 있는데 두 컴포넌트 중 하나라도 로드가 안됐다면 fallback으로 전환된다. 항상 동시에 함께 “등장”한다고 생각하자. 그리고 무조건 직접적인 자식 관계가 아니여도 된다. 즉 계층적인 구조로 되어있어도 가장 가까운 상위 Suspense 경계를 공유하므로 표시 여부가 함께 조정된다.

<Suspense fallback={<Loading />}>
  <Biography />
  <Panel>
    <Albums />
  </Panel>
</Suspense>

 

 

<Suspense의 중첩 활용>

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

 

Suspense 컴포넌트를 중첩하여 Panel, Albums 컴포넌트 한정으로 Suspense를 적용할 수 있다. 이렇게 적용하면 Bigraphy 컴포넌트 입장에서 Panel, Albums 컴포넌트에 관계없이 BigSpinner UI가 fallback으로 렌더링 될 수 있다.

 

 

  항상 동시에 함께 “등장”한다고 생각하자. 

 

이처럼 어떻게 중첩하느냐에 따라서 어떤 부분이 항상 동시에 “등장”해야 하는지, 어떤 부분은 생략해도 되는지를 결정할 수 있다.

 

 

<새 콘텐츠가 로드되는 동안 오래된 콘텐츠 표시하기>

만약 특정 단어들을 검색한다고 가정해보자. 준영을 입력한 후 준영2를 입력 할 경우 만약 Suspense가 적용되어 있다면 새로 데이터가 로드될 때까지 fallback에 작성 한 UI가 렌더링 될 것이다. 하지만 대체로 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>
    </>
  );
}

 

 

<useTransition과 Suspense의 콜라보>

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

  function navigate(url) {
    startTransition(() => {
      setPage(url);
    });
  } -> startTransition을 통해 
  Suspense의 Loading 폴백을 막아주고 변경되지 않는 시간만큼 대기가 가능(이전 콘텐츠 보임) 

  let content;
  if (page === '/') {
    content = (
      <IndexPage navigate={navigate} />
    );
  } else if (page === '/the-beatles') {
    content = (
      <ArtistPage
        artist={{
          id: 'the-beatles',
          name: 'The Beatles',
        }}
      />
    );
  }
  return (
    <Layout isPending={isPending}>
      {content}
    </Layout>
  );
}

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

 

useTransition의 isPending 속성을 사용해서 트렌지션이 진행되는 동안 웹 사이트의 헤더 스타일을 조정하는 코드이다. isPending 상태라면 투명도를 0.7로 하여 사용자에게 탐색이 진행 중이라는 시각적 표시를 해줄 수 있다. 

export default function Layout({ children, isPending }) {
  return (
    <div className="layout">
      <section className="header" style={{
        opacity: isPending ? 0.7 : 1
      }}>
        Music Browser
      </section>
      <main>
        {children}
      </main>
    </div>
  );
}

 

<서버 오류 및 클라이언트 전용 콘텐츠에 대한 폴백 제공하기>

공식문서에서 해당 챕터를 읽으면서 느낀 점은 해당 내용은 React에서 서버 사이드 렌더링(SSR) 또는 클라이언트 사이드 렌더링(CSR)을 사용할 때, 서버에서 오류가 발생하거나 클라이언트에서만 필요한 콘텐츠를 처리할 수 있는 전략과 관련된 개념임을 알 수 있었다. 

 

서버에서 오류가 발생할 경우 페이지가 완전히 렌더링되지 않을 수 있기 때문에, 이러한 경우 폴백(fallback) 처리가 필요하고 위에 있는 가장 가까운 <Suspense> 컴포넌트를 찾아서 생성된 서버 HTML에 그 폴백(예: 스피너)을 포함시킨다.  

 

또는 일부 컴포넌트를 서버에 렌더링 안되도록 할 수 있는데 서버 환경에서 오류를 발생시킨 다음 해당 컴포넌트를 <Suspense> 경계로 감싸서 해당 HTML을 폴백으로 전환 시키면 된다.

 

<Suspense fallback={<Loading />}> -> 오류발생 시 Loading 컴포넌트로 전환
  <Chat /> -> 서버 환경에서 오류발생
</Suspense>

function Chat() {
  if (typeof window === 'undefined') {
    throw Error('Chat should only render on the client.');
  }
  // ...
}

 

<다음 블로그 주제>

https://react-ko.dev/reference/react/useDeferredValue

 

useDeferredValue – React

The library for web and native user interfaces

react-ko.dev