๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ

๐Ÿ‘ฉ๐Ÿป‍๐Ÿ’ป dev

โš›๏ธ <Suspense> ํ™œ์šฉ

์ด์ „ ๊ธ€: 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>
    );
  }
}

 

๋™์ž‘์€ ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

์“ฐ์ž„ 1: ์ปจํ…์ธ ๊ฐ€ ๋กœ๋”ฉ๋˜๊ณ  ์žˆ๋Š” ๋™์•ˆ ๋ณด์—ฌ์ค„ fallback์„ ๋ณด์—ฌ์ฃผ๊ณ  ์‹ถ์„ ๋•Œ

โš ๏ธ ์ฃผ์˜ํ•  ์ 

  • Suspense-enabled ๋ฐ์ดํ„ฐ ์†Œ์Šค๋งŒ Suspense ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ž‘๋™์‹œํ‚จ๋‹ค.
    • Suspense-enabled ๋ฐ์ดํ„ฐ ์†Œ์Šค์—๋Š”:
      • ๋ฐ์ดํ„ฐ fetch์™€ Suspense-enabled ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ ์ง€์›ํ•˜๋Š” Relay๋‚˜ Next.js์™€ ๊ฐ™์€ ํ”„๋ ˆ์ž„์›Œํฌ
      • lazy๋กœ ๊ตฌํ˜„ํ•œ Lazy-loading ์ปดํฌ๋„ŒํŠธ
      • use (canary, ์•„์ง์€ ์‹คํ—˜์ ์ธ ์š”์†Œ)๋กœ Promise๊ฐ’์„ ์ฝ์„ ๋•Œ
    • 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 ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋…ธ์ถœ๋  ์ค€๋น„๊ฐ€ ๋˜์–ด์„œ์•ผ ๋น„๋กœ์†Œ ํ•œ๊บผ๋ฒˆ์— ๋ชจ๋‘ ๋‚˜ํƒ€๋‚œ๋‹ค.

์“ฐ์ž„ 2: ์ปจํ…์ธ ๊ฐ€ ๋กœ๋”ฉ๋˜๊ณ  ์žˆ๋Š” ๋™์•ˆ ๋ณด์—ฌ์ค„ fallback์„ ๋ณด์—ฌ์ฃผ๊ณ  ์‹ถ์„ ๋•Œ

// ํ•˜๋‚˜์˜ 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์˜ ์ฝ˜ํ…์ธ ๊ฐ€ ๊ธฐ๋‹ค๋ ค์ค„ ํ•„์š”๊ฐ€ ์—†์–ด์ง„๋‹ค.

 

์‹œํ€€์Šค ํ๋ฆ„์€ ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

  1. Biography ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋กœ๋”ฉ๋˜์ง€ ์•Š์•˜๋‹ค๋ฉด BigSpinner๊ฐ€ ์ „์ฒด ์ฝ˜ํ…์ธ ๋ฅผ ๋Œ€์‹ ํ•ด์„œ ๋…ธ์ถœ๋œ๋‹ค.
  2. Biography ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋กœ๋”ฉ์„ ๋งˆ์น˜๋ฉด ํ•ด๋‹น ์ฝ˜ํ…์ธ ๋กœ BigSpinner๋ฅผ ๋Œ€์ฒดํ•œ๋‹ค.
  3. Albums์ด ์•„์ง ๋กœ๋“œ๋˜์ง€ ์•Š์•˜๋‹ค๋ฉด, AlbumsGlimmer๊ฐ€ Albums์™€ Albums์˜๋ถ€๋ชจ ์ปดํฌ๋„ŒํŠธ์ธ Panel ๋Œ€์‹ ์— ๋…ธ์ถœ๋œ๋‹ค.
  4. Albums์ด ๋กœ๋”ฉ์„ ๋งˆ์น˜๋ฉด AlbumsGlimmer๋ฅผ ํ•ด๋‹น ์ฝ˜ํ…์ธ ๋กœ ๋Œ€์ฒดํ•œ๋‹ค.

์“ฐ์ž„ 3: ๋กœ๋”ฉ๋˜๊ณ  ์žˆ๋Š” ๋™์•ˆ ์ค‘์ฒฉ(nested)๋œ ์ฝ˜ํ…์ธ ๋ฅผ ๋…ธ์ถœํ•˜๊ณ  ์‹ถ์„ ๋•Œ

 

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>
    </>
  );
}

 

