<Suspense>는 자식 요소가 로드되기 전까지 화면에 대체 UI를 보여준다.
<Suspense fallback={<Loading />}>
<SomeComponent />
</Suspense>
레퍼런스
Props
- children : 궁극적으로 렌더링하려는 실제 UI. children의 렌더링이 지연되면, Suspense는 fallback을 대신 렌더링한다..
- fallback : 실제 UI가 로드되기 전까지 대신 되는 대체 UI다. 보통 스피너나 스켈레톤처럼 간단한것을 사용한다.
주의사항
- React는 컴포넌트가 처음으로 마운트 되기 전에 지연된 렌더링을 하는 동안 어떤 state도 유지하지 않는다.
- Suspense가 트리의 콘텐츠를 보여주고 있을 때 또다시 지연되면 startTransition나 useDeferredValue로 인한 업데이트가 아닌 한, fallback이 다시 보인다.
사용법
콘텐츠가 로드되는 동안 대체 UI 보여주기
애플리케이션의 모든 곳을 Suspense 경계로 감쌀 수 있다.
<Suspense fallback={<Loading />}>
<Albums />
</Suspense>
Suspense가 가능한 데이터만이 서스펜스 컴포넌트를 활성화 한다.
- Relay와 next.js같이 suspense가 가능한 프레임워크를 사용한 데이터 가져오기
- lazy를 활용한 지연 로딩 컴포넌트
- use를 사용해서 Promise 값 읽기
위같은 경우 해당된다.
콘텐츠를 한꺼번에 함께 보여주기
기본적으로 Suspense 내부의 전체 트리는 하나의 단위로 취급한다.
이러한 구성 요소 중 하나라도 어떤 데이터에 의해 지연되더라도 모든 구성 요소가 함께 로딩 표시도 대체된다.
<Suspense fallback={<Loading />}>
<Biography />
<Panel>
<Albums />
</Panel>
</Suspense>
그런 다음 모두 보일 준비가 되면 한꺼번에 보이게 된다.
중첩된 콘텐츠가 로드될 때 보여 주기
컴포넌트가 일시 중단되면 가장 가까운 상위 Suspense 컴포넌트가 Fallback을 보여준다.
이를 통해 여러 Suspense 컴포넌트를 중첩하여 로딩 순서를 만들 수 있다.
<Suspense fallback={<BigSpinner />}>
<Biography />
<Suspense fallback={<AlbumsGlimmer />}>
<Panel>
<Albums />
</Panel>
</Suspense>
</Suspense>
새 콘텐츠가 로드되는 동안 이전 콘텐츠 보여주기
이 예제에서는 검색 결과를 가져오는 동안 SearchResults 컴포넌트가 지연된다.
import { Suspense, useState } from 'react';
import SearchResults from './SearchResults.js';
export default function App() {
const [query, setQuery] = useState('');
return (
<>
<label>
Search albums:
<input value={query} onChange={e => setQuery(e.target.value)} />
</label>
<Suspense fallback={<h2>Loading...</h2>}>
<SearchResults query={query} />
</Suspense>
</>
);
}
일반적인 대체 UI 패턴은 목록들에 대한 업데이트를 연기하고 새 결과가 준비될 때까지 이전 결과를 계속 보여주는 것이다.
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>
이렇게 하면 a를 입력하고 결과가 나오길 기다렸다 ab로 바꾸면 이전 결과 목록이 투명하게 표시되는 것을 확인할 수 있다.
이미 보인 콘텐츠가 숨겨지지 않도록 방지
컴포넌트가 지연되면 가장 가까운 상위 Suspense가 Fallback을 보여주도록 전환한다.
이미 일부 콘텐츠가 보이는 경우 사용자 경험이 끊길 수 있다.
import { Suspense, startTransition, 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) {
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 (
<Layout>
{content}
</Layout>
);
}
function BigSpinner() {
return <h2>🌀 Loading...</h2>;
}
Transition이 발생하고 있음을 보여주기
위의 예시에서는 버튼을 클릭해도 navigation이 진행 중이라는 시각적 표시가 없습니다.
표시기를 추가하려면 startTransition을 boolean 값인 isPending 값을 제공하는 useTransition으로 바꾸면 된다.
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);
});
}
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>;
}
Navigation에서 Suspense 재설정 하기
Transition이 진행되는 동안 React는 이미 보인 콘텐츠를 숨기지 않는다. 하지만 다른 매개변수가 있는 경로로 이동하는 경우 React에 다른 콘텐츠라고 알려주고 싶을 수 있다.이를 key로 표현할 수 있다.
<ProfilePage key={queryParams.id} />
서버 에러 및 서버 전용 콘텐츠에 대한 Fallback 제공
스트리밍 서버 렌더링 API 중 하나(또는 이에 의존하는 프레임워크)를 사용하는 경우, React는 서버의 에러를 처리하기 위해 <Suspense> 바운더리도 사용할 것입니다. 컴포넌트가 서버에서 에러를 발생시키더라도 React는 서버 렌더링을 중단하지 않습니다. 대신, 그 위에 있는 가장 가까운 <Suspense> 컴포넌트를 찾아서 그 Fallback(예: 스피너)을 생성된 서버 HTML에 포함합니다. 사용자는 처음에는 스피너를 보게 됩니다.
클라이언트에서 React는 동일한 컴포넌트를 다시 렌더링하려고 시도합니다. 클라이언트에서도 에러가 발생하면 React는 에러를 던지고 가장 가까운 error boundary를 표시합니다. 그러나 클라이언트에서 에러가 발생하지 않으면 콘텐츠가 결국 성공적으로 보였기 때문에 React는 사용자에게 에러를 보여주지 않습니다.
이를 사용하여 일부 컴포넌트를 서버에서 렌더링하지 않도록 선택할 수 있습니다. 이렇게 하려면 서버 환경에서 에러를 발생시킨 다음 <Suspense> 바운더리로 감싸서 해당 HTML을 Fallback으로 대체합니다.
<Suspense fallback={<Loading />}>
<Chat />
</Suspense>
function Chat() {
if (typeof window === 'undefined') {
throw Error('Chat should only render on the client.');
}
// ...
}
문제 해결
업데이트 중에 UI가 Fallback으로 대체되는 것을 방지하려면 어떻게 해야 하나?
표시되는 UI를 Fallback으로 대체하면 사용자 환경이 불안정해진다.
이는 업데이트로 인해 컴포넌트가 지연되고 가장 가까운 Suspense가 이미 사용자에게 콘텐츠를 보여주고 있을 때 발생할 수 있다.
이런일이 발생하지 않도록 하려면, startTransition을 사용해 업데이트를 긴급하지 않은 것으로 처리해야 한다.
function handleNextPageClick() {
// If this update suspends, don't hide the already displayed content
startTransition(() => {
setCurrentPage(currentPage + 1);
});
}
이렇게 하면 기존 컨텐츠가 숨겨지지 않는다. 그러나 새로 렌더링 된 Suspense는 여전히 즉시 Fallback을 보여줘서 UI를 차단하지 않고 사용자가 컨텐츠를 이용할 수 있게 된다.
React는 긴급하지 않은 업데이트 중에만 원치 않는 Fallback을 방지한다.
긴급한 업데이트의 결과인 경우 렌더링을 지연시키지 않는다.
startTransition 또는 useDeferredValue와 같은 API를 사용해야 한다.
Router가 Suspense와 통합인 경우, router는 업데이트를 자동으로 startTransition에 래핑해야 한다.
'리액트 공식문서 읽어보기' 카테고리의 다른 글
| useDebugValue (0) | 2024.03.03 |
|---|---|
| useCallback (0) | 2024.03.02 |
| lazy (0) | 2024.02.25 |
| useState (0) | 2024.02.24 |
| useRef (0) | 2024.02.23 |