본문 바로가기
FRONT-END/React

Suspense 이해하기

by 랄라J 2024. 10. 8.

react 공식문서를 보고 정리한 글입니다.


Suspense란?

자식 요소가 로드되기 전까지 화면에 대체 UI를 보여주는 것

<Suspense fallback={<Loading />}>
    <SomeComponent />
</Suspense>
  • children: 궁극적으로 렌더링 하려는 실제 UI (위 코드에서는 SomeComponent)
  • fallback: 실제 UI가 로드되기 전까지 대신 렌더링 되는 대체 UI
  • React node 형식은 무엇이든 대체 UI로 활용가능하나, 보통 로딩 스피너나 스켈레톤처럼 간단한 placeholder를 활용한다.

 

Suspense는 children의 렌더링이 지연되면 자동으로 fallback으로 전환하고, 데이터가 준비되면 children으로 다시 전환한다.
fallback의 렌더링이 지연되면, 가장 가까운 부모 Suspense가 활성화된다.

React가 특정 컴포넌트를 마운트하기 전에, 비동기 작업이 완료될 때까지 해당 컴포넌트의 렌더링을 지연할 수 있다.

컴포넌트가 완전히 마운트되기 전까지는 state나 기타 로컬 상태를 관리하지 않는다.
따라서 이 시점의 state는 아직 존재하지 않거나 초기화되지 않은 상태다.

Suspense가 트리의 콘텐츠를 보여주고 있을 때 또다시 지연되면 startTransition이나 useDeferredValue로 인한 업데이트가 아닌 한 fallback이 다시 보인다.
→ startTransition: UI를 Blocking 않고, 상태를 업데이트할 수 있게 해 준다.
→ useDeferredValue: UI 일부 업데이트를 지연시킬 수 있는 React Hook이다.

React가 다시 일시 중지되어 보이는 콘텐츠를 숨겨야 하는 경우 콘텐츠 트리에서 layout Effect들을 정리한다.
콘텐츠가 다시 보일 준비가 되면 React는 layout Effect들을 다시 실행한다.

React는 Suspence와 통합된 Streaming Server Rendering와 Selective Hydration 같은 내부 최적화를 포함하고 있다.

 

 

사용법 1. 콘텐츠가 로드되는 동안 대체 UI 보여주기

애플리케이션의 모든 곳을 Suspense로 감쌀 수 있다.

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

 

Suspense 컴포넌트를 활성화시키는 것

1.Relay와 Next.js와 같이 Suspense가 가능한 프레임워크를 사용한 데이터 가져오기

2. lazy를 활용한 지연 로딩 컴포넌트

3. use를 사용해 Promise 값 읽기

Effect 또는 이벤트 핸들러 내부에서 가져오는 데이터를 감지하지 않는다.

 


lazy?

lazy는 로딩 중인 컴포넌트 코드가 처음으로 렌더링 될 때까지 연기할 수 있다.

lazy는 tree에 렌더링 할 수 있는 React 컴포넌트를 반환한다. 컴포넌트 코드가 여전히 로드되는 동안 렌더링을 시도하면 일시 중지된다.

로딩 중에 loading indicator를 표시하려면 Suspense를 사용한다.

const SomeComponent = lazy(load)

load: Promise 또는 또 다른 thenable을 반환하는 함수다.

React는 반환된 컴포넌트를 처음 렌더링하려고 할 때까지 load를 호출하지 않을 것이다.

React는 먼저 load를 실행한 후 load가 이행될 때까지 기다렸다가 이행된 값의. default를 React 컴포넌트로 렌더링 한다.

반환된 Promise와 Promise의 이행된 값이 모두 캐시 되므로 React는 load를 두 번 이상 호출하지 않는다.

Promise가 reject 되면 React는 가장 가까운 Error Boundary를 처리하기 위해 Error Boundary에 대한 거부 사유를 throw 한다.

import { lazy } from 'react'
const MarkdownPreview = lazy(() => import('./MarkdownPreview.js'));

항상 모듈의 최상위 수준에서 선언해야 한다.


use?

Promise나 context와 같은 데이터를 참조하는 React Hook

const value = use(resource);
import { use } from 'react'