์“ฐ์ž„ 4: ์ƒˆ (fresh) ์ฝ˜ํ…์ธ ๋ฅผ ๋กœ๋”ฉํ•˜๊ณ  ์žˆ๋Š” ๋™์•ˆ ์ง์ „(stale) ์ฝ˜ํ…์ธ ๋ฅผ ๋ณด์—ฌ์ค„ ๋•Œ

 

์ƒˆ๋กœ์šด ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์ค€๋น„๋  ๋•Œ๊นŒ์ง€ ์ด์ „์˜ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๋ฅผ ๋ณด์—ฌ์ค˜์„œ ๋ฆฌ์ŠคํŠธ ์—…๋ฐ์ดํŠธ๋ฅผ 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>
    </>
  );
}

 

์“ฐ์ž„ 4: ์ƒˆ (fresh) ์ฝ˜ํ…์ธ ๋ฅผ ๋กœ๋”ฉํ•˜๊ณ  ์žˆ๋Š” ๋™์•ˆ ์ง์ „(stale) ์ฝ˜ํ…์ธ ๋ฅผ ๋ณด์—ฌ์ค„ ๋•Œ

 

๐Ÿ“ 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>
  );
}

 

์“ฐ์ž„ 5: ์ด๋ฏธ ๋…ธ์ถœ๋œ ์ฝ˜ํ…์ธ ๋ฅผ ์ˆจ๊ธฐ๋Š” ๊ฑธ ๋ฐฉ์ง€ํ•˜๊ณ  ์‹ถ์„ ๋•Œ

 

์ด๋ ‡๊ฒŒ ์ƒ์œ„์˜ Suspense boundary์˜ fallback์ด ์ „์ฒด ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋Œ€์ฒดํ•˜๋Š” ์ƒํ™ฉ์„ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” 

navigation state update๋ฅผ Transition์œผ๋กœ ํ‘œ์‹œํ•ด ๋‘˜ ์ˆ˜ ์žˆ๋‹ค.

function Router() {
  const [page, setPage] = useState('/');

  function navigate(url) {
    startTransition(() => {
      setPage(url);      
    });
  }
  // ...

 

startTransition์œผ๋กœ ํ‘œ์‹œํ•จ์œผ๋กœ์จ ๋ฆฌ์•กํŠธํ•œํ…Œ  ์ƒํƒœ๋ณ€ํ™˜(state transition)์ด ๊ธ‰ํ•œ ๊ฒŒ ์•„๋‹ˆ๋ผ๋Š” ๊ฑธ ์•Œ๋ ค์ค„ ์ˆ˜ ์žˆ๊ณ ,

์ด๋ฏธ ๋…ธ์ถœ๋œ ์ฝ˜ํ…์ธ ๋“ค์„ ์ˆจ๊ธฐ๋Š” ๊ฒƒ๋ณด๋‹ค ์ด์ „ ํŽ˜์ด์ง€์— ์žˆ๋˜ ๊ฒƒ๋“ค์„ ์œ ์ง€ํ•˜๋ผ๊ณ  ์ง€์‹œํ•  ์ˆ˜ ์žˆ๋‹ค.

 

์ด์ œ startTransition์œผ๋กœ ํ‘œ์‹œํ•œ ์˜ˆ์‹œ๋ฅผ ์‚ดํŽด๋ณด์ž๋ฉด ๋ฒ„ํŠผ ํด๋ฆญ์ด Biography๊ฐ€ ๋กœ๋“œ๋  ๋•Œ๊นŒ์ง€ ๊ธฐ๋‹ค๋ฆฐ๋‹ค.

startTransition์œผ๋กœ ์ด๋ฏธ ๋…ธ์ถœ๋œ ์ฝ˜ํ…์ธ ๊ฐ€ ์ˆจ๊ฒจ์ง€์ง€ ์•Š๋„๋ก ํ•  ์ˆ˜ ์žˆ๋‹ค.

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>
  );
}

useTransition์„ ํ™œ์šฉํ•ด์„œ ํ˜„์žฌ navigation์ด ์ง„ํ–‰ ์ค‘์ด๋ผ๋Š” ์‹œ๊ฐ์  ํžŒํŠธ๋ฅผ ์ค„ ์ˆ˜ ์žˆ๋‹ค


์“ฐ์ž„ 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 ๋ฅผ ์ถ”๊ฐ€ํ•ด์ฃผ์ง€ ์•Š์•˜๋˜..๐Ÿ˜ฑ