<Suspense>를 사용하면 자식의 로딩이 완료될 때까지 fallback을 표시할 수 있음.
<Suspense fallback={<Loading />}>
<SomeComponent />
</Suspense>
<Suspense>children: 렌더링하려는 실제 UI. 렌더링하는 동안 children이 일시 중단되면 Suspense boundary가 렌더링 fallback으로 전환됨.
fallback: 로딩이 완료되지 않은 경우 실제 UI 대신 렌더링할 대체 UI. 유효한 모든 React 노드를 사용할 수 있지만, 실제로 fallback은 로딩 스피너나 스켈레톤과 같은 경량 placeholder 뷰임. Suspense는 children이 일시 중단되면 자동으로 fallback으로 전환되고, 데이터가 준비되면 다시 children으로 전환됨. 렌더링 중에 fallback이 일시 중단되면 가장 가까운 상위 Suspense boundary가 활성화됨.
?React는 처음으로 mount가 가능하기 전에 일시 중단된 렌더링의 state를 보존하지 않음.? 컴포넌트가 로드되면 React는 일시 중단된 트리의 렌더링을 처음부터 다시 시도함.
Suspense가 트리의 콘텐츠를 표시하다가 다시 일시 중단된 경우, 그 원인이 startTransition 또는 useDeferredValue로 인한 업데이트가 아닌 한 fallback이 다시 표시됨.
React가 다시 일시 중단되어 이미 표시된 콘텐츠를 숨겨야 하는 경우, 콘텐츠 트리에서 layout Effects를 cleanup 함. 콘텐츠가 다시 표시될 준비가 되면 React는 layout Effects를 다시 실행함. 이렇게 하면 콘텐츠가 숨겨져 있는 동안 DOM 레이아웃을 측정하는 Effects가 이 작업을 시도하지 않음.
React에는 스트리밍 서버 렌더링 및 선택적 Hydration과 같은 내부 최적화가 포함되어 있으며, 이는 Suspense와 통합되어 있음.
애플리케이션의 어느 부분이든 Suspense boundary로 감쌀 수 있음:
<Suspense fallback={<Loading />}>
<Albums />
</Suspense>
React는 자식에게 필요한 모든 코드와 데이터가 로드될 때까지 loading fallback을 표시함.
아래 예시에서는 앨범 목록을 가져오는 동안 Albums 컴포넌트가 일시 중단됨. 렌더링할 준비가 될 때까지 React는 가장 가까운 상위 Suspense boundary를 전환하여 fallback, 즉 Loading 컴포넌트를 표시함. 그런 다음 데이터가 로드되면, React는 Loading fallback을 숨기고 데이터와 함께 Albums 컴포넌트를 렌더링함.
import { Suspense } from 'react';
import Albums from './Albums.js';
export default function ArtistPage({ artist }) {
return (
<>
<h1>{artist.name}</h1>
<Suspense fallback={<Loading />}>
<Albums artistId={artist.id} />
</Suspense>
</>
);
}
function Loading() {
return <h2>🌀 Loading...</h2>;
}
Note
Suspense-enabled한 데이터 소스만 Suspense 컴포넌트를 활성화함. 여기에는 다음이 포함됨:
- Relay 및 Next.js와 같은 Suspense-enabled 프레임워크를 사용한 데이터 fetching
lazy를 사용한 Lazy-loading 컴포넌트 코드use를 사용한 Prokise 값 읽기Suspense는 Effect 또는 이벤트 핸들러 내부에서 데이터를 가져오는 시점을 감지하지 못함.
위의
Albums컴포넌트에서 데이터를 로드하는 정확한 방법은 프레임워크에 따라 다름. Suspense-enabled 프레임워크를 사용하는 경우, 해당 프레임워크의 데이터 fetching 문서에서 자세한 내용을 확인할 수 있음.Suspense-enabled 프레임워크를 사용하지 않는 Suspense-enabled 데이터 fetching은 아직 지원되지 않음. Suspense-enabled 데이터 소스를 구현하기 위한 요구 사항은 불안정하고 문서화되어 있지 않음. 데이터 소스를 Suspense와 통합하기 위한 공식 API는 향후의 React 버전에서 출시될 예정임.
기본적으로 Suspense 내부의 전체 트리는 하나의 단위로 취급됨. 예를 들어, Suspense 내부의 하나의 컴포넌트가 데이터를 기다리느라 일시 중단되더라도 Suspense 내부의 모든 컴포넌트가 함께 loading indicator로 대체됨:
<Suspense fallback={<Loading />}>
<Biography />
<Panel>
<Albums />
</Panel>
</Suspense>
그런 다음, Suspense 내부의 모든 컴포넌트가 표시될 준비가 되면 한 번에 모두 함께 표시됨.
위의 코드에서 Biography와 Albums가 각각 데이터를 fetch 하더라도, 이 두 컴포넌트는 하나의 Suspense boundary 아래에 그룹화되어 있기 때문에 항상 동시에 함께 'pop in' 됨.
데이터를 로드하는 컴포넌트가 Suspense boundary의 직접적인 자식일 필요는 없음. 예를 들어, 아래 코드처럼 Biography 및 Albums를 새로운 Details 컴포넌트 안으로 이동할 수 있음. 이렇게 해도 동작은 변경되지 않음. Biography와 Albums는 가장 가까운 상위 Suspense boundary를 공유하므로 표시 여부가 함께 조정됨.
<Suspense fallback={<Loading />}>
<Details artistId={artist.id} />
</Suspense>
function Details({ artistId }) {
return (
<>
<Biography artistId={artistId} />
<Panel>
<Albums artistId={artistId} />
</Panel>
</>
);
}
컴포넌트가 일시 중단되면, 가장 가까운 상위 Suspense 컴포넌트가 fallback을 표시함. 이를 통해 여러 Suspense 컴포넌트를 중첩하여 로딩 시퀀스를 만들 수 있음. 각 Suspense boundary의 fallback은 다음 단계의 콘텐츠를 사용할 수 있게 되면 채워짐. 예를 들어, 앨범 목록에 자체 fallback을 지정할 수 있음:
<Suspense fallback={<BigSpinner />}>
<Biography />
<Suspense fallback={<AlbumsGlimmer />}>
<Panel>
<Albums />
</Panel>
</Suspense>
</Suspense>
이렇게 하면 Biography를 표시할 때 Albums가 로드될 때까지 "기다릴" 필요가 없음.
시퀀스는 다음과 같음:
Biography가 아직 로드되지 않은 경우, 전체 콘텐츠 영역 대신 BigSpinner가 표시됨.Biography 로드가 완료되면, BigSpinner가 콘텐츠로 대체됨.Albums가 아직 로드되지 않은 경우, Albums와 그 부모인 Panel 대신 AlbumsGlimmer가 표시됨.Albums 로딩이 완료되면, AlbumsGlimmer가 Albums와 그 부모인 Panel로 대체됨.Suspense boundaries를 사용하면 UI의 어느 부분이 항상 동시에 "pop in" 되어야 하는지, 어느 부분이 로딩 상태 시퀀스에서 점진적으로 더 많은 콘텐츠를 표시해야 하는지를 조정할 수 있음. 앱의 나머지 동작에 영향을 주지 않고 트리의 어느 위치에서나 Suspense boundaries를 추가, 이동 또는 삭제할 수 있음.
모든 컴포넌트 주위에 Suspense boundaries를 두지 말 것. Suspense boundaries는 사용자가 경험하게 될 로딩 시퀀스보다 더 세분화되어서는 안 됨. 디자이너와 함께 작업하는 경우, 로딩 상태를 어디에 배치해야 하는지 디자이너에게 물어볼 것. 디자이너가 이미 디자인 와이어프레임에 포함시켰을 가능성이 높음.
다음 예에서는 검색 결과를 가져오는 동안 SearchResults 컴포넌트가 일시 중단됨. "a"를 입력하고 결과를 기다린 다음 "ab"로 수정하면, "a"에 대한 결과는 loading fallback으로 대체됨.
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 패턴은 목록 업데이트를 지연하고 새 결과가 준비될 때까지 이전 결과를 계속 표시하는 것. 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>
</>
);
}
쿼리가 즉시 업데이트되므로 input에는 새 값이 표시됨. 하지만 데이터가 로드될 때까지 deferredQuery는 이전 값을 유지하므로 SearchResults는 잠시 동안 이전 결과를 표시함.
사용자에게 더 명확하게 알리기 위해, 이전 결과 목록이 표시될 때 시각적 indication을 추가할 수 있음:
<div style={{
opacity: query !== deferredQuery ? 0.5 : 1
}}>
<SearchResults query={deferredQuery} />
</div>
이제 "a"를 입력하고 결과가 로드될 때까지 기다린 다음 입력을 "ab"로 수정하면 새 결과가 로드될 때까지 Suspense fallback 대신 희미한 이전 결과 목록이 표시됨.
Note
지연된 값과 transition 모두 인라인 indicator를 위해 Suspense fallback을 표시하지 않도록 함. Transition은 전체 업데이트를 긴급하지 않은 것으로 표시하므로 일반적으로 프레임워크와 라우터 라이브러리에서 navigation을 위해 사용함. 반면 지연 값은 UI의 일부를 긴급하지 않은 것으로 표시하고 나머지 UI보다 '지연'시키려는 애플리케이션 코드에서 주로 유용함.
컴포넌트가 일시 중단되면 가장 가까운 상위 Suspense boundary가 fallback을 표시하도록 전환됨. 이로 인해 이미 일부 콘텐츠가 표시되고 있는 경우 사용자 경험이 불안정해질 수 있음:
// 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>;
}
버튼을 누르면 Router 컴포넌트는 IndexPage 대신 ArtistPage를 렌더링함. ArtistPage 내부의 컴포넌트가 일시 중단되었기 때문에 가장 가까운 Suspense boundary가 fallback을 표시하기 시작함. 가장 가까운 Suspense boundary가 루트 근처에 있었기 때문에 전체 사이트 레이아웃이 BigSpinner로 대체됨.
이를 방지하기 위해 startTransition을 사용하여 navigation state 업데이트를 transition으로 표시할 수 있음:
function Router() {
const [page, setPage] = useState('/');
function navigate(url) {
startTransition(() => {
setPage(url);
});
}
// ...
이는 state transiton이 긴급하지 않으며, 이미 공개된 콘텐츠를 숨기는 대신 이전 페이지를 계속 표시하는 것이 낫다는 것을 React에게 알려줌. 이제 버튼을 클릭하면 Biography가 로드될 때까지 "대기"함.
Transition은 모든 콘텐츠가 로드될 때까지 기다리지 않음. 이미 표시된 콘텐츠를 숨기지 않을 만큼만 기다림. 예를 들어 웹사이트 Layout이 이미 공개되었으므로 로딩 스피너 뒤에 숨기는 것은 좋지 않음. 그러나 Albums를 둘러싼 중첩된 Suspense boundary는 새로운 것이므로 transition이 기다리지 않음.
Note
Suspense-enabled 라우터는 기본적으로 navigation 업데이트를 transition으로 감쌀 것으로 예상됨.
위의 예에서는 버튼을 클릭해도 navigation이 진행 중이라는 시각적 indication이 없었음. Indicator를 추가하려면 startTransiton을 useTransition으로 대체하여 boolean 값인 isPending을 반환하면 됨. 아래 예에서는 transition이 진행되는 동안 웹사이트 헤더 스타일을 변경하는 데 사용됨:
// App.js
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>;
}
// Layout.js
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>
);
}
Transition 중에 React는 이미 노출된 콘텐츠를 숨기지 않음. 그러나 다른 parameters가 있는 경로로 이동하는 경우, React에게 다른 콘텐츠임을 알려주고 싶을 수 있음. 이를 key로 표현할 수 있음:
<ProfilePage key={queryParams.id} />
사용자의 프로필 페이지 내에서 탐색 중에 무언가가 일시 중단된 경우, 해당 업데이트가 transition으로 감싸지면 이미 표시된 콘텐츠에 대한 fallback이 트리거되지 않음. 이것은 예상되는 동작임.
하지만 서로 다른 두 개의 사용자 프로필 사이를 탐색한다고 가정하면, 이 경우 fallback을 표시하는 것이 좋음. 예를 들어 한 사용자의 타임라인은 다른 사용자의 타임라인과 다른 콘텐츠임. key를 지정하면 React가 서로 다른 사용자의 프로필을 서로 다른 컴포넌트로 취급하고 탐색 중에 Suspense boundaries를 재설정하도록 할 수 있음. Suspense-integrated 라우터는 이 작업을 자동으로 수행해야 함.
스트리밍 서버 렌더링 API 중 하나(또는 이에 의존하는 프레임워크)를 사용하는 경우, React는 서버에서 발생하는 오류를 처리하기 위해 Suspense boundaries도 사용함. 컴포넌트가 서버에서 에러를 발생시키면 React는 서버 렌더링을 중단하지 않음. 대신, 가장 가까운 상위 <Suspense> 컴포넌트를 찾아서 생성된 서버 HTML에 그 fallback(예: 스피너)을 포함시킴. 사용자는 처음에는 스피너를 보게 됨.
클라이언트에서 React는 동일한 컴포넌트를 다시 렌더링하려고 시도함. 클라이언트에서도 에러가 발생하면 React는 에러를 던지고 가장 가까운 error boundary를 표시함. 그러나 클라이언트에서 에러가 발생하지 않는다면 콘텐츠가 결국 성공적으로 표시되었기 때문에 React는 사용자에게 에러를 표시하지 않음.
이를 사용하여 일부 컴포넌트를 서버에서 렌더링하지 않도록 선택할 수 있음. 이렇게 하려면 서버 환경에서 에러를 발생시킨 다음 Suspense boundary로 감싸서 해당 HTML을 fallback으로 대체하면 됨:
<Suspense fallback={<Loading />}>
<Chat />
</Suspense>
function Chat() {
if (typeof window === 'undefined') {
throw Error('Chat should only render on the client.');
}
// ...
}
서버 HTML에는 indicator가 포함됨. 이 indicator는 클라이언트의 Chat 컴포넌트로 대체됨.
표시되는 UI를 fallback으로 대체하면 사용자 경험이 불안정해짐. 이는 업데이트로 인해 컴포넌트가 일시 중단되고 가장 가까운 Suspense boundary가 이미 사용자에게 콘텐츠를 표시하고 있을 때 발생할 수 있음.
이런 일이 발생하지 않도록 하려면 startTransition을 사용하여 업데이트를 긴급하지 않은 것으로 표시할 것. Transition이 진행되는 동안 React는 원치 않는 fallback이 나타나지 않도록 충분한 데이터가 로드될 때까지 기다림:
function handleNextPageClick() {
// If this update suspends, don't hide the already displayed content
startTransition(() => {
setCurrentPage(currentPage + 1);
});
}
이렇게 하면 기존 콘텐츠가 숨겨지지 않음. 그러나 새로 렌더링된 Suspense boundaries는 여전히 즉시 fallback을 표시하여 UI를 가리지 않고 사용자가 콘텐츠를 사용할 수 있게 되면 볼 수 있도록 함.
React는 긴급하지 않은 업데이트 중에만 원치 않는 fallback을 방지함. 긴급한 업데이트의 결과인 경우 렌더링을 지연시키지 않음. startTransition이나 useDeferredValue와 같은 API로 opt in 해야 함.
라우터가 Suspense와 통합된 경우, 라우터는 자동으로 업데이트를 startTransition으로 감싸야 함.