React - Suspense
Suspense vs. Traditional conditional loading
1. Suspense Component π©βπ»
Suspense λ React κ° μ 곡νλ Wrapper Component λ‘ children
μ λ‘λ©μ΄ λλ λκΉμ§ λ λλ§ ν λ체 μ»΄ν¬λνΈλ₯Ό fallback
μΌλ‘
μ 곡νλ μ»΄ν¬λνΈλ‘ κΈ°λ³Έ Syntax λ λ€μκ³Ό κ°λ€.
<Suspense fallback={<Loading />}>
<SomeComponent />
</Suspense>
React 19 μμ μΆκ°λ μμ μΈ κΈ°λ₯λ€μ΄ μκΈ° λλ¬Έμ 19 λ²μ μ μ μΆμ μ΄μ μλ canary λ²μ μΌλ‘ μ€μΉν΄μΌ λͺ¨λ κΈ°λ₯μ μ¬μ©ν μ μλ€.
# React 19 λ²μ λ―Έλ§μΌ κ²½μ° canary λ₯Ό μ€μΉνλ€.
npm i react@canary react-dom@canary --legacy-peer-deps
2. Usage π©βπ»
1. Displaying a fallback while content is loading
Suspense κ° λ±μ₯νκΈ° μ Albums
μ»΄ν¬λνΈμ λ‘λ© fallback μ μ 곡νλ λ°©λ²μ λ€μκ³Ό κ°μλ€.
- Albums.jsx
export default function Albums({ artistId }) {
const [loading, setLoading] = useState(false);
const [albums, setAlbums] = useState();
const getAlbums = async () => {
setLoading(true);
const response = await fetchData(`/${artistId}/albums`);
setAlbums(response);
setLoading(false);
};
useEffect(() => {
getAlbums();
}, [getAlbums]);
if (loading) {
return <Loading />;
}
return (
<ul>
{albums?.map((album) => (
<li key={album.id}>
{album.title} ({album.year})
</li>
))}
</ul>
);
}
Suspense λ₯Ό λμ νλ©΄ μ μ»΄ν¬λνΈλ λΉμ¦λμ€ λ‘μ§κ³Ό fallback μ λΆλ¦¬ν μ μλ€.
- Albums.jsx
export default function Albums({ artistId }) {
const albums = use(fetchData(`/${artistId}/albums`));
return (
<ul>
{albums.map((album) => (
<li key={album.id}>
{album.title} ({album.year})
</li>
))}
</ul>
);
}
<Suspense fallback={<Loading />}>
<Albums />
</Suspense>
Albums
κ° λ‘λ©μ€μΌ λλ μλμΌλ‘ Loading
μ΄ λ λλ§ λκ³ , λ‘λ©μ΄ λλλ©΄ Albums
λ₯Ό λ λλ§ ν΄ κ΅μ²΄νλ€. μ΄λ₯Ό μ’ λ μ ννκ²
μ€λͺ
νλ©΄, Suspense μ children μ΄ λͺ¨λ λ‘λ©λ ν fallback μ΄ ν΄μ λλ€.
μ΄λ Suspense Component κ° λ°μ΄ν° λ‘λ©μ κ°μ§ν μ μλ 쑰건μ λ€μκ³Ό κ°λ€.
Relay
λNext.js
μ κ°μ Suspense-enabled frameworks κ° λ°μ΄ν°λ₯Ό fetching ν λ.lazy
ν€μλλ₯Ό μ¬μ©ν Lazy-loading Component λ₯Ό μ¬μ©ν λ.use
λ‘ wrapping ν Promise λ₯Ό μ½μ λ.
μ¬κΈ°μ use
λ React 18 λ²μ μλ μ‘΄μ¬νμ§ μλλ€. canary λ²μ μμ μ¬μ© κ°λ₯ν μ κ·
API λ‘ React 19 λ²μ μ μΆκ°λ μμ μ΄λ€.
λ°λΌμ Suspense with axios on React 18 μμ use
μν μ νλ
Wrapping ν¨μλ₯Ό λ§λ€μ΄ μ¬μ©νλ λ°©λ²μ ꡬνν κ²μ΄λ€. λ¨, use
μ λ€λ₯Έ μ μ use λ Promise λΏ μλλΌ Context λ₯Ό
Wrapping νλ λ°λ μ¬μ©ν μ μλ λ°λ©΄ μλμ ꡬνν ν¨μλ μ€μ§ Suspense λ₯Ό axios μ ν¨κ» μ¬μ©νκΈ° μν Wrapping ν¨μ λ€.
λ€μ μ½λλ₯Ό 보μ.
<Suspense fallback={<Loading />}>
<Biography />
<Panel>
<Albums />
</Panel>
</Suspense>
μ΄λ κ² children μ΄ μ¬λ¬ κ° μλ κ²½μ°λ μ΄λ»κ² μλν κΉ? Suspense μ children μ΄ λͺ¨λ λ‘λ©λ ν fallback μ΄ ν΄μ λλ€κ³ νλ€. νΉν μμ μ»΄ν¬λνΈ λΏ μλλΌ μμ μ»΄ν¬λνΈκΉμ§ λͺ¨λ λ€ λ‘λ©μ΄ μλ£λμ΄μΌ fallback μ ν΄μ ν μ μλ€.
λ°λΌμ μΌλΆ μ»΄ν¬λνΈμ λ‘λ©μ΄ μλ£ λλλΌλ μμμ ν¬ν¨ν λͺ¨λ νμ μ»΄ν¬λνΈκ° λ‘λ©λ λκΉμ§ κ³μ fallback μνμ λμ¬ μκ² λκ³ , μ¬μ©μλ μ±μ΄ λλ¦¬κ² λ°μν΄ μ’μ§ λͺ»ν μ¬μ©μ κ²½νμ νκ² λλ€.
2. Revealing nested content as it loads
κ·Έλ°λ° μ΄λ€ μ»΄ν¬λνΈλ λλ¦¬κ² λ‘λ©μ΄ λμ§λ§ μ΄λ€ μ»΄ν¬λνΈλ λΉ λ₯΄κ² λ‘λ©μ΄ λ μλ μλ€. νΉν API μλ΅μ΄ λλ¦° λΆλΆμ νλ‘ νΈμλμμ μ΄λ»κ² ν μ μλ λΆλΆμ΄ μλκΈ° λλ¬Έμ λΉ λ₯΄κ² λ‘λ© λ κ²λ€ λ¨Όμ 보μ¬μ£Όλ©° λ‘λ©μ΄ μλ£λ¨μ λ°λΌ μ μ§μ μΌλ‘ νλ©΄μ μ λ°μ΄νΈ νλ©΄ λ μ’μ μ¬μ©μ κ²½νμ μ κ³΅ν΄ μ€ μ μλ€.
Error Boundary
μ λ§μ°¬κ°μ§λ‘ Suspense
μμ μ€μ²©μ΄ κ°λ₯νλ€. κ·Έλ¦¬κ³ μ€μ²©λ κ²½μ° κ°μ₯ κ°κΉμ΄ μλ Suspense μ μν₯μ λ°μΌλ―λ‘
λͺ¨λ μμμ κ°μνμ§ μλλ€. λ¬΄μ¨ λ§μΈμ§ μ½λλ₯Ό 보μ.
<Suspense fallback={<BigSpinner />}>
<Biography />
<Suspense fallback={<AlbumsGlimmer />}>
<Panel>
<Albums />
</Panel>
</Suspense>
</Suspense>
Biography
κ° λ‘λ©λμ§ μμλ€λ©΄BigSpinner
κ° fallback λμ΄ μ 체 μμμ λμ νλ€.Biography
κ° λ‘λ©λλ©΄BigSpinner
fallback μ΄ ν΄μ λμ΄ content κ° λ³΄μ¬μ§λ€.- λ§μ½
Albums
κ° μμ§ λ‘λ©λμ§ μμλ€λ©΄AlbumGlimmer
κ° fallback λμ΄ children μ λ‘λ©μ κΈ°λ€λ¦°λ€. Albums
κ° λ‘λ©λλ©΄AlbumGlimmer
fallback μμ ν΄μ λμ΄ λͺ¨λ content κ° λ³΄μ¬μ§λ€.
3. useDeferredValue
λ°μ΄ν°λ₯Ό κ²μνλ λμ Loading
fallback μ΄ λ체νλ κ²½μ°λ₯Ό μκ°ν΄λ³΄μ.
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>
</>
);
}
μ΄ μ»΄ν¬λνΈλ μ΄μ κ³Ό λ¬λ¦¬ λ λλ§μ΄ μλ£λ μ΄νμλ κ³μν΄μ API μμ²μ ν μ μλ€. μ΄ λ§μ κ²μμ ν λλ§λ€ Promise λ₯Ό μμ±νκ³ , λ§€λ² fallback μ΄ λ λλ§ λλ€λ κ²μ΄λ€.
μ΄λ―Έ λ λλ§ λ μ»΄ν¬λνΈκ° λ§€λ² μ§μμ§κ³ μλ‘κ² κ·Έλ €μ§λ κ²μ μ±λ₯μλ μν₯μ λ―ΈμΉμ§λ§ 무μλ³΄λ€ UI μ μ’μ§ λͺ»νλ€λ λ¬Έμ μ μ κ°λλ€. λ°λΌμ fallback μΌλ‘ λ체λμ§ μκ³ , μ λ°μ΄ν°κ° λ‘λ©λμ΄ update λ λκΉμ§ UI μ λ°μ΄νΈλ₯Ό μ§μ°μν€λ κ²μ΄ μ’λ€.
const deferredValue = useDeferredValue(value)
useDeferredValue λ UI μ λ°μ΄νΈλ₯Ό μ§μ°μν€λ Hooks λ‘ debounce ν¨μμ λ¬λ¦¬ λΉμ¦λμ€ λ‘μ§μ μ§μ°μν€μ§ μκ³ UI μ λ°μ΄νΈλ§ μ§μ°μν¨λ€.
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=>
<SearchResults query={deferredQuery} />
</div>
</Suspense>
</>
);
}
μ΄μ λμ΄μ fallback μΌλ‘ λ체λμ§ μκ³ λ‘λ©μ΄ μλ£λ λκΉμ§ UI μ
λ°μ΄νΈλ₯Ό μ§μ°μν¨λ€. λ¨ μ΄ κ²½μ° μ΄λ―Έ λ‘λ©λ μ»΄ν¬λνΈμΌ λλ§ UI μ
λ°μ΄νΈλ₯Ό
μ§μ°μν€λ κ²μ΄ μλκ³ useDefereedValue
μ μν΄ μ
λ°μ΄νΈ λλ λͺ¨λ UI μ
λ°μ΄νΈλ₯Ό μ§μ°μν€κΈ° λλ¬Έμ μ μ½λμ fallback μ νμ λ체κ°
λμ§ μλλ€. λ§μ½ fallback μΌλ‘ λμ²΄κ° λλ €λ©΄ useDefereedValue
κ° μλ λ€λ₯Έ state
μ μν μ
λ°μ΄νΈκ° μ΄λ£¨μ΄μ ΈμΌνλ€.
4. startTransition
useState
μ μ
λ°μ΄νΈμ μν UI μ
λ°μ΄νΈ μ§μ°μ useDeferredValue
ν
μ μ¬μ©ν΄ ν΄κ²°ν μ μμλ€. κ·Έλ°λ° μ»΄ν¬λνΈ νΈλ¦¬ ꡬ쑰μμ
νΉμ μ»΄ν¬λνΈλ₯Ό μ§μ°μν€κ³ μ ν λλ μ΄λ»κ² ν΄μΌν κΉ? μλ μ½λλ₯Ό 보μ.
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=
/>
);
}
return (
<Layout>
{content}
</Layout>
);
}
function BigSpinner() {
return <h2>π Loading...</h2>;
}
μ μ½λλ₯Ό 보면 λΌμ°ν°λ₯Ό Suspense μ»΄ν¬λνΈκ° κ°μΈκ³ μκ³ fallback μΌλ‘ BigSpinner
λ₯Ό μ 곡νκ³ μλ€. κ·Έλ¦¬κ³ λΌμ°ν°λ μΈμ λ
맀μΉλλ μ»΄ν¬λνΈλ₯Ό Layout
μ»΄ν¬λνΈλ‘ κ°μΈκΈ° λλ¬Έμ μ»΄ν¬λνΈκ° λ€μ λ λλ§ λλλΌλ Layuout
μ λ³ν¨ μμ΄ κ³΅ν΅μΌλ‘ λ λλ§ λλ
κ²μ μ μ μλ€.
νμ§λ§ children μ ν΅μ§Έλ‘ fallback μΌλ‘ λ체νκΈ° λλ¬Έμ Router
μ»΄ν¬λνΈλ ν΅μ§Έλ‘ BigSpinner
λ‘ λ체λλ€. μ§μμ§ νμκ° μλ
λ μ΄μμκΉμ§ λͺ¨λ fallback λμ΄λ²λ¦¬λ λ§μΉ SSR μ μ¬μ©νλ κ²κ³Ό κ°μ μ’μ§ λͺ»ν μ¬μ©μ κ²½νμ νκ² λλ€.
μ¦, νμ¬ μνλ Revealing nested content as it loads μ κ°λ€κ³ ν μ μλ€.
Layout
μ fallback μ΄ λμ§ μκ³ νμ μ»΄ν¬λνΈλ§ fallback λκ² νλ €λ©΄ μ΄λ»κ² ν΄μΌ ν κΉ? κ°μ₯ κ°λ¨ν λ°©λ²μ Router
λ₯Ό
Suspense
λ‘ κ°μΈμ§ μλ κ²μ΄λ€. κ·Έλ¬λ©΄ νμ μ»΄ν¬λνΈκ° κ°κ³ μλ Suspense μ fallback λ§ μλν κ²μ΄λ€.
λμ μ΄ κ²½μ° Layout
μ fallback μ΄ λμ§ μκΈ° λλ¬Έμ 첫 λ‘λ©μμλ fallback λμ²΄κ° λΆκ°λ₯νλ€. λ§μ½ 첫 λ‘λ©μμλ fallback μ
μ¬μ©νλ, μ΄ν νμ μ»΄ν¬λνΈ μ
λ°μ΄νΈ μ μ΄λ―Έ λ λλ§ λ content λ₯Ό μ μ§ν μ±λ‘ νμ fallback λ§ λ체νλλ‘ νκ³ μΆλ€λ©΄ μ΄λ»κ² ν΄μΌν κΉ?
useDeferredValue
μTransitions
μ UI μ λ°μ΄νΈλ₯Ό μ§μ°μν¨λ€.
μ¬κΈ°μ μ°λ¦¬μκ² νμν κ²μ Transition
μ΄λ€.
startTransition(scope)
startTransition μ UI λΈλ‘νΉ μμ΄ state λ₯Ό μ λ°μ΄νΈ νλλ‘ λ§λλ React API λ€. Hooks κ° μλλΌ API μμ μ μνλλ‘ νμ.
useDefferedValue
λ state
λ₯Ό parameter λ‘ λ°μ μ λ³μλ₯Ό λ°ννλ€. startTransition
μ setState
λ₯Ό closure ννλ‘
λ°μΌλ©° API μ λ°λ‘ λ±λ‘λκΈ° λλ¬Έμ λ³λμ λ°νκ°μ μλ€.
startTransition(() => {
// β
Setting state *during* startTransition call
setPage('/about');
});
μ΄λ―Έ λ λλ§ λ μ»΄ν¬λνΈμ λΆνμν fallback λμ²΄κ° λ°μνμ§ μλλ‘ startTransition
μ μ μ©ν΄λ³΄μ.
export default function App() {
return (
<Suspense fallback={<BigSpinner />}>
<Router />
</Suspense>
);
}
function Router() {
const [page, setPage] = useState('/');
function navigate(url) {
startTransition(() => {
setPage(url);
});
}
// ...
}
μ΄μ Transitions κ° μμλλ©° UI μ
λ°μ΄νΈκ° μ§μ°λμ΄ BigSpinner
fallback μ λ체λμ§ μκ³ νμ μ»΄ν¬λνΈ ArtistPage
μ
μλ Albums
λ₯Ό κ°μΈκ³ μλ Suspense μ AlbumsGlimmer
fallback λ§ λ체λλ€. μ΄λ μ£Όμν΄μΌ ν κ²μ΄ Layout
μ΄ fallback
μΌλ‘ λ체λμ§ μμλ€κ³ μμ λ λ ν¨μ νΈμΆ μμ²΄κ° μ΄λ£¨μ΄μ§μ§ μλ κ²μ μλλ€!
λ λ ν¨μμ νΈμΆμ μ μμ μΌλ‘ μ΄λ£¨μ΄μ§μ§μ§λ§ fallback μΌλ‘ λ체λλ λμ UI μ λ°μ΄νΈλ₯Ό μ§μ°μν€λ κ² λΏμ΄λ€. κ·Έλ¦¬κ³ μ΄κ²μ μ΄λ―Έ κ·Έλ €μ§ UI μΌ λλ§ μ§μ°λκΈ° λλ¬Έμ μμ§ UI κ° κ·Έλ €μ§μ§ μμ ν΄λΉ Suspense κ° μ΅μ΄λ‘ μλνλ μμ μλ fallback μΌλ‘ λμ²΄κ° λλ€. μ¦, ν΄λΉ νμ΄μ§ 첫 λ°©λ¬Έ μμλ Layout μ fallback κ³Ό μμ μ»΄ν¬λνΈμ fallback μ΄ λͺ¨λ μλνμ§λ§, μ΄νμλ μμμ fallback λ§ μλνλ κ²μ΄λ€.
리μ‘νΈλ‘ μ±μ λ§λ€ λ νμ΄μ§ μ΄λ μ Transitions λ₯Ό μ¬μ©νλ κ²μ΄ κΆμ₯λλ λ° λ κ°μ§ μ΄μ λ λ€μκ³Ό κ°λ€.
- λ λλ§μ΄ μλ£λκΈ° μ μ¬μ©μκ° λ€λ₯Έ κ²μ ν΄λ¦νλ©΄ Transition μ μ€λ¨ν μ μλ€ (λ§μ½ Transition μ μ¬μ©νμ§ μλλ€λ©΄ μ¬μ©μμ ν΄λ¦ μ λ ₯μ μ λ¬λμ§λ§ μ΄μ μ ν΄λ¦ν UI μ λ°μ΄νΈ μ§μ°μ΄ μ€λ¨λμ§ μμ κΈ°λ€λ¦° ν μμ°¨μ μΌλ‘ μ λ°μ΄νΈλλ€).
- Transition μ μμΉ μλ loading indicator κ° λνλμ§ μκ² ν μ μλ€.
λΌμ°ν°μ Suspense μ Transitions λ₯Ό μ μ©νλ κ²μ Building a Suspense-enabled router λΌ νλ€.
startTransition μ¬μ©μ μ μμ λ κ°μ§λ λ€μκ³Ό κ°λ€.
1 ) startTransition is synchronous
μ΄λ μ£Όμν΄μΌ ν κ²μ΄ startTransition
μ λ°λμ Synchronous μ¬μΌ νλ€. λ°λΌμ λ€μμ
νμ©λμ§ μλλ€.
startTransition(() => {
// β Setting state *after* startTransition call
setTimeout(() => {
setPage('/about');
}, 1000);
});
λ§μ½, setTimeout μ μ μ©ν΄μΌ νλ€λ©΄ μμλ₯Ό λ°κΏ startTransition
μ λ°λμ Synchronous νλλ‘ ν΄μΌ νλ€.
setTimeout(() => {
startTransition(() => {
// β
Setting state *during* startTransition call
setPage('/about');
});
}, 1000);
λ§μ°¬κ°μ§λ‘ async/await
μμ Asynchronous μ½λμ΄κΈ° λλ¬Έμ νμ©λμ§ μλλ€.
startTransition(async () => {
await someAsyncFunction();
// β Setting state *after* startTransition call
setPage('/about');
});
μ΄λ κ² startTransition
λ°μΌλ‘ λΉΌλ΄ λ°λμ Synchronous μ¬μΌ νλ€.
await someAsyncFunction();
startTransition(() => {
// β
Setting state *during* startTransition call
setPage('/about');
});
κ·Έλ¦¬κ³ startTransition
μ μ¬μ©ν λ μ£Όμν΄μΌ ν κ²μ΄ μ΄κ²μ λΈλΌμ°μ API μΈ setTimeout
κ³Ό λ¬λ¦¬ λμ€μ μ€νλμ§ μλλ€λ κ²μ΄λ€.
UI μ
λ°μ΄νΈλ₯Ό μ§μ°μν€κΈ° λλ¬Έμ νΌλν μ μμ§λ§ startTransition μμ λκΈ°λ‘ μλνλ€.
λ§μΉ context μ 체λ₯Ό async λ‘ κ°μΈκ³ await μ μ¬μ©ν κ²κ³Ό μ μ¬νλ€.
console.log(1);
startTransition(() => {
console.log(2);
setPage('/about');
});
console.log(3);
1
2
3
λκΈ°μ μΌλ‘ μλνκΈ° λλ¬Έμ 1, 3, 2 μμκ° μλ 1, 2, 3 μμλ‘ μ½λκ° μ€νλλ€. λ¨μ§ UI μ λ°μ΄νΈ μ§μ°μ΄ λλ μ΄νμ λ€μ μ½λκ° μ€νλλλ‘ μ μ μ§μ°λ λΏμ΄λ€.
2 ) Updating an input in a Transition doesnβt work
λ λ€λ₯Έ μ£Όμμ μ input μ λκΈ°μ μΌλ‘ μ¦μ μ
λ°μ΄νΈ λκΈ° λλ¬Έμ startTransition
κ³Ό
ν¨κ» μ¬μ©ν μ μλ€. Transitions κ° non-blocking μ΄κΈ° λλ¬Έμ΄λ€.
const [text, setText] = useState('');
// ...
function handleChange(e) {
// β Can't use Transitions for controlled input state
startTransition(() => {
setText(e.target.value);
});
}
// ...
return <input value={text} onChange={handleChange} />;
μ΄λ° μν©μμ ν΄κ²° λ°©λ²μ λ κ°μ§κ° μλ€.
- μ΄ κ²½μ° λ κ°μ
state
λ₯Ό λ§λ€μ΄ νλλ κ°μ μ¦μ μ λ°μ΄νΈ νκ³ , λ€λ₯Έ νλλ Transitions UI λ λλ§ λ‘μ§μ μ¬μ©νλλ‘ νλ€. - νλμ
state
λ₯Ό μ¬μ©νλ λμ useDeferredValue λ₯Ό μΆκ°ν΄ μ€μ κ°μ κ°μΆλ€.
5. useTransition
startTransition
API λ₯Ό μ¬μ©νλ©΄ Suspense
μ fallback μ μ μ΄ν μ μλ€. νμ§λ§ μ¬μ ν μμ¬μμ΄ μλλ° λ‘λ© μ€μ΄λΌλ μκ°μ
indicator κ° μλ€ λ³΄λ UI μ
λ°μ΄νΈ μ§μ°μΌλ‘ μΈν΄ μ±μ λ°μμ΄ λ리거λ ν΄λ¦μ λ°μμ΄ μλ κ²μΌλ‘ μ€ν΄ν μ μλ€λ μ μ΄λ€.
μ΄μ λν λμμΌλ‘ κ°μ₯ μ’μ λ°©λ²μ useTransition
Hooks λ₯Ό μ¬μ©νλ κ²μ΄λ€.
startTransition
μ React API μμΌλ useTransition
μ Hooks λΌλ κ²μ μ μνμ. useTransition
μ startTransition
μ
λ°ννκΈ° λλ¬Έμ μ§μ import ν νμκ° μμΌλ©° μ¬μ© λ°©λ²μ λ€μκ³Ό κ°λ€.
const [isPending, startTransition] = useTransition()
function Router() {
const [page, setPage] = useState('/');
const [isPending, startTransition] = useTransition();
function navigate(url) {
startTransition(() => {
setPage(url);
});
}
// ...
return (
<Layout isPending={isPending}>
{content}
</Layout>
);
}
export default function Layout({ children, isPending }) {
return (
<div className="layout">
<section className="header" style=>
Music Browser
</section>
<main>
{children}
</main>
</div>
);
}
μ΄μ Layout μ ν€λλ₯Ό ν΅ν΄ Loading indicator λ₯Ό ννν μ μκ² λμλ€. μ°Έκ³ λ‘ μ¬κΈ°μ isPending
μ΄ true
μΈ κΈ°κ°μ Layout
μ»΄ν¬λνΈκ° μν Suspense μ fallback μ΄ νμλμ΄μΌ νλ κΈ°κ° λμμ ν΄λΉνλ€. μ¦, BigSpinner
λ‘ λ체λλ λμ isPending
indicator λ₯Ό μ¬μ©ν΄ λ‘λ©μ€μ μκ°μ μΌλ‘ ννν΄μ£Όλ κ²μ΄λ€. isPending
μ νμ μ»΄ν¬λνΈμ μλ Suspense μ AlbumsGlimmer
fallback κ³Όλ 무κ΄νλ€.
useTransition μ¬μ©μ μ μμ λ κ°μ§λ λ€μκ³Ό κ°λ€.
1 ) The useTransition
should be called inside a component
useTransition
μ Hooks μ΄κΈ° λλ¬Έμ λ°λμ μ»΄ν¬λνΈ μμμλ§ νΈμΆλμ΄μΌνλ€. μλ₯Ό λ€μ΄ data library μ κ°μ΄ μ»΄ν¬λνΈ λ°μμ
νΈμΆμ΄ νμνλ€λ©΄ Hooks λμ API μΈ startTransition
μ μ¬μ©ν΄μΌνλ©°, μ΄ κ²½μ° isPending
indicator λ μ¬μ©ν μ μλ€.
2 ) Suspense integrated router should wrap its updates into startTransition
automatically
λΌμ°ν°κ° Suspense μ ν΅ν©λ κ²½μ° λΌμ°ν° μ
λ°μ΄νΈλ₯Ό startTransition
μΌλ‘ wrapping ν΄μΌ νλ€.
6. key props
Suspense
μ Transitions
λ₯Ό μ¬μ©νλ©΄ μ΄λ―Έ λ λλ§ λ μ»΄ν¬λνΈμ UI μ
λ°μ΄νΈλ₯Ό μ§μ°μμΌ fallback λμ²΄κ° μλνμ§ μλλ‘ ν μ μλ€.
νμ§λ§ λλ‘λ κ°μ UI λΌ νλλΌλ λ€μ λ λλ§ λλ κ²μ΄ λ μ’μ UI κ° λ λκ° μλ€. μλ₯Ό λ€μ΄ μ΄λ€ μ¬μ©μμ μ 보λ₯Ό 보λ ProfilePage
μ»΄ν¬λνΈκ° μλ€κ³ ν΄λ³΄μ. λμΌ μ¬μ©μμ μ¬λ¬ μ 보λ₯Ό 보λ λμ μ΄ μ¬μ©μμ 곡ν΅μ μΈ μ 보λ₯Ό λνλ΄λ μμ μ»΄ν¬λνΈμ content λ fallback μ΄
μλνμ§ μλ κ²μ΄ μ’λ€. μΈλΆ μ 보μ μ
λ°μ΄νΈλ§ fallback μ΄ λ°μνλ κ²μ΄ μ’μ UI λ€.
νμ§λ§ λ€λ₯Έ μ¬μ©μλ‘ λ³κ²½λ κ²½μ°λ μ΄λ¨κΉ? λμΌν UI μ΄κΈ° λλ¬Έμ μ€νλ € fallback μ΄ μμ κ²½μ° λ³νκ° ν¬μ§ μλ€λ©΄ UI μ λ°μ΄νΈλ₯Ό μΈμ§νμ§ λͺ» ν κ°λ₯μ±μ΄ μλ€. μ΄λ° κ²½μ° μλμ μΌλ‘ fallback λ체λ₯Ό μλμμΌ UI μ λ°μ΄νΈκ° μ΄λ£¨μ΄μ‘μμ ννν΄μ£Όλ κ²μ΄ νμ€ν UI μ λ°μ΄νΈλ₯Ό μΈμ§μμΌ λ μ’μ μ¬μ©μ κ²½νμ κ°κ² νλ€.
μ΄λ»κ² ꡬλΆν μ μμκΉ? μ»΄ν¬λνΈμ iterating μ key
props λ₯Ό μ¬μ©ν΄ μ»΄ν¬λνΈλ₯Ό ꡬλΆν μ μλ κ³ μ κ°μ μ 곡νλ€. μ΄ κ²½μ°λ λμΌνκ²
key
props λ₯Ό μ¬μ©νλ©΄ Suspense λ Transition μ μ¬μ©νλλΌλ key
κ° λ³κ²½λλ©΄ λ€λ₯Έ μ»΄ν¬λνΈλ‘ μΈμν΄ fallback λ체λ₯Ό μννλ€.
<ProfilePage key={queryParams.id} />
μ΄λ κ² key
props λ§ μ 곡νλ©΄ λ³κ²½ μ¬λΆλ₯Ό 리μ‘νΈκ° νμΈ ν fallback λ체 μ¬λΆλ₯Ό νλ¨νλ€.
7. Error Boundary
useTransition
μ μν Error Boundary λ 리μ‘νΈ 19 μ μΆκ°λ μμ μΌλ‘ νμ¬λ canary λ²μ μμλ§ μ¬μ©μ΄ κ°λ₯νλ€.
npm i react-error-boundary
곡μ λ¬Έμ μμλ ν΄λμ€ μ»΄ν¬λνΈλ‘ μ§μ ꡬνν기보λ€λ bvaughn / react-error-boundary λΌμ΄λΈλ¬λ¦¬λ₯Ό μ¬μ©νκΈ°μ μ¬κΈ°μλ λμΌνκ² μ§ννκ³ μ νλ€. νΉμ ErrorBoundary cannot be used as a JSX component μλ¬κ° λ°μνλ©΄ λ§ν¬λ₯Ό μ°Έμ‘°ν΄ ν΄κ²°νλλ‘ νλ€.
export function AddCommentContainer() {
return (
<ErrorBoundary fallback={<p>β οΈSomething went wrong</p>}>
<AddCommentButton />
</ErrorBoundary>
);
}
function addComment(comment) {
if (comment == null) {
throw new Error("Example Error: An error thrown to trigger error boundary");
}
}
function AddCommentButton() {
const [pending, startTransition] = useTransition();
return (
<button
disabled={pending}
onClick={() => {
startTransition(() => {
addComment();
});
}}
>
Add comment
</button>
);
}
3. Suspense with axios on React 18 π©βπ»
use
λ νμ¬ canary μμλ§ μ¬μ©μ΄ κ°λ₯νλ€. νμ§λ§ κΈ°λ₯μ μμ λͺ» μ°λ 건 μλκ³ React κ° κ³΅μμ μΌλ‘ μ 곡νλ κΈ°λ₯μΌλ‘ Promise λΏ μλλΌ
Context μλ μ¬μ© κ°λ₯ν ν¨μμΌ λΏ νμ¬λ Suspense μ Promise λ₯Ό 컨νΈλ‘€ νκΈ° μν΄ Wrapping ν¨μ use
λ₯Ό λ§λ€μ΄ μ¬μ©ν μ μλ€.
곡μ λ¬Έμμμ μνλ‘ λ§λ€μ΄ μ¬μ©νλ ν¨μλ λ€μκ³Ό κ°μ ννλ₯Ό λκ³ μλ€.
export function use(promise) {
if (promise.status === "fulfilled") {
return promise.value;
} else if (promise.status === "rejected") {
throw promise.reason;
} else if (promise.status === "pending") {
throw promise;
} else {
promise.status = "pending";
promise.then(
(result) => {
promise.status = "fulfilled";
promise.value = result;
},
(reason) => {
promise.status = "rejected";
promise.reason = reason;
},
);
throw promise;
}
}
TypeScript νκ²½μ΄λ©° Axios λΌμ΄λΈλ¬λ¦¬λ₯Ό μ¬μ©νλ€λ©΄ λ€μκ³Ό κ°μ΄ Wrapping ν¨μ use
λ₯Ό λ§λ€μ΄ μ¬μ©ν μ μλ€.
import { AxiosPromise } from "axios";
export const use = <T>(promise: AxiosPromise<T>) => {
let status = "pending";
let result: T;
const suspender = promise
.then((response) => {
status = "success";
result = response.data;
})
.catch((response) => {
status = "error";
result = response.error;
});
return () => {
switch (status) {
case "pending":
throw suspender;
case "success":
return result;
case "error":
return result as any;
default:
throw new Error("Unknown status");
}
};
};
<Suspense fallback={<Loading />}>
<Purchased />
<button onClick={() => navigate("/")}>첫 νμ΄μ§λ‘</button>
</Suspense>
const Purchased: React.FC = () => {
const [orderHistory, setOrderHistory] = useState<OrderInfo[]>();
const { totals } = useContext(OrderContext);
const getData = async () => {
const promise = axios.post("/order", { price: totals.total });
setOrderHistory(use(promise));
};
useEffect(() => {
getData();
}, []);
return (
<PurchasedWrapper>
<h2>μ£Όλ¬Έμ΄ μλ£λμμ΅λλ€</h2>
<h3>μ£Όλ¬Έ λ΄μ</h3>
<div className="order-history">
<h5>μ£Όλ¬Έ λ²νΈ</h5>
<h5>μ£Όλ¬Έ κΈμ‘</h5>
{orderHistory?.map((orderInfo) => (
<Fragment key={orderInfo.orderNumber}>
<div>{orderInfo.orderNumber}</div>
<div>{orderInfo.price}</div>
</Fragment>
))}
</div>
</PurchasedWrapper>
);
};
Reference
- βSuspense.β React.dev Docs. accessed Jun. 03, 2024, https://react.dev/reference/react/Suspense.
- βuseTransition.β React.dev Docs. accessed Jun. 03, 2024, https://react.dev/reference/react/useTransition.