function MessageComponent({ messagePromise }) {
	const message = use(messagePromise)
	const theme = use(ThemeContext);
	// ...

다른 React Hook과 달리 use는 if와 같은 조건문, 반복문 내부에서 호출이 가능함.

use는 컴포넌트 또는 Hook에서만 호출할 수 있음

Promise와 함께 호출될 때 use Hook은 Suspense 및 error boundaries와 통합됨.

use에 전달된 Promise가 pending 되는 동안 use를 호출하는 컴포넌트는 suspend 됨, reject 되면 가장 가까운 Error Boundary의 fallbakc이 표시됨

function HorizontalRule({ show }) {
	if (show) {
		const theme = use(ThemeContext);
		return <hr className={theme} />
	}
	return false
}

 

사용법 2. 콘텐츠를 한꺼번에 함께 보여주기

기본적으로 Suspense 내부의 전체 트리는 하나의 단위로 취급되기 때문에, 하위 구성 요소 중 하나라도 어떤 데이터에 의해 지연되면 모든 구성요소가 함께 로딩 표시로 대체된다. 모두 표시될 준비가 되면 한꺼번에 함께 보인다.

<Suspense fallback={<Loading />}>
    <A />
    <B />
</Suspense>

 

사용법 3. 중첩된 콘텐츠가 로드될 때 보여주기

컴포넌트가 일시 중단되면 가장 가까운 상위 Suspense 컴포넌트가 Fallback을 보여준다.

이를 활용해 여러 Suspense 컴포넌트를 중첩해 로딩 순서를 만들 수 있다.

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

위와 같은 코드의 경우 Albums 컴포넌트가 로드될 때까지 Biography는 기다리지 않아도 된다.

Biography가 로드되기 전이라면 가 표시되고, Biography가 로드되면 는 보인다. Albums이 로드되기 전이라면 가 표시되고 로드되면 보여진다.

이러한 방식으로 Suspense를 활용하면 UI의 어떤 부분이 동시에 그려져야 하는지 어떤 부분이 로딩 순서에서 점진적으로 더 많은 콘텐츠를 보여줘야 하는지를 조정할 수 있다.

 

사용법 4. 새 콘텐츠가 로드되는 동안 이전 콘텐츠 보여주기

검색하는 창을 예로 들어 a를 검색하고, ab를 검색하는 과정에서 loading이 표시되는 것이 아니라 이전 a의 검색결과를 표시하는 것을 대체 UI 패턴이라고 한다.

대체 UI 패턴은 목록들에 대한 업데이트를 연기하고 새 결과가 준비될 때까지 이전 결과를 계속 보여주는 것이다.

useDeferredValue Hook을 사용하면 쿼리의 지연된 버전을 아래로 전달할 수 있다.

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

query는 즉시 업데이트되므로 입력창에 새 값이 표시되지만, deferredQuery는 데이터가 로드될 때까지 이전 값을 유지하므로 SearchResults는 잠시 동안 이전 결과를 보여준다.

<div style={{
  opacity: query !== deferredQuery ? 0.5 : 1 
}}>
  <SearchResults query={deferredQuery} />
</div>

위처럼 값을 비교하는 처리로 시각적 표시를 추가할 수 있다.

 

사용법 5. 이미 보인 콘텐츠가 숨겨지지 않도록 방지

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

위 코드의 경우 Suspense가 App 즉 Root에 있어 navigate가 되면 page가 변하는 동안 보여주던 페이지가 모두 사라지고 BigSpinner가 보이게 된다.

이를 방지하기 위해 startTransition을 사용해 navigation state 업데이트를 Transition으로 처리할 수 있다.

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

이는 State 전환이 급하지 않으며, 이미 공개된 콘텐츠를 숨기는 대신 이전 페이지를 계속 표시하는 것이 좋다고 React에게 알려준다.

 

사용법 6. Transition이 발생하고 있음을 보여주기

표시기를 추가하려면 startTransition을 boolean 값인 isPending 값을 제공하는 useTransition으로 바꾸면 된다. isPending을 이용해 style을 추가할 수 있다.

const [isPending, startTransition] = useTransition();

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

 

사용법 7. Navigation에서 Suspense 재설정하기

Transition이 진행되는 동안 React는 이미 보인 콘텐츠를 숨기지 않는다.

다른 매개변수가 있는 경로로 이동하는 경우 React에 다른 콘텐츠라고 알려주고 싶다면 key로 표현할 수 있다.

 

사용법 8. 서버 에러 및 서버 전용 콘텐츠에 대한 Fallback 제공

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

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

 

 

사실 이 Suspense를 알아보게 된 계기는 useQuery, useSuspenseQuery의 차이를 알기 전 Suspense가 뭔지 다시 한번 짚고 넘어가자는 의도였는데, 생각보다 알게 된 부분이 많아 한번 꼼꼼히 읽어보기 잘했다고 생각했다 :-)

 

마무리하며, 원래 알아보려 했던 것도 적어두자면

useQuery와 useSuspenseQuery의 차이는 간단하다.

useSuspenseQuery는 useQuery와 option과 return 값이 유사하다.
options에서는 throwOnError, enabled, placeholderData를 가지지 않는다는 점이 다르고, 반환 값에서는 useQuery와 동일한 객체지만 data가 정의됨이 보장이 되고 isPlaceholderData가 없고, status가 항상 success라는 점이 다르다.

즉, 요청이 아직 로딩 중인 상태라면 Suspense 컴포넌트를 트리거하고, 컴포넌트에 전달되는 data는 로드가 완료된 상태이기 때문에 data의 유효성이나 status의 상태 등을 관리할 필요가 없다는 점이 다르다.

728x90

댓글