✔️ React 18에서 추가된 Suspense에 대해 살펴보자
Suspense란?
- fallback 컴포넌트를 로딩이 끝날 때 까지 보여주는 기능이다.
- 사실 Suspense는 React 16.6 버전에서 실험적으로 도입된 기능이었다.
기존의 한계
- Next.js에서 사용할 수 없었다. → (React 18) Next.js에서도 사용할 수 있게 되었다.
- 컴포넌트가 마운트가 되기 전에 effect가 실행되는 문제가 발생했다. → (React 18) 컴포넌트가 화면에 실제로 노출될 때 effect가 실행된다.
- 이전에는 컴포넌트가 스스로 Suspense에 의해 보여지고 있는지 없는지 알 방법이 없었다. → (React 18) Suspense로 인해 컴포넌트가 보이거나 사라질 때도 effect가 정상적으로 실행이 되는 방식으로 바뀌었다.
- Suspense에 의해 노출된다면 useLayoutEffectdml effect(componentDidMount)가
- 가려진다면 useLayOutEffect의 cleanUp(componentWillMount)이 정상적으로 실행
- Suspense 내의 쓰로틀링이 추가
- 중첩된 Suspense의 fallback이 있다면, 자동으로 쓰로틀링 되어 최대한 자연스럽게 보여준다
예제
https://react.dev/reference/react/Suspense
Suspense는 기본적으로 다음과 같이 사용된다
<Suspense fallback={<Loading />}>
<SomeComponent />
</Suspense>
- 어떤 컴포넌트가 있고 그 컴포넌트를 바깥에서 Suspense로 감싸준 후에 그 Suspense의 fallback props를 통해서 어떤 컴포넌트를 이 children이 자식 컴포넌트가 Loading이 끝났을 때까지 보여줄 것인지를 넣어주면 된다.
주의할 점
- React는 최초 마운트가 되기 전에 중단된 렌더링 상태에 대해서는 어떤한 상태도 보존이 되지 않는다. 만약에 컴포넌트가 로드가 되면 React는 중단된 트리를 처음부터 다시 리렌더링 시도를 하게 된다
- Suspense가 트리에서 컨텐츠를 표시하다가 중단된 경우 그 업데이트가 startTransition 또는 useDeferredValue에 의해서 발생된게 아니라면 fallback 컴포넌트가 다시 보여지게 된다.
- React가 이미 보이는 컨텐츠를 다시 중단해서 숨겨야 할 때 React는 컨텐츠 트리에서 레이아웃 이펙트를 정리를 하게 된다. 만약에 다시 컨텐츠를 표시할 준비가 되었을 때 React는 이 레이아웃 이펙트를 다시 발생을 시키는데 이러한 작업은 컨텐츠가 숨겨진 동안 DOM 레이아웃을 측정하는 이펙트가 이를 시도하지 않도록 보장하는 것을 의미한다.
기본예제
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>;
}
- 이러한 어떤 Albums라는 자식 컴포넌트가 데이터를 불러올 때까지 Loading을 보여주고 싶다고 했을 때 Suspense를 감싸주고 Loading 컴포넌트로 fallbac을 넣어주면 된다.
Suspense안에 하나의 컨텐츠만 들어갈 수도 있지만 여러 개의 컨텐츠가 들어갈 수도 있다
<Suspense fallback={<Loading />}>
<Biography />
<Panel>
<Albums />
</Panel>
</Suspense>
- 이렇게 여러 개의 컨텐츠가 들어간 경우에는 동일하게 Loading이 보여지고 여러 개의 컨텐츠가 모두 다 Loading이 끝난 이후에 한꺼번에 다 보여주게 된다.
하지만 어떤 컴포넌트는 금방 Loading이 되는데 어떤 컴포넌트는 굉장히 오래 걸리는 경우는 어떻게 해야 될까? 그러면 마지막 컴포넌트가 끝날 때 까지 기다려야 하나?
Suspense에서는 중첩된 방식으로 구현할 수도 있다!
<Suspense fallback={<BigSpinner />}>
<Biography />
<Suspense fallback={<AlbumsGlimmer />}>
<Panel>
<Albums />
</Panel>
</Suspense>
</Suspense>
- 이렇게 여러 개의 컨텐츠가 있을 때 컨텐츠별로 중첩해서 Suspense를 만들어 줄 수가 있다.
실행 순서는 Biography가 로드가 되기 전에는 BigSpinner가 돌아가게 된다 → Biography로드가 다 되었으면 BigSpinner가 사라지고 Biography가 보여지게 된다 → 밑에 애들은 Suspense 보여지지 않는 상태가 있다 → Panel과 Albums 이 부분이 로드가 안 되었기 때문에 Biography가 보여지고 나서도 AlbumsGlimmer는 계속 보여지게 된다 → Panel과 Albums이 다 로드가 되었을 때 AlbumsGlimmer가 사라지고 Panel과 Albums이 보여진다.
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>
<Suspense fallback={<BigSpinner />}>
<Biography artistId={artist.id} />
<Suspense fallback={<AlbumsGlimmer />}>
<Panel>
<Albums artistId={artist.id} />
</Panel>
</Suspense>
</Suspense>
</>
);
}
function BigSpinner() {
return <h2>🌀 Loading...</h2>;
}
function AlbumsGlimmer() {
return (
<div className="glimmer-panel">
<div className="glimmer-line" />
<div className="glimmer-line" />
<div className="glimmer-line" />
</div>
);
}
만약에 컨텐츠가 조금 지연될 경우 어떻게 될까? (useDeferredValue)
- 먼저 우리가 어떤 쿼리를 한다고 했을 때 쿼리를 하면 쿼리 데이터를 불러오는데 시간 때문에 Suspense가 보여지게 된다
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>
</>
);
}
- Loading 보여지고 데이터가 보여지고를 반복하는 코드다
그런데 만약에 우리가 deferredQuery를 사용한다면 state가 바뀔 때마다 매번 바로 Loading이 이루어지는게 아니라 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>
</>
);
}
- 쿼리를 이렇게 deferredQuery로 한 번 감싸주고
<div style={{
opacity: query !== deferredQuery ? 0.5 : 1
}}>
<SearchResults query={deferredQuery} />
</div>
- deferredQuery가 실제 쿼리랑 변화를 눈으로 볼 수 있게 한 번 처리를 해준 다음에 deferredQuery를 넣어보자
import { Suspense, useState, useDeferredValue } from 'react';
import SearchResults from './SearchResults.js';
export default function App() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
const isStale = query !== deferredQuery;
return (
<>
<label>
Search albums:
<input value={query} onChange={e => setQuery(e.target.value)} />
</label>
<Suspense fallback={<h2>Loading...</h2>}>
<div style={{ opacity: isStale ? 0.5 : 1 }}>
<SearchResults query={deferredQuery} />
</div>
</Suspense>
</>
);
}
- Loading이라는 fallback을 넣어도 보지 못 한다. Loading을 Suspense에서 인지하고 있는게 아니라 deferred 처리로 인해서 state가 지연된 처리가 이루어졌기 때문이다.
컨텐츠가 부분적으로 리렌더링 될 때 기존 방식은 이미 보여지고 있었던 컨텐츠가 사라지게 되고 숨겨지게 되는 요소이기 때문에 그렇게 좋은 권장하는 패턴은 아니다. 기존에 만들어 놨던 것들은 계속해서 유지를 한 상태로 이제 새로운 컨텐츠들을 Suspense로 보여주는 것이 굉장히 중요하다 → 여기서 사용하는 개념이 startTransition이다.
startTransition을 사용을 하게되면 Route처리 navigate를 하는 처리에서 URL 상태 페이지의 상태를 바꿔줄 때 그냥 바꿔주는게 아니라 기존에는 그냥 바로 바꿔줬다면
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>;
}
→ startTransition을 감싸줘서 바꿔주면
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>;
}
- 브라우저는 그대로 있고 이 안에 컨텐츠만 새롭게 fallback도 보여지고 그리고 fallback 안에 중첩이 된 컨텐츠들이 보여지게 된다.
- 만약에 state를 그냥 바꾸면 가장 가까운 부모의 Suspense의 fallback을 나타내 주게 된다.
- startTransition 쓰게 되면 React는 이 상태 변화가 급한게 아니고 천천히 해도 된다고 인식을 하고 이미 보여줬던 것들을 그대로 놔두고 하위 컴포넌트가 보여질 때까지 기존 것을 유지하는 경향을 가지고 있다.
'Front-End > React' 카테고리의 다른 글
자동 배치(Automatic Batching) (0) | 2024.07.31 |
---|---|
React v18 (0) | 2024.07.31 |
React 동작 방식 (0) | 2024.03.25 |
엑셀 다운로드 (0) | 2023.07.02 |
useSWR vs React(TanStack) Query를 비교해보자 (2) | 2023.01.10 |