์ด์ ๊ธ: 2024.06.15 - [๐ฉ๐ป๐ป dev] - โ๏ธ <Suspense>๋? ์์๋ ๋ฆฌ์กํธ ๊ณต์๋ฌธ์์ Suspense props์ ๋ํด ์์๋ดค๋ค.
โ๏ธ <Suspense>๋?
๐ฃ๏ธ 'Suspense'๋ฅผ ์จ๋ณด์ ์ ์๋์? ์๋ค๋ฉด ์ธ์ , ์ ์ฐ์๋์? ์์ฐ๋ฉด ์ด๋ค ์ ์ด ๋ถํธํ๋์? ์ฐ๊ธด ์ฐ๋๋ฐ..๋ก๋ฉ ์ค์ ๋์ ๋ณด์ฌ์ค UI๋ฅผ ์ ๊ณตํ๊ธฐ ์ํด์ ์๋๊ฐ? ๋ฆฌ์กํธ์ ๊ณต์๋ฌธ์์ ์ํ๋ฉด
pyotato-dev.tistory.com
์ด๋ฒ ๊ธ์์๋ ์ด์ ๊ธ์์ ๊ฐ๋จํ๊ฒ ์๊ฐ๋ง ํ Suspense๊ฐ ์ฐ์ด๋ ๊ฒฝ์ฐ์ ๋ํด ์์์ ํจ๊ป ์ดํด๋ณด์.
Suspense ํ์ฉ
1. ์ปจํ ์ธ ๊ฐ ๋ก๋ฉ๋๊ณ ์๋ ๋์ ๋ณด์ฌ์ค fallback์ ๋ณด์ฌ์ฃผ๊ณ ์ถ์ ๋
2. ํ๊บผ๋ฒ์ ์ฝํ ์ธ ๋ฅผ ๋ ธ์ถํ๊ณ ์ถ์ ๋
3. ๋ก๋ฉ๋๊ณ ์๋ ๋์ ์ค์ฒฉ(nested)๋ ์ฝํ ์ธ ๋ฅผ ๋ ธ์ถํ๊ณ ์ถ์ ๋
4. ์ (fresh) ์ฝํ ์ธ ๋ฅผ ๋ก๋ฉํ๊ณ ์๋ ๋์ ์ง์ (stale) ์ฝํ ์ธ ๋ฅผ ๋ณด์ฌ์ค ๋
5. ์ด๋ฏธ ๋ ธ์ถ๋ ์ฝํ ์ธ ๋ฅผ ์จ๊ธฐ๋ ๊ฑธ ๋ฐฉ์งํ๊ณ ์ถ์ ๋
6. Transition์ด ์ผ์ด๋๊ณ ์๋ค๋ ๊ฑธ ์๋ฆฌ๊ณ ์ถ์ ๋
7. navigation์ด ๋ฐ์ํ ๋ Suspense boundary๋ค์ ๋ฆฌ์ ํ๊ณ ์ถ์ ๋
8. ์๋ฒ ์๋ฌ ๋ฐ์ ์์ ํด๋ผ์ด์ธํธ์๋ง ํด๋นํ๋ ์ฝํ ์ธ ๋ฅผ ๋ณด์ฌ์ค fallback์ ์ ๊ณตํ๊ณ ์ถ์ ๋
์ฐ์ 1: ์ปจํ ์ธ ๊ฐ ๋ก๋ฉ๋๊ณ ์๋ ๋์ ๋ณด์ฌ์ค fallback์ ๋ณด์ฌ์ฃผ๊ณ ์ถ์ ๋
์ ํ๋ฆฌ์ผ์ด์ ์ด๋๋ ์ง ์ํ๋ ๊ณณ์ Suspense boundary๋ฅผ ๊ฐ์ธ์ค ์ ์๋ค.
children์์ ์ฐ์ผ ๋ฐ์ดํฐ์ ์ฝ๋๊ฐ ๋ชจ๋ ๋ก๋๋๊ธฐ ์ ๊น์ง loading fallback์ ๋์ ๋ณด์ฌ์ฃผ๊ธฐ ์ํด์ ์ฐ์ผ ์ ์๋ค.
Album ์ปดํฌ๋ํธ๊ฐ ๋ก๋ฉ๋๊ธฐ ์ ๊น์ง Loading ์ปดํฌ๋ํธ๋ฅผ fallback์ผ๋ก ๋ณด์ฌ์ฃผ๋ ๋ค์ ์๋ฅผ ์ดํด๋ณด์.
<Suspense fallback={<Loading />}>
<Albums />
</Suspense>
<Album/> ์ปดํฌ๋ํธ๋ ์จ๋ฒ ๋ฆฌ์คํธ๋ฅผ fetch ํด์ค๋ ๋์ suspend ํ๋ค.
๋ ๋ ๋ ์ค๋น๊ฐ ๋๊ธฐ ์ ๊น์ง ๋ฆฌ์กํธ๋ ๊ฐ์ฅ ๊ฐ๊น์ด ์์์ Suspense boundary๋ฅผ ์ฐพ์์ fallback์ธ <Loading/>์ ๋ณด์ฌ์ค๋ค..
Suspend์ ๋ป์ ๋์ ํด ๋ณด๋ฉด '์ผ์์ ์ผ๋ก ์ด๋ค ๋์(<Album/> ์ปดํฌ๋ํธ๊ฐ ๋ ๋ ๋๋ ๊ฒ)์ด ๋ฐ์ํ๊ฑฐ๋ ์ผ์ด๋๋ ๊ฒ์ ๋ฉ์ถ๋ ๊ฒ'์ด๋ค.
import { Suspense } from 'react';
import Albums from './Albums.js';
function ArtistPage({ artist }) {
return (
<>
<h1>{artist.name}</h1>
<Suspense fallback={<Loading />}>
<Albums artistId={artist.id} />
</Suspense>
</>
);
}
function Loading() {
return <h2>๐ Loading...</h2>;
}
export default function App() {
const [show, setShow] = useState(false);
if (show) {
return (
<ArtistPage
artist={{
id: 'the-beatles',
name: 'The Beatles',
}}
/>
);
} else {
return (
<button onClick={() => setShow(true)}>
Open The Beatles artist page
</button>
);
}
}
๋์์ ๋ค์๊ณผ ๊ฐ๋ค.
โ ๏ธ ์ฃผ์ํ ์
- Suspense-enabled ๋ฐ์ดํฐ ์์ค๋ง Suspense ์ปดํฌ๋ํธ๋ฅผ ์๋์ํจ๋ค.
- Suspense-enabled ๋ฐ์ดํฐ ์์ค์๋:
- Suspense๋ Effect๋ ์ด๋ฒคํธ ํธ๋ค๋ฌ ๋ด์ ๋ฐ์ดํฐ๊ฐ fetch ๋๋ ๊ฒ์ ๊ฐ์งํ์ง ๋ชปํ๋ค.
- Albums ์ปดํฌ๋ํธ ๋ด์ ๋ฐ์ดํฐ๋ฅผ ๋ก๋ํ๋ ์ ํํ ๋ฐฉ์์ ํ๋ ์์ํฌ์ ๋ฐ๋ผ ๋ค๋ฅผ ์ ์๋ค.
- Suspense-enabled ํ๋ ์์ํฌ๋ฅผ ์ฌ์ฉํ๋ค๋ฉด ๋ฐ์ดํฐ fetch ๋ฌธ์์์ ํด๋น ๋ด์ฉ์ ์ฐพ์ ์ ์๋ค.
- ํ๋ ์์ํฌ์ ์ข ์์ (opinionated)์ด์ง ์์ Suspense-enabled data fetching์ ์์ง ์ง์ํ์ง๋ ์๋๋ค.
- ๋ฐ๋ผ์ ์์ง ๋ถ์์ ํ๊ณ ๋ฌธ์๊ฐ ์๋ค๊ณ ํ๋ค.
์์ผ๋ก์ ๋ฆฌ์กํธ ๋ฒ์ ๋ค์์ ๊ณต์์ API๋ฅผ Suspense๋ฅผ ๊ธฐ๋ค๋ฆด ์๋ฐ์
์ฐ์ 2: ์ฝํ ์ธ ๊ฐ ๋ก๋ฉ๋๊ณ ์๋ ๋์ ๋ณด์ฌ์ค fallback์ ๋ณด์ฌ์ฃผ๊ณ ์ถ์ ๋
๊ธฐ๋ณธ์ ์ผ๋ก Suspense ์์ ํธ๋ฆฌ๋ ํ๋์ ๋ฌถ์์ผ๋ก ์ทจ๊ธ๋๋ค.
Biography ์ปดํฌ๋ํธ, Panel์ ๊ฐ์ผ Album ์ปดํฌ๋ํธ๋ฅผ children์ผ๋ก ๊ฐ๋ ๋ค์๊ณผ ๊ฐ์ ์๋ฅผ ์ดํด๋ณด์.
<Suspense fallback={<Loading />}>
<Biography />
<Panel>
<Albums />
</Panel>
</Suspense>
์์ ๊ฒฝ์ฐ, ์ปดํฌ๋ํธ ์ค ํ๋๋ผ๋ ๋ฐ์ดํฐ๋ฅผ ๊ธฐ๋ค๋ฆฌ๊ธฐ ์ํด suspend ๋๋ค๋ฉด
Suspense๋ก ๊ฐ์ผ ๋ชจ๋ ์ปดํฌ๋ํธ๊ฐ <Loading/>์ผ๋ก ๋์ฒด๋๋ค.
๋ชจ๋ children ์ปดํฌ๋ํธ๊ฐ ๋ ธ์ถ๋ ์ค๋น๊ฐ ๋์ด์์ผ ๋น๋ก์ ํ๊บผ๋ฒ์ ๋ชจ๋ ๋ํ๋๋ค.
// ํ๋์ Suspense๋ก ๊ฐ์ธ์ฃผ๋ฉด
// ํ์์ ์์๋ค์ ๊ฐ์ฅ ๊ฐ๊น์ด ๊ณตํต์ ๋ถ๋ชจ Suspense boundary๋ฅผ ๊ณต์ ํ๋ฏ๋ก
// ์๋์ ๊ฐ์ด Biography์ Panel์ Details ์ปดํฌ๋ํธ๋ก ์ฎ๊ฒจ์ค๋
// ์์ ๋์ผํ๊ฒ ๋์ํ๋ค
<Suspense fallback={<Loading />}>
<Details artistId={artist.id} />
</Suspense>
function Details({ artistId }) {
return (
<>
<Biography artistId={artistId} />
<Panel>
<Albums artistId={artistId} />
</Panel>
</>
);
}
์ฐ์ 3: ๋ก๋ฉ๋๊ณ ์๋ ๋์ ์ค์ฒฉ(nested)๋ ์ฝํ ์ธ ๋ฅผ ๋ ธ์ถํ๊ณ ์ถ์ ๋
์ปดํฌ๋ํธ๊ฐ suspend ๋๋ฉด, ๊ฐ์ฅ ๊ฐ๊น์ด ๋ถ๋ชจ Suspense ์ปดํฌ๋ํธ๊ฐ fallback์ ๋ณด์ฌ์ค๋ค.
๋ฐ๋ผ์ ์ฌ๋ฌ ๊ฐ์ Suspense ์ปดํฌ๋ํธ๋ฅผ ์ค์ฒฉํด์ ๋ก๋ฉ ์ํ์ค๋ฅผ ๋ง๋ค ์ ์๋ค.
์ฝํ ์ธ ๊ฐ ์ค๋น๋๋ฉด ์์์ Suspense Boundary์ fallback์ ํด๋น ์ฝํ ์ธ ๋ก ์ฑ์์ง๋ค.
๋ค์๊ณผ ๊ฐ์ด Album๋ฆฌ์คํธ๋ง์ fallback์ ์ฃผ๋ ๊ฒฝ์ฐ๋ฅผ ์ดํด๋ณด์.
<Suspense fallback={<BigSpinner />}>
<Biography />
<Suspense fallback={<AlbumsGlimmer />}>
<Panel>
<Albums />
</Panel>
</Suspense>
</Suspense>
์์ ๊ฐ์ ๊ฒฝ์ฐ์๋ Album์ ๋ฆฌ์คํธ๊ฐ ๋ก๋ฉ๋ ๋๊น์ง Biography์ ์ฝํ ์ธ ๊ฐ ๊ธฐ๋ค๋ ค์ค ํ์๊ฐ ์์ด์ง๋ค.
์ํ์ค ํ๋ฆ์ ๋ค์๊ณผ ๊ฐ๋ค.
- Biography ์ปดํฌ๋ํธ๊ฐ ๋ก๋ฉ๋์ง ์์๋ค๋ฉด BigSpinner๊ฐ ์ ์ฒด ์ฝํ ์ธ ๋ฅผ ๋์ ํด์ ๋ ธ์ถ๋๋ค.
- Biography ์ปดํฌ๋ํธ๊ฐ ๋ก๋ฉ์ ๋ง์น๋ฉด ํด๋น ์ฝํ ์ธ ๋ก BigSpinner๋ฅผ ๋์ฒดํ๋ค.
- Albums์ด ์์ง ๋ก๋๋์ง ์์๋ค๋ฉด, AlbumsGlimmer๊ฐ Albums์ Albums์๋ถ๋ชจ ์ปดํฌ๋ํธ์ธ Panel ๋์ ์ ๋ ธ์ถ๋๋ค.
- Albums์ด ๋ก๋ฉ์ ๋ง์น๋ฉด AlbumsGlimmer๋ฅผ ํด๋น ์ฝํ ์ธ ๋ก ๋์ฒดํ๋ค.
Suspense boundary๋ฅผ ์ด๋์ ๋ฐฐ์นํ๋๋์ ๋ฐ๋ผ
์ด๋ค UI๋ฅผ ๋์์ ์ง ! ํ๊ณ ๋ณด์ด๊ฒ ํ ์ง, ๋๋ ๋ก๋ฉ ์ํ์ ๋ฐ๋ผ์ ์ ์ง์ ์ผ๋ก ๋ณด์ด๊ฒ ํ ์ง ์ ํ ์ ์๋ค.
ํ์ง๋ง ๋ชจ๋ ์ปดํฌ๋ํธ์ Suspense boundary๋ฅผ ๋ฃ๋ ๊ฑด ๋ฐ๋์งํ์ง ์๋ค.
์ ์ ๊ฐ ๊ฒฝํํ ๋ก๋ฉ ์ํ์ค๋ณด๋ค ๋ ์ธ์ธํ๊ฒ ๋๋ ์ ๋ฃ์ผ๋ฉด ์ ๋๋ค.
๋์์ด๋์ ํจ๊ป ์์
์ ํ๊ณ ์๋ค๋ฉด, ๋ก๋ฉ ์ํ๊ฐ ์ธ์ ๋ณด์ฌ์ผ ํ ์ง ์์ํ๋ ๊ฒ ์ข๋ค. ์ด๋ฏธ ๋์์ธ ์์ด์ด ํ๋ ์์ ๋ฐ์๋์ด ์์ ๊ฐ๋ฅ์ฑ์ด ๋งค์ฐ ๋๊ธฐ ๋๋ฌธ
์ฐ์ 4: ์ (fresh) ์ฝํ ์ธ ๋ฅผ ๋ก๋ฉํ๊ณ ์๋ ๋์ ์ง์ (stale) ์ฝํ ์ธ ๋ฅผ ๋ณด์ฌ์ค ๋
๊ฒ์ ๊ฒฐ๊ณผ๋ฅผ fetch ํด์ค๋ ๋์ SearchResults ์ปดํฌ๋ํธ๊ฐ suspend ๋๋ ์๋ฅผ ์ดํด๋ณด์.
'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>
</>
);
}
์๋ก์ด ๊ฒ์ ๊ฒฐ๊ณผ๊ฐ ์ค๋น๋ ๋๊น์ง ์ด์ ์ ๊ฒ์ ๊ฒฐ๊ณผ๋ฅผ ๋ณด์ฌ์ค์ ๋ฆฌ์คํธ ์ ๋ฐ์ดํธ๋ฅผ defer ํ๋ UI ํจํด์ผ๋ก๋ ํํ ๋์ฒดํ ์ ์๋ค.
useDeferredValue๋ฅผ ํ์ฉํ๋ฉด defer ๋ ๋ฒ์ ์ ์ฟผ๋ฆฌ๋ฅผ ๋๊ธธ ์ ์๊ฒ ํ๋ค.
export default function App() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
return (
<>
<label>
Search albums:
// query๋ ์ฆ์ ์
๋ฐ์ดํธ ๋๊ธฐ ๋๋ฌธ์ ๋ฐ๋ก ์ ๊ฐ์ ๋ณด์ฌ์ค๋ค
<input value={query} onChange={e => setQuery(e.target.value)} />
</label>
<Suspense fallback={<h2>Loading...</h2>}>
// ํ์ง๋ง deferredQuery๋ ์๋ก์ด ๋ฐ์ดํฐ๊ฐ ๋ก๋๋๊ธฐ ์ ๊น์ง ์ด์ ๋ฐ์ดํฐ๋ฅผ ๋ณด์ฌ์ฃผ๊ณ
// SearchResults๋ stale(์ด์ ) ๊ฒฐ๊ณผ๋ฅผ ๋ณด์ฌ์ค๋ค
<SearchResults query={deferredQuery} />
</Suspense>
</>
);
}
๐ค ์ด? ๊ทผ๋ฐ ์ด๋ฌ๋ฉด ์ด์ (stale)ํ๋ ๊ฐ์ธ ์ง ์๋ก์ด(fresh)ํ ๊ฐ์ธ ์ง ๊ตฌ๋ถํ๊ธฐ ์ด๋ ต์ง ์์๊น?
๊ทธ๋ด ๊ฒฝ์ฐ ๋ค์๊ณผ ๊ฐ์ด ์ด์ ๊ฒ์ ๊ฒฐ๊ณผ์ ํฌ๋ช ๋๋ฅผ ๋ฎ์ถฐ์ ์๊ฐ์ ์ธ ์งํ๋ฅผ ์ ์ ์๊ฒ ์ ๊ณตํ ์ ์๋ค.
<div style={{
opacity: query !== deferredQuery ? 0.5 : 1
}}>
<SearchResults query={deferredQuery} />
</div>
์ด์ ์ด์ ๊ฒฐ๊ณผ๋ฅผ placeholder์ฒ๋ผ ์ฌ์ฉํ๊ณ ๋ค์ ๊ฒ์ ๊ฒฐ๊ณผ๊ฐ ์ค๋น๋๋ฉด ํด๋น ๋ฐ์ดํฐ์ ์ปดํฌ๋ํธ๋ฅผ ๋ณผ ์ ์๋ค.
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>
</>
);
}
๐ Note
Defer ๋ ๊ฐ๊ณผ Transitions ๋ชจ๋ inline indicator ๋์ Suspense fallback๊ฐ ๋ ธ์ถ๋๋ ๊ฑธ ๋ฐฉ์งํ๋ค.
- Transitions๋ ์ ๋ฐ์ดํธ ์ ์ฒด๋ฅผ ๊ธํ์ง ์์(non-urgent) ์์ ์ผ๋ก ํ์ํด์ ์ผ๋ฐ์ ์ผ๋ก ํ๋ ์์ํฌ์ navigation ์ํ router ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ์ฃผ๋ก ์ฌ์ฉํ๋ค.
- Defer๋ ๊ฐ๋ค์ ์ฃผ๋ก UI ์ผ๋ถ๋ถ์ ๊ธํ์ง ์์ ์์ ์ผ๋ก ํ์ํ๊ณ , ๋๋จธ์ง UI๋ค ๋ณด๋ค ์ด์ง ๋ค์ฒ์ง๊ฒ๋ ํ๊ณ ์ถ์ ๋ ์ ์ฉํ๊ฒ ์ธ ์ ์๋ค.
์ฐ์ 5: ์ด๋ฏธ ๋ ธ์ถ๋ ์ฝํ ์ธ ๋ฅผ ์จ๊ธฐ๋ ๊ฑธ ๋ฐฉ์งํ๊ณ ์ถ์ ๋
์ปดํฌ๋ํธ๊ฐ suspend ๋๋ฉด ๊ฐ์ฅ ๊ฐ๊น์ด ๋ถ๋ชจ Suspense boundary๋ ์ด fallback์ ๋์ ๋ณด์ฌ์ค๋ค.
์ด๋ ์ ์ ํํ ์ด๋ฏธ ์ฝํ ์ธ ๋ฅผ ๋ณด์ฌ์ฃผ๊ณ ์๋ ์ํฉ์์ ๋ฐ์ํ๋ฉด ์คํ๋ ค ๋ถ์ ์ ์ธ UX๋ฅผ ์ ๊ณตํ๋ค.
์๋์ ๊ฐ์ด ๋ฒํผ์ ํด๋ฆญํด์ Router์ปดํฌ๋ํธ๊ฐ IndexPage๋์ ArtistPage๋ฅผ ๋ ๋ ํ๋ ๊ฒฝ์ฐ๋ฅผ ์ดํด๋ณด์.
ArtistPage ๋ด๋ถ์ ์ปดํฌ๋ํธ๊ฐ suspend ๋์๊ธฐ ๋๋ฌธ์ ๊ฐ์ฅ ๊ฐ๊น์ด ์์์ Suspense boundary์ fallback์ ๋ณด์ฌ์ฃผ๊ธฐ ์์ํ๋ค.
๊ทผ๋ฐ ๊ฐ์ฅ ๊ฐ๊น์ด Suspense boundary๋ root์ ๊ฐ๊น์ ๊ธฐ ๋๋ฌธ์ ์ ์ฒด ๋ ์ด์์์ด BigSpinner ์ปดํฌ๋ํธ๋ก ๋์ฒด๋๋ค.
// App.js
import { Suspense, useState } from 'react';
import IndexPage from './IndexPage.js';
import ArtistPage from './ArtistPage.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 (
<div className="layout">
<section className="header">
Music Browser
</section>
<main>
{content}
</main>
</div>
);
}
function BigSpinner() {
return <h2>๐ Loading...</h2>;
}
// Index.js
export default function IndexPage({ navigate }) {
return (
<button onClick={() => navigate('/the-beatles')}>
Open The Beatles artist page
</button>
);
}
// ArtistPage.js
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>
<Biography artistId={artist.id} />
<Suspense fallback={<AlbumsGlimmer />}>
<Panel>
<Albums artistId={artist.id} />
</Panel>
</Suspense>
</>
);
}
function AlbumsGlimmer() {
return (
<div className="glimmer-panel">
<div className="glimmer-line" />
<div className="glimmer-line" />
<div className="glimmer-line" />
</div>
);
}
์ด๋ ๊ฒ ์์์ Suspense boundary์ fallback์ด ์ ์ฒด ์ปดํฌ๋ํธ๋ฅผ ๋์ฒดํ๋ ์ํฉ์ ๋ฐฉ์งํ๊ธฐ ์ํด์๋
navigation state update๋ฅผ Transition์ผ๋ก ํ์ํด ๋ ์ ์๋ค.
function Router() {
const [page, setPage] = useState('/');
function navigate(url) {
startTransition(() => {
setPage(url);
});
}
// ...
startTransition์ผ๋ก ํ์ํจ์ผ๋ก์จ ๋ฆฌ์กํธํํ ์ํ๋ณํ(state transition)์ด ๊ธํ ๊ฒ ์๋๋ผ๋ ๊ฑธ ์๋ ค์ค ์ ์๊ณ ,
์ด๋ฏธ ๋ ธ์ถ๋ ์ฝํ ์ธ ๋ค์ ์จ๊ธฐ๋ ๊ฒ๋ณด๋ค ์ด์ ํ์ด์ง์ ์๋ ๊ฒ๋ค์ ์ ์งํ๋ผ๊ณ ์ง์ํ ์ ์๋ค.
์ด์ startTransition์ผ๋ก ํ์ํ ์์๋ฅผ ์ดํด๋ณด์๋ฉด ๋ฒํผ ํด๋ฆญ์ด Biography๊ฐ ๋ก๋๋ ๋๊น์ง ๊ธฐ๋ค๋ฆฐ๋ค.
Transition์ ๋ชจ๋ ์ฝํ ์ธ ๊ฐ ๋ก๋๋ ๋๊น์ง ๊ธฐ๋ค๋ ค์ฃผ์ง๋ ์์ง๋ง ์ด๋ฏธ ๋ ธ์ถ๋ ์ฝํ ์ธ ๋ฅผ ์จ๊ธฐ์ง ์์ ๋งํผ์ ๊ธฐ๋ค๋ ค์ค๋ค.
์๋ฅผ ๋ค๋ฉด, ๋ ์ด์์(Musci Broswer ํ ๋๋ฆฌ)์ ์ด๋ฏธ ๊ณต๊ฐ๊ฐ ๋์๊ธฐ ๋๋ฌธ์ ๋ก๋ฉ ์คํผ๋๋ก ํด๋น ์์ญ์ ๊ฐ๋ ค์ฃผ๋ ๊ฑด ์ข์ ์๊ฐ์ ์๋ ๊ฑฐ ๊ฐ๋ค. ํ์ง๋ง Albums๋ฅผ ๊ฐ์ธ๊ณ ์๋ ์ค์ฒฉ๋ Suspense boundary๋ ์๋ก์ด ์ฝํ ์ธ ์ด๊ธฐ ๋๋ฌธ์ Transition์ด ๊ธฐ๋ค๋ ค์ค ํ์๊ฐ ์๊ณ , ๋ก๋ฉ ์ค์ผ๋ ํค์ ๋ณด์ฌ์ค๋ค.
๐ Note
Suspense-enabled ๋ผ์ฐํฐ๋ค์ ๊ธฐ๋ณธ์ ์ผ๋ก Transition์ผ๋ก navigation update๊ฐ ๊ฐ์ธ์ ธ์ผ ํ๋ค.
์ฐ์ 6: Transition์ด ์ผ์ด๋๊ณ ์๋ค๋ ๊ฑธ ์๋ฆฌ๊ณ ์ถ์ ๋
์ง์ ์ ์ดํด๋ดค๋ ์์๋ ๋ฒํผ์ ํด๋ฆญํ๋ฉด ์ค์ ๋ก navigation์ด ์งํ ์ค์ด๋ผ๋ ๊ฑธ ์ ์ ์๋ ์๊ฐ์ ์ธ ํํธ๊ฐ ์๋ค.
์๊ฐ์ ์ธ ํํธ๋ฅผ ์ฃผ๊ณ ์ถ๋ค๋ฉด startTransition ๋์ useTransition์ ์ฌ์ฉํ๋ฉด isPending ๋ถ๋ฆฌ์ธ ๊ฐ์ ์ฃผ๋ useTransition์ ์ธ ์ ์๋ค.
๋ค์ ์์์์๋ navigation์ด ์งํ ์ค์ด๋ผ๋ ์๊ฐ์ ํํธ๋ก Music Browser ํค๋์ ์์์ ํํฌ์์ผ๋ก ๋ณ๊ฒฝํ๋ ์๋ฅผ ์ดํด๋ณด์.
// App.js
import { Suspense, useState } from 'react';
import IndexPage from './IndexPage.js';
import ArtistPage from './ArtistPage.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 (
<div className="layout">
// ์ถ๊ฐ๋ ๋ถ๋ถ
<section className="header" style={{
backgroundColor: isPending ? 'pink' : 'black'
}}>
Music Browser
</section>
<main>
{content}
</main>
</div>
);
}
function BigSpinner() {
return <h2>๐ Loading...</h2>;
}
// Index.js
export default function IndexPage({ navigate }) {
return (
<button onClick={() => navigate('/the-beatles')}>
Open The Beatles artist page
</button>
);
}
// ArtistPage.js
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>
<Biography artistId={artist.id} />
<Suspense fallback={<AlbumsGlimmer />}>
<Panel>
<Albums artistId={artist.id} />
</Panel>
</Suspense>
</>
);
}
function AlbumsGlimmer() {
return (
<div className="glimmer-panel">
<div className="glimmer-line" />
<div className="glimmer-line" />
<div className="glimmer-line" />
</div>
);
}
์ฐ์ 7: navigation์ด ๋ฐ์ํ ๋ Suspense boundary๋ค์ ๋ฆฌ์ ํ๊ณ ์ถ์ ๋
Transition ๋์ค์ ๋ฆฌ์กํธ๋ ์ด๋ฏธ ๋ ธ์ถ๋ ์ฝํ ์ธ ๋ฅผ ์จ๊ธฐ์ง ์์ผ๋ ค๊ณ ํ๋ค.
ํ์ง๋ง ๋ค๋ฅธ ํ๋ผ๋ฏธํฐ๋ก ๋ค๋ฅธ route์ผ๋ก navigate ํ๋ ค๊ณ ํ ๋ ๋ฆฌ์กํธํํ ์ด๋ฅผ ๋ค๋ฅธ ์ฝํ ์ธ ๋ผ๊ณ ์๋ ค์ฃผ๊ณ ์ถ์ ์ ์๋ค.
์ด๋ key๋ฅผ ํ์ฉํด์ ๋ํ๋ผ ์ ์๋ค.
<ProfilePage key={queryParams.id} />
์ ์ ํ๋กํ ํ์ด์ง์์ navigate ํ๊ณ ์๋๋ฐ ์ด๋ค ์์๊ฐ suspend ๋๋ค๊ณ ์๊ฐํด ๋ณด์.
๋ง์ฝ ์ ๋ฐ์ดํธ๊ฐ Transition์ผ๋ก ๊ฐ์ธ์ ธ ์๋ค๋ฉด ์ด๋ฏธ ๋ณด์ด๋ ์ฝํ ์ธ ๋ฅผ fallback์ผ๋ก ๋์ฒด๋์ง๋ ์๋๋ค.
์ด๋ ์์๋ ๋์์ด๋ค.
ํ์ง๋ง ์๋ก ๋ค๋ฅธ ์ ์ ํ๋กํ ๊ฐ์ navigate์ ํ๋ค๊ณ ๊ฐ์ ํด ๋ณด์.
์ด ๊ฒฝ์ฐ์๋ fallback์ด ๋ณด์ด๋ ๊ฒ์ด ์์ฐ์ค๋ฌ์ด ๋์ ๊ฐ๋ค.
์๋ฅผ ๋ค์ด, ํ๋์ ์ ์ ํ์๋ผ์ธ์ ๋ค๋ฅธ ์ ์ ์ ํ์๋ผ์ธ๊ณผ๋ ๋ค๋ฅธ ์ฝํ ์ธ ๋ค.
์ด๋ฅผ key๋ก ํน์ ํํ๋ค๋ฉด ๋ฆฌ์กํธ๊ฐ ๋ค๋ฅธ ์ ์ ๋ค ๊ฐ์ ํ๋กํ์ ๋ค๋ฅธ ์ปดํฌ๋ํธ๋ก ์ทจ๊ธํ๋๋ก ํ๊ณ ,
Suspense boundary๊ฐ navigation ๋์ค์ ๋ฆฌ์ ๋๋๋ก ํ๋ค.
Suspense๊ฐ ํตํฉ๋ ๋ผ์ฐํฐ๋ค์ ์ด๋ฅผ ์๋์ ์ผ๋ก ํ๊ณ ์์ ๊ฒ์ด๋ค.
์ฐ์ 8: ์๋ฒ ์๋ฌ ๋ฐ์ ์์ ํด๋ผ์ด์ธํธ์๋ง ํด๋นํ๋ ์ฝํ ์ธ ๋ฅผ ๋ณด์ฌ์ค fallback์ ์ ๊ณตํ๊ณ ์ถ์ ๋
streaming server rendering APIs๋ฅผ ์ฌ์ฉ ์ค์ด๊ฑฐ๋ ์ด์ ์์กดํ๊ณ ์๋ ํ๋ ์์ํฌ๋ฅผ ์ฌ์ฉํ๊ณ ์๋ค๋ฉด ๋ฆฌ์กํธ๋ ์๋ฒ์์ ๋ฐ์ํ๊ณ ์๋ ์๋ฌ๋ค๋ Suspense boundary๊ฐ ์ฒ๋ฆฌํ๋๋ก ํ ๊ฒ์ด๋ค.
๋ง์ฝ ์ปดํฌ๋ํธ๊ฐ ์๋ฒ ์ชฝ์ ์๋ฌ๋ฅผ ๋์ง๋ค๋ฉด ๋ฆฌ์กํธ๋ ์๋ฒ ๋ ๋๋ฅผ ํฌ๊ธฐํ๋ ๋์ ๊ฐ์ฅ ๊ฐ๊น์ด ์์์ Suspense ์ปดํฌ๋ํธ๋ฃฐ ์ฐพ์์ ์์ฑ๋ ์๋ฒ HTML์ ์คํผ๋์ ๊ฐ์ fallback์ ์ถ๊ฐํด ์ค๋ค.
์ ์ ๋ค์ ๊ฐ์ฅ ๋จผ์ ์ด ์คํผ๋๋ฅผ ๋ณด๊ฒ ๋ ๊ฒ์ด๋ค.
ํด๋ผ์ด์ธํธ ์ชฝ์์ ๋ฆฌ์กํธ๋ ๊ณ์ํด์ ๊ฐ์ ์ปดํฌ๋ํธ๋ฅผ ๋ ๋ ํ๋ ๊ฒ์ ์๋ํ ๊ฒ์ด๋ค.
ํด๋ผ์ด์ธํธ ์ชฝ์์์ ์๋ฌ๋ ๋ฆฌ์กํธ๋ ์๋ฌ๋ฅผ ๋์ง๊ณ ๊ฐ์ฅ ๊ฐ๊น์ด ๊ณณ์ error boundary๋ฅผ ๋ณด์ฌ์ค๋ค.
ํ์ง๋ง ํด๋ผ์ด์ธํธ ์ชฝ์ ์๋ฌ๊ฐ ์๋๋ผ๋ฉด fallback์ ๋ณด์ฌ์ค ๊ฑฐ์๋ค๊ณ ํด๋ ์จ๋ ์ฑ๊ณต์ ์ผ๋ก ๋ญ๊ฐ๋ฅผ ์ ๊ณตํ๊ธฐ ๋๋ฌธ์
๋ฆฌ์กํธ๋ ์ ์ ํํ ์๋ฌ๋ฅผ ๋ณด์ฌ์ฃผ์ง ์์ ๊ฒ์ด๋ค.
์ด ํน์ฑ์ ํ์ฉํด ํน์ ์ปดํฌ๋ํธ๊ฐ ์๋ฒ ์ชฝ์ ๋ ๋ ๋์ง ์๋๋ก ํ ์ ์๋ค.
๋ค์๊ณผ ๊ฐ์ด ์๋ฒ ํ๊ฒฝ์์ ์๋ฌ๋ฅผ ๋์ง๊ณ ์ด๋ฅผ 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์์๋ ๋ก๋ฉ ์ค์ด๋ผ๋ ํํธ (Loading fallback)์ด ์ ๊ณต๋๊ณ ,
ํด๋ผ์ด์ธํธ ์ชฝ์์๋ Chat ์ปดํฌ๋ํธ๋ก ๋์ฒด๋๋ค.
โ๏ธ ํธ๋ฌ๋ธ ์ํ
๐ฃ๏ธ ์ ๋ฐ์ดํธ ๋์ค์ fallback์ผ๋ก UI๊ฐ ๋์ฒด๋๋ ๊ฑด ์ด๋ป๊ฒ ๋ง๋์?
๋์ ๋ณด์ด๋ UI๋ฅผ fallback์ผ๋ก ๋ฐ๊ฟ์น๊ธฐํ๋ ๊ฑด ์ ์ ํํ ๊ทธ๋ค์ง ์ข์ ๊ฒฝํ์ ์๋๋ค.
์ด๋ ์ ๋ฐ์ดํธ๊ฐ ๋ฐ์ํ๋ฉด ์ปดํฌ๋ํธ๊ฐ suspend ๋์ด ๊ฐ์ฅ ๊ฐ๊น์ด Suspense boundary๊ฐ ์ด๋ฏธ ์ ์ ํํ ์ฝํ ์ธ ๋ฅผ ๋ณด์ฌ์ฃผ๊ณ ์๋ ๊ฒฝ์ฐ์ ์ผ์ด๋ ์ ์๋ค.
์ด๋ฐ ์ผ์ด ๋ฐ์ํ๋ ๊ฒ์ ๋ง๊ธฐ ์ํด์๋ ์ ๋ฐ์ดํธ๋ฅผ startTranstion์ผ๋ก ๊ฐ์ธ์ค์ ํด๋น ์ ๋ฐ์ดํธ๊ฐ ๊ธํ ์ผ์ด ์๋(non-urgent)๋ผ๋ ๊ฒ์ ์๋ ค์ฃผ๋ฉด ๋๋ค (mark the update as non-urgent using startTransition). Transition ๋์ค์ ๋ฆฌ์กํธ๋ ์ํ์ง ์๋ fallback์ด ๋ณด์ด๋ ์ํฉ์ ํผํ๊ธฐ ์ํด์ ์ถฉ๋ถํ ๋ฐ์ดํฐ๊ฐ ๋ก๋ฉ๋ ๋๊น์ง ๊ธฐ๋ค๋ฆฐ๋ค.
function handleNextPageClick() {
// If this update suspends, don't hide the already displayed content
startTransition(() => {
setCurrentPage(currentPage + 1);
});
}
์ด๋ฏธ ์ฐ์ 5์์ ๋ค๋ฃฌ ๋ด์ฉ์ด๊ธฐ๋ ํ์ง๋ง, ์ด๋ ๊ฒ ํ๋ฉด ์ด๋ฏธ ๋
ธ์ถ๋ ์ฝํ
์ธ ๋ฅผ ์จ๊ธฐ๋ ์ผ์ ํผํ ์ ์๋ค.
ํ์ง๋ง ์๋กญ๊ฒ ๋ ๋ ๋ Suspense boudnary๋ UI ๋ธ๋กํน์ ๋ฐฉ์งํ๊ธฐ ์ํด ์ฌ์ ํ fallback์ ๋ณด์ฌ์ฃผ๊ณ ์ฝํ ์ธ ๊ฐ ์ค๋น๋์ด์ผ ์ ์ ๊ฐ ๋ณผ ์ ์๋๋ก ํ๋ค.
์ฆ, ๋ฆฌ์กํธ๋ ๊ธํ์ง ์์(non-urgent) ์
๋ฐ์ดํธ์ ํ์์๋ง fallback์ด ๋ณด์ด์ง ์๊ฒ ํ๋ค.
๊ธํ ์ ๋ฐ์ดํธ๋ผ๋ฉด ๋ ๋๊ฐ ๋ฏธ๋ค์ง์ง ์๊ธฐ ๋๋ฌธ์ startTransition ๋ useDeferredValue๋ก ์ ๋ฐ์ดํธ๊ฐ ๊ธํ์ง ์๋ค๋ ๊ฑธ ํ์ํด์ผ ํ๋ค.
๋ผ์ฐํฐ๊ฐ Suspense์ ํตํฉ๋์ด ์๋ค๋ฉด ์๋์ ์ผ๋ก startTransition์ผ๋ก ์ ๋ฐ์ดํธ๋ฅผ ์๋์ ์ผ๋ก ๊ฐ์ธ๊ณ ์์ด์ผ ํ๋ค.
๐ References
<Suspense> – React
The library for web and native user interfaces
react.dev
๐ค TMI, ๋๋ ์
ํ.. ์ง๊ธ๊น์ง ๋ฆฌ์กํธ(nextjs โ)์์ tanstack-query+Suspense๋ฅผ ์ฐ๊ณ ์์๋๋ฐ ๋ถ์์ ํ feautre์๋ค๋..
์ ์์ ์ถฉ๊ฒฉ.. ๋ค์์๋ Error boundary๋ฅผ ์ดํด๋ด์ผ๊ฒ ๋ค. ํด๋ผ์ด์ธํธ ์ชฝ์์ ์๋ฌ๊ฐ ๋ฐ์ํ๋ฉด ์์์ ๊ฐ์ฅ ๊ฐ๊น์ด Error boundary๋ฅผ ๋ณด์ฌ์ค๋ค๊ณ ํ๋๋ฐ, vinylify์์ Error boundary ๋ฅผ ์ถ๊ฐํด์ฃผ์ง ์์๋..๐ฑ
'๐ฉ๐ปโ๐ป dev' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
๋ชจ๋ ์์คํ (0) | 2024.06.26 |
---|---|
โ๏ธ Error boundary (0) | 2024.06.19 |
โ๏ธ <Suspense>๋? (0) | 2024.06.15 |
์ ์ธ์ ์ธ ์ฝ๋ ์์ฑํ๊ธฐ (2) | 2024.06.13 |
[spotify-api] VINYLIFY : ์คํฌํฐํ์ด apiํ์ฉํ ๊ฒ์ + ์ฌ์ ํ๋ก์ ํธ(3) ์ค๋น๋ฌผ: authorization์ ๋ด๋นํ ์๋ฒ ์ฝ๋ (0) | 2024.06.12 |