<Suspense> ๋Š” ์ž์‹ ์š”์†Œ๊ฐ€ ๋กœ๋“œ๋˜๊ธฐ ์ „๊นŒ์ง€ ํ™”๋ฉด์— ๋Œ€์ฒด UI๋ฅผ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค.

<Suspense fallback={<Loading />}>
<SomeComponent />
</Suspense>

๋ ˆํผ๋Ÿฐ์Šค

<Suspense>

Props

  • children: ๊ถ๊ทน์ ์œผ๋กœ ๋ Œ๋”๋งํ•˜๋ ค๋Š” ์‹ค์ œ UI์ž…๋‹ˆ๋‹ค. children์˜ ๋ Œ๋”๋ง์ด ์ง€์—ฐ๋˜๋ฉด, Suspense๋Š” fallback์„ ๋Œ€์‹  ๋ Œ๋”๋งํ•ฉ๋‹ˆ๋‹ค.
  • fallback: ์‹ค์ œ UI๊ฐ€ ๋กœ๋“œ๋˜๊ธฐ ์ „๊นŒ์ง€ ๋Œ€์‹  ๋ Œ๋”๋ง ๋˜๋Š” ๋Œ€์ฒด UI์ž…๋‹ˆ๋‹ค. ์˜ฌ๋ฐ”๋ฅธ React node ํ˜•์‹์€ ๋ฌด์—‡์ด๋“  ๋Œ€์ฒด UI๋กœ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ์ง€๋งŒ, ์‹ค์ œ๋กœ๋Š” ๋ณดํ†ต ๋กœ๋”ฉ ์Šคํ”ผ๋„ˆ๋‚˜ ์Šค์ผˆ๋ ˆํ†ค์ฒ˜๋Ÿผ ๊ฐ„๋‹จํ•œ placeholder๋ฅผ ํ™œ์šฉํ•ฉ๋‹ˆ๋‹ค. Suspense๋Š” children์˜ ๋ Œ๋”๋ง์ด ์ง€์—ฐ๋˜๋ฉด ์ž๋™์œผ๋กœ fallback์œผ๋กœ ์ „ํ™˜ํ•˜๊ณ , ๋ฐ์ดํ„ฐ๊ฐ€ ์ค€๋น„๋˜๋ฉด children์œผ๋กœ ๋‹ค์‹œ ์ „ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ๋งŒ์•ฝ fallback์˜ ๋ Œ๋”๋ง์ด ์ง€์—ฐ๋˜๋ฉด, ๊ฐ€์žฅ ๊ฐ€๊นŒ์šด ๋ถ€๋ชจ Suspense๊ฐ€ ํ™œ์„ฑํ™”๋ฉ๋‹ˆ๋‹ค.

์ฃผ์˜ ์‚ฌํ•ญ

  • React๋Š” ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์ฒ˜์Œ์œผ๋กœ ๋งˆ์šดํŠธ ๋˜๊ธฐ ์ „์— ์ง€์—ฐ๋œ ๋ Œ๋”๋ง์„ ํ•˜๋Š” ๋™์•ˆ์˜ ์–ด๋–ค state๋„ ์œ ์ง€ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋กœ๋“œ๋˜๋ฉด React๋Š” ์ผ์‹œ ์ค‘์ง€๋œ ํŠธ๋ฆฌ๋ฅผ ์ฒ˜์Œ๋ถ€ํ„ฐ ๋‹ค์‹œ ๋ Œ๋”๋งํ•ฉ๋‹ˆ๋‹ค.
  • Suspense๊ฐ€ ํŠธ๋ฆฌ์˜ ์ฝ˜ํ…์ธ ๋ฅผ ๋ณด์—ฌ์ฃผ๊ณ  ์žˆ์„ ๋•Œ ๋˜๋‹ค์‹œ ์ง€์—ฐ๋˜๋ฉด startTransition๋‚˜ useDeferredValue๋กœ ์ธํ•œ ์—…๋ฐ์ดํŠธ๊ฐ€ ์•„๋‹Œ ํ•œ, fallback์ด ๋‹ค์‹œ ๋ณด์ž…๋‹ˆ๋‹ค.
  • React๊ฐ€ ๋‹ค์‹œ ์ผ์‹œ ์ค‘์ง€๋˜์–ด ๋ณด์ด๋Š” ์ฝ˜ํ…์ธ ๋ฅผ ์ˆจ๊ฒจ์•ผ ํ•˜๋Š” ๊ฒฝ์šฐ, ์ฝ˜ํ…์ธ  ํŠธ๋ฆฌ์—์„œ layout Effect๋“ค์„ ์ •๋ฆฌํ•ฉ๋‹ˆ๋‹ค. ์ฝ˜ํ…์ธ ๊ฐ€ ๋‹ค์‹œ ๋ณด์ผ ์ค€๋น„๊ฐ€ ๋˜๋ฉด React๋Š” layout Effect๋“ค์„ ๋‹ค์‹œ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. ์ด๋กœ์จ DOM ๋ ˆ์ด์•„์›ƒ์„ ์ธก์ •ํ•˜๋Š” Effect๊ฐ€ ์ฝ˜ํ…์ธ ๊ฐ€ ์ˆจ๊ฒจ์ ธ ์žˆ๋Š” ๋™์•ˆ ๋™์ž‘ํ•˜์ง€ ์•Š๋„๋ก ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค.
  • React๋Š” Suspense์™€ ํ†ตํ•ฉ๋œ Streaming Server Rendering์™€ Selective Hydration๊ฐ™์€ ๋‚ด๋ถ€ ์ตœ์ ํ™”๋ฅผ ํฌํ•จํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ์•„ํ‚คํ…์ฒ˜ ๊ฐœ์š”๋ฅผ ์ฝ๊ณ  ๊ธฐ์ˆ  ๊ฐ•์—ฐ์„ ์‹œ์ฒญํ•˜์—ฌ ๋” ์ž์„ธํžˆ ์•Œ์•„๋ณด์„ธ์š”.

์‚ฌ์šฉ๋ฒ•

์ฝ˜ํ…์ธ ๊ฐ€ ๋กœ๋“œ๋˜๋Š” ๋™์•ˆ ๋Œ€์ฒด UI ๋ณด์—ฌ์ฃผ๊ธฐ

์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ๋ชจ๋“  ๊ณณ์„ Suspense ๊ฒฝ๊ณ„๋กœ ๊ฐ์Œ€ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

<Suspense fallback={<Loading />}>
<Albums />
</Suspense>

React๋Š” children์— ํ•„์š”ํ•œ ๋ชจ๋“  ์ฝ”๋“œ์™€ ๋ฐ์ดํ„ฐ๊ฐ€ ๋กœ๋“œ๋  ๋•Œ๊นŒ์ง€ loading fallback์„ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค.

์•„๋ž˜ ์˜ˆ์‹œ์—์„œ๋Š” ์•จ๋ฒ” ๋ชฉ๋ก์„ ๊ฐ€์ ธ์˜ค๋Š” ๋™์•ˆ ์•จ๋ฒ” ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์ง€์—ฐ๋ฉ๋‹ˆ๋‹ค. ๋ Œ๋”๋งํ•  ์ค€๋น„๊ฐ€ ๋  ๋•Œ๊นŒ์ง€ ๊ฐ€์žฅ ๊ฐ€๊นŒ์šด Suspense๋Š” fallback, ์ฆ‰ Loading ์ปดํฌ๋„ŒํŠธ๋ฅผ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค. ๋ฐ์ดํ„ฐ๊ฐ€ ๋ชจ๋‘ ๋กœ๋“œ๋˜๋ฉด React๋Š” Loading fallback์„ ์ˆจ๊ธฐ๊ณ  ๋กœ๋“œ๋œ ๋ฐ์ดํ„ฐ๋กœ ์•จ๋ฒ” ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ Œ๋”๋งํ•ฉ๋‹ˆ๋‹ค.

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

์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค!

Suspense๊ฐ€ ๊ฐ€๋Šฅํ•œ ๋ฐ์ดํ„ฐ๋งŒ์ด Suspense ์ปดํฌ๋„ŒํŠธ๋ฅผ ํ™œ์„ฑํ™”ํ•ฉ๋‹ˆ๋‹ค. ์•„๋ž˜์™€ ๊ฐ™์€ ๊ฒƒ๋“ค์ด ํ•ด๋‹น๋ฉ๋‹ˆ๋‹ค.

  • Relay์™€ Next.js ๊ฐ™์ด Suspense๊ฐ€ ๊ฐ€๋Šฅํ•œ ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ ์‚ฌ์šฉํ•œ ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ
  • lazy๋ฅผ ํ™œ์šฉํ•œ ์ง€์—ฐ ๋กœ๋”ฉ ์ปดํฌ๋„ŒํŠธ
  • use๋ฅผ ์‚ฌ์šฉํ•ด์„œ Promise ๊ฐ’ ์ฝ๊ธฐ

Suspense๋Š” Effect ๋˜๋Š” ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ ๋‚ด๋ถ€์—์„œ ๊ฐ€์ ธ์˜ค๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ์ง€ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

์œ„์˜ ์•จ๋ฒ” ์ปดํฌ๋„ŒํŠธ์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๋กœ๋“œํ•˜๋Š” ์ •ํ™•ํ•œ ๋ฐฉ๋ฒ•์€ ํ”„๋ ˆ์ž„์›Œํฌ๋งˆ๋‹ค ๋‹ค๋ฆ…๋‹ˆ๋‹ค. Suspense๊ฐ€ ๊ฐ€๋Šฅํ•œ ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ์— ํ”„๋ ˆ์ž„์›Œํฌ์˜ ๋ฐ์ดํ„ฐ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ ๊ด€๋ จ ๋ฌธ์„œ์—์„œ ์ž์„ธํ•œ ๋‚ด์šฉ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋…๋‹จ์ ์ธ ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” Suspense๊ฐ€ ๊ฐ€๋Šฅํ•œ ๋ฐ์ดํ„ฐ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ ๊ธฐ๋Šฅ์€ ์•„์ง ์ง€์›๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. Suspense ์ง€์› ๋ฐ์ดํ„ฐ ์†Œ์Šค๋ฅผ ๊ตฌํ˜„ํ•˜๊ธฐ ์œ„ํ•œ ์š”๊ตฌ ์‚ฌํ•ญ์€ ๋ถˆ์•ˆ์ •ํ•˜๊ณ  ๋ฌธ์„œํ™”๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ๋ฐ์ดํ„ฐ ์†Œ์Šค๋ฅผ Suspense์™€ ํ†ตํ•ฉํ•˜๊ธฐ ์œ„ํ•œ ๊ณต์‹ API๋Š” ํ–ฅํ›„ React ๋ฒ„์ „์—์„œ ์ถœ์‹œ๋  ์˜ˆ์ •์ž…๋‹ˆ๋‹ค.


์ฝ˜ํ…์ธ ๋ฅผ ํ•œ๊บผ๋ฒˆ์— ํ•จ๊ป˜ ๋ณด์—ฌ์ฃผ๊ธฐ

๊ธฐ๋ณธ์ ์œผ๋กœ Suspense ๋‚ด๋ถ€์˜ ์ „์ฒด ํŠธ๋ฆฌ๋Š” ํ•˜๋‚˜์˜ ๋‹จ์œ„๋กœ ์ทจ๊ธ‰๋ฉ๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, ์ด๋Ÿฌํ•œ ๊ตฌ์„ฑ ์š”์†Œ ์ค‘ ํ•˜๋‚˜๋ผ๋„ ์–ด๋–ค ๋ฐ์ดํ„ฐ์— ์˜ํ•ด ์ง€์—ฐ๋˜๋”๋ผ๋„ ๋ชจ๋“  ๊ตฌ์„ฑ ์š”์†Œ๊ฐ€ ํ•จ๊ป˜ ๋กœ๋”ฉ ํ‘œ์‹œ๋กœ ๋Œ€์ฒด๋ฉ๋‹ˆ๋‹ค.

<Suspense fallback={<Loading />}>
<Biography />
<Panel>
<Albums />
</Panel>
</Suspense>

๊ทธ๋Ÿฐ ๋‹ค์Œ ๋ชจ๋‘ ๋ณด์ผ ์ค€๋น„๊ฐ€ ๋˜๋ฉด ํ•œ๊บผ๋ฒˆ์— ๋ชจ๋‘ ํ•จ๊ป˜ ๋ณด์ž…๋‹ˆ๋‹ค.

์•„๋ž˜ ์˜ˆ์‹œ์—์„œ๋Š” Biography์™€ Albums ๋ชจ๋‘ ์–ด๋–ค ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ๋‘ ๊ตฌ์„ฑ ์š”์†Œ๋Š” ๊ฐ™์€ ๋‹จ์ผ Suspense ์•„๋ž˜์— ๊ทธ๋ฃนํ™”๋˜์–ด ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ํ•ญ์ƒ ๋™์‹œ์— ํ•จ๊ป˜ ๊ทธ๋ ค์ง€๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

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={<Loading />}>
        <Biography artistId={artist.id} />
        <Panel>
          <Albums artistId={artist.id} />
        </Panel>
      </Suspense>
    </>
  );
}

function Loading() {
  return <h2>๐ŸŒ€ Loading...</h2>;
}

๋ฐ์ดํ„ฐ๋ฅผ ๋กœ๋“œํ•˜๋Š” ์ปดํฌ๋„ŒํŠธ๊ฐ€ Suspense์˜ ์ง์ ‘์ ์ธ ์ž์‹์ผ ํ•„์š”๋Š” ์—†์Šต๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, Biography์™€ Albums๋ฅผ ์ƒˆ๋กœ์šด Details ์ปดํฌ๋„ŒํŠธ๋กœ ์ด๋™ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋ ‡๊ฒŒ ํ•ด๋„ ๋™์ž‘์€ ๋ณ€๊ฒฝ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. Biography์™€ Albums๋Š” ๊ฐ€์žฅ ๊ฐ€๊นŒ์šด ์ƒ์œ„ Suspense๋ฅผ ๊ณต์œ ํ•˜๋ฏ€๋กœ ๋‘ ์ปดํฌ๋„ŒํŠธ์˜ ๋…ธ์ถœ ์—ฌ๋ถ€๋Š” ํ•จ๊ป˜ ์กฐ์ •๋ฉ๋‹ˆ๋‹ค.

<Suspense fallback={<Loading />}>
<Details artistId={artist.id} />
</Suspense>

function Details({ artistId }) {
return (
<>
<Biography artistId={artistId} />
<Panel>
<Albums artistId={artistId} />
</Panel>
</>
);
}

์ค‘์ฒฉ๋œ ์ฝ˜ํ…์ธ ๊ฐ€ ๋กœ๋“œ๋  ๋•Œ ๋ณด์—ฌ ์ฃผ๊ธฐ

์ปดํฌ๋„ŒํŠธ๊ฐ€ ์ผ์‹œ ์ค‘๋‹จ๋˜๋ฉด ๊ฐ€์žฅ ๊ฐ€๊นŒ์šด ์ƒ์œ„ Suspense ์ปดํฌ๋„ŒํŠธ๊ฐ€ Fallback์„ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด ์—ฌ๋Ÿฌ Suspense ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ค‘์ฒฉํ•˜์—ฌ ๋กœ๋”ฉ ์ˆœ์„œ๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ฐ Suspense์˜ Fallback์€ ๋‹ค์Œ ๋ ˆ๋ฒจ์˜ ์ฝ˜ํ…์ธ ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋˜๋ฉด ์ฑ„์›Œ์ง‘๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด ์•จ๋ฒ” ๋ชฉ๋ก์— ์ž์ฒด Fallback์„ ์ง€์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

<Suspense fallback={<BigSpinner />}>
<Biography />
<Suspense fallback={<AlbumsGlimmer />}>
<Panel>
<Albums />
</Panel>
</Suspense>
</Suspense>

์ด ๋ณ€๊ฒฝ์œผ๋กœ Biography๋ฅผ ๋ณด์—ฌ์ค„ ๋•Œ Albums์ด ๋กœ๋“œ๋  ๋•Œ๊นŒ์ง€ โ€œ๊ธฐ๋‹ค๋ฆดโ€ ํ•„์š”๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.

์ˆœ์„œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

  1. Biography๊ฐ€ ์•„์ง ๋กœ๋“œ๋˜์ง€ ์•Š์€ ๊ฒฝ์šฐ, ์ „์ฒด ์ฝ˜ํ…์ธ  ์˜์—ญ ๋Œ€์‹  BigSpinner๊ฐ€ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค.
  2. Biography ๋กœ๋”ฉ์ด ์™„๋ฃŒ๋˜๋ฉด BigSpinner๊ฐ€ ์ฝ˜ํ…์ธ ๋กœ ๋Œ€์ฒด๋ฉ๋‹ˆ๋‹ค.
  3. Albums๊ฐ€ ์•„์ง ๋กœ๋“œ๋˜์ง€ ์•Š์œผ๋ฉด Albums์™€ ๊ทธ ์ƒ์œ„ Panel ๋Œ€์‹  AlbumsGlimmer๊ฐ€ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค.
  4. ๋งˆ์ง€๋ง‰์œผ๋กœ Albums๊ฐ€ ๋กœ๋”ฉ์„ ์™„๋ฃŒํ•˜๋ฉด AlbumsGlimmer๋ฅผ ๋Œ€์ฒดํ•ฉ๋‹ˆ๋‹ค.
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>
  );
}

Suspense๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด UI์˜ ์–ด๋–ค ๋ถ€๋ถ„์ด ํ•ญ์ƒ ๋™์‹œ์— ๊ทธ๋ ค์ ธ์•ผ ํ•˜๋Š”์ง€, ์–ด๋–ค ๋ถ€๋ถ„์ด ๋กœ๋”ฉ ์ˆœ์„œ์—์„œ ์ ์ง„์ ์œผ๋กœ ๋” ๋งŽ์€ ์ฝ˜ํ…์ธ ๋ฅผ ๋ณด์—ฌ์ค˜์•ผ ํ•˜๋Š”์ง€ ์กฐ์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์•ฑ์˜ ๋‚˜๋จธ์ง€ ๋™์ž‘์— ์˜ํ–ฅ์„ ์ฃผ์ง€ ์•Š๊ณ  ํŠธ๋ฆฌ์˜ ์–ด๋Š ์œ„์น˜์—์„œ๋‚˜ Suspense๋ฅผ ์ถ”๊ฐ€, ์ด๋™ ๋˜๋Š” ์‚ญ์ œํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋ชจ๋“  ์ปดํฌ๋„ŒํŠธ ์ฃผ์œ„์— Suspense๋ฅผ ๋‘์ง€ ๋งˆ์„ธ์š”. Suspense๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ๊ฒฝํ—˜ํ•˜๊ธฐ๋ฅผ ์›ํ•˜๋Š” ๋กœ๋”ฉ ์ˆœ์„œ๋ณด๋‹ค ๋” ์„ธ๋ถ„ํ™”๋˜์–ด์„œ๋Š” ์•ˆ ๋ฉ๋‹ˆ๋‹ค. ๋””์ž์ด๋„ˆ์™€ ํ•จ๊ป˜ ์ž‘์—…ํ•˜๋Š” ๊ฒฝ์šฐ ๋กœ๋”ฉ ์ƒํƒœ๋ฅผ ์–ด๋””์— ๋ฐฐ์น˜ํ•ด์•ผ ํ•˜๋Š”์ง€ ๋””์ž์ด๋„ˆ์—๊ฒŒ ๋ฌผ์–ด๋ณด์„ธ์š”. ๋””์ž์ด๋„ˆ๊ฐ€ ์ด๋ฏธ ๋””์ž์ธ ์™€์ด์–ด ํ”„๋ ˆ์ž„์— ํฌํ•จํ–ˆ์„ ๊ฐ€๋Šฅ์„ฑ์ด ๋†’์Šต๋‹ˆ๋‹ค.


์ƒˆ ์ฝ˜ํ…์ธ ๊ฐ€ ๋กœ๋“œ๋˜๋Š” ๋™์•ˆ ์ด์ „ ์ฝ˜ํ…์ธ  ๋ณด์—ฌ์ฃผ๊ธฐ

์ด ์˜ˆ์ œ์—์„œ๋Š” ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ๋™์•ˆ SearchResults ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์ง€์—ฐ๋ฉ๋‹ˆ๋‹ค. "a"๋ฅผ ์ž…๋ ฅํ•˜๊ณ  ๊ฒฐ๊ณผ๋ฅผ ๊ธฐ๋‹ค๋ฆฐ ๋‹ค์Œ "ab"๋กœ ๋ฐ”๊ฟ”๋ณด์„ธ์š”. "a"์— ๋Œ€ํ•œ ๊ฒฐ๊ณผ๋Š” ๋กœ๋”ฉ 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>
</>
);
}

query๋Š” ์ฆ‰์‹œ ์—…๋ฐ์ดํŠธ๋˜๋ฏ€๋กœ ์ž…๋ ฅ์— ์ƒˆ ๊ฐ’์ด ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ deferredQuery๋Š” ๋ฐ์ดํ„ฐ๊ฐ€ ๋กœ๋“œ๋  ๋•Œ๊นŒ์ง€ ์ด์ „ ๊ฐ’์„ ์œ ์ง€ํ•˜๋ฏ€๋กœ SearchResults๋Š” ์ž ์‹œ ๋™์•ˆ ์ด์ „ ๊ฒฐ๊ณผ๋ฅผ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค.

์‚ฌ์šฉ์ž์—๊ฒŒ ๋” ๋ช…ํ™•ํ•˜๊ฒŒ ์•Œ๋ฆฌ๊ธฐ ์œ„ํ•ด ์ด์ „ ๊ฒฐ๊ณผ ๋ชฉ๋ก์ด ํ‘œ์‹œ๋  ๋•Œ ์‹œ๊ฐ์  ํ‘œ์‹œ๋ฅผ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

<div style={{
opacity: query !== deferredQuery ? 0.5 : 1
}}>
<SearchResults query={deferredQuery} />
</div>

์•„๋ž˜ ์˜ˆ์‹œ์—์„œ "a"๋ฅผ ์ž…๋ ฅํ•˜๊ณ  ๊ฒฐ๊ณผ๊ฐ€ ๋กœ๋“œ๋  ๋•Œ๊นŒ์ง€ ๊ธฐ๋‹ค๋ฆฐ ๋‹ค์Œ ์ž…๋ ฅ์„ "ab"๋กœ ํŽธ์ง‘ํ•ด๋ณด์„ธ์š”. ์ด์ œ ์ƒˆ ๊ฒฐ๊ณผ๊ฐ€ ๋กœ๋“œ๋  ๋•Œ๊นŒ์ง€ Suspense Fallback ๋Œ€์‹  ํฌ๋ฏธํ•œ ์ด์ „ ๊ฒฐ๊ณผ ๋ชฉ๋ก์ด ํ‘œ์‹œ๋˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

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

์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค!

์ง€์—ฐ๋œ ๊ฐ’(deferred value)๊ณผ transitions ์„ ์‚ฌ์šฉํ•˜๋ฉด Suspense fallback์„ ํ‘œ์‹œํ•˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. transitions๋Š” ์ „์ฒด ์—…๋ฐ์ดํŠธ๋ฅผ ๊ธด๊ธ‰ํ•˜์ง€ ์•Š์€ ๊ฒƒ์œผ๋กœ ์ฒ˜๋ฆฌํ•˜๋ฏ€๋กœ ์ผ๋ฐ˜์ ์œผ๋กœ ํ”„๋ ˆ์ž„์›Œํฌ์™€ router ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์—์„œ navigation์„ ์œ„ํ•ด ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค. ๋ฐ˜๋ฉด์— ์ง€์—ฐ๋œ ๊ฐ’(deferred value)์€ UI์˜ ์ผ๋ถ€๋ฅผ ๊ธด๊ธ‰ํ•˜์ง€ ์•Š์€ ๊ฒƒ์œผ๋กœ ์ฒ˜๋ฆฌํ•˜๊ณ  ๋‚˜๋จธ์ง€ UI๋ณด๋‹ค โ€œ์ง€์—ฐโ€์‹œํ‚ค๋ ค๋Š” ๋ชฉ์ ์˜ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ฝ”๋“œ์—์„œ ์œ ์šฉํ•ฉ๋‹ˆ๋‹ค.


์ด๋ฏธ ๋ณด์ธ ์ฝ˜ํ…์ธ ๊ฐ€ ์ˆจ๊ฒจ์ง€์ง€ ์•Š๋„๋ก ๋ฐฉ์ง€

์ปดํฌ๋„ŒํŠธ๊ฐ€ ์ง€์—ฐ๋˜๋ฉด ๊ฐ€์žฅ ๊ฐ€๊นŒ์šด ์ƒ์œ„ Suspense๊ฐ€ Fallback์„ ๋ณด์—ฌ์ฃผ๋„๋ก ์ „ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฏธ ์ผ๋ถ€ ์ฝ˜ํ…์ธ ๊ฐ€ ๋ณด์ด๋Š” ๊ฒฝ์šฐ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์ด ๋Š๊ธธ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ๋ฒ„ํŠผ์„ ๋ˆŒ๋Ÿฌ ๋ณด์„ธ์š”.

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๊ฐ€ Fallback์„ ๋ณด์—ฌ์ฃผ๊ธฐ ์‹œ์ž‘ํ–ˆ์Šต๋‹ˆ๋‹ค. ๊ฐ€์žฅ ๊ฐ€๊นŒ์šด Suspense๊ฐ€ root ๊ทผ์ฒ˜์— ์žˆ์—ˆ๊ธฐ ๋•Œ๋ฌธ์— ์ „์ฒด ์‚ฌ์ดํŠธ ๋ ˆ์ด์•„์›ƒ์ด BigSpinner๋กœ ๋Œ€์ฒด๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

์ด๋ฅผ ๋ฐฉ์ง€ํ•˜๋ ค๋ฉด startTransition์„ ์‚ฌ์šฉํ•˜์—ฌ navigation state ์—…๋ฐ์ดํŠธ๋ฅผ transition์œผ๋กœ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

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

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

์ด๋Š” state ์ „ํ™˜์ด ๊ธ‰ํ•˜์ง€ ์•Š์œผ๋ฉฐ, ์ด๋ฏธ ๊ณต๊ฐœ๋œ ์ฝ˜ํ…์ธ ๋ฅผ ์ˆจ๊ธฐ๋Š” ๋Œ€์‹  ์ด์ „ ํŽ˜์ด์ง€๋ฅผ ๊ณ„์† ํ‘œ์‹œํ•˜๋Š” ๊ฒƒ์ด ์ข‹๋‹ค๋Š” ๊ฒƒ์„ React์—๊ฒŒ ์•Œ๋ ค์ค๋‹ˆ๋‹ค. ์ด์ œ ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜๋ฉด Biography๊ฐ€ ๋กœ๋“œ๋  ๋•Œ๊นŒ์ง€ โ€œ๋Œ€๊ธฐโ€ํ•ฉ๋‹ˆ๋‹ค:

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

transition์€ ๋ชจ๋“  ์ฝ˜ํ…์ธ ๊ฐ€ ๋กœ๋“œ๋  ๋•Œ๊นŒ์ง€ ๊ธฐ๋‹ค๋ฆฌ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ด๋ฏธ ๋ณด์—ฌ์ง„ ์ฝ˜ํ…์ธ ๊ฐ€ ์ˆจ๊ฒจ์ง€์ง€ ์•Š๋„๋ก ์ถฉ๋ถ„ํžˆ ์˜ค๋ž˜ ๊ธฐ๋‹ค๋ฆฝ๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด ์›น์‚ฌ์ดํŠธ Layout์€ ์ด๋ฏธ ๋ณด์ด๋ฏ€๋กœ ๋กœ๋”ฉ ์Šคํ”ผ๋„ˆ ๋’ค์— ์ˆจ๊ธฐ๋Š” ๊ฒƒ์€ ์ข‹์ง€ ์•Š์„ ๊ฒƒ์ž…๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ Albums ์ฃผ์œ„์— ์ค‘์ฒฉ๋œ Suspense๋Š” ์ƒˆ๋กœ์šด ๊ฒƒ์ด๋ฏ€๋กœ transition์ด ๊ธฐ๋‹ค๋ฆฌ์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค!

Suspense๋ฅผ ์ง€์›ํ•˜๋Š” router๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ navigation ์—…๋ฐ์ดํŠธ๋ฅผ transition์œผ๋กœ ๋ž˜ํ•‘ํ•  ๊ฒƒ์œผ๋กœ ์˜ˆ์ƒ๋ฉ๋‹ˆ๋‹ค.


Transition์ด ๋ฐœ์ƒํ•˜๊ณ  ์žˆ์Œ์„ ๋ณด์—ฌ์ฃผ๊ธฐ

์œ„์˜ ์˜ˆ์‹œ์—์„œ๋Š” ๋ฒ„ํŠผ์„ ํด๋ฆญํ•ด๋„ navigation์ด ์ง„ํ–‰ ์ค‘์ด๋ผ๋Š” ์‹œ๊ฐ์  ํ‘œ์‹œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ํ‘œ์‹œ๊ธฐ๋ฅผ ์ถ”๊ฐ€ํ•˜๋ ค๋ฉด startTransition์„ boolean ๊ฐ’์ธ isPending ๊ฐ’์„ ์ œ๊ณตํ•˜๋Š” useTransition์œผ๋กœ ๋ฐ”๊พธ๋ฉด ๋ฉ๋‹ˆ๋‹ค. ์•„๋ž˜ ์˜ˆ์‹œ์—์„œ๋Š” transition์ด ์ง„ํ–‰๋˜๋Š” ๋™์•ˆ ์›น์‚ฌ์ดํŠธ ํ—ค๋” ์Šคํƒ€์ผ์„ ๋ณ€๊ฒฝํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.

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


Navigation์—์„œ Suspense ์žฌ์„ค์ •ํ•˜๊ธฐ

Transition์ด ์ง„ํ–‰๋˜๋Š” ๋™์•ˆ React๋Š” ์ด๋ฏธ ๋ณด์ธ ์ฝ˜ํ…์ธ ๋ฅผ ์ˆจ๊ธฐ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ๋‹ค๋ฅธ ๋งค๊ฐœ๋ณ€์ˆ˜๊ฐ€ ์žˆ๋Š” ๊ฒฝ๋กœ๋กœ ์ด๋™ํ•˜๋Š” ๊ฒฝ์šฐ React์— ๋‹ค๋ฅธ ์ฝ˜ํ…์ธ ๋ผ๊ณ  ์•Œ๋ ค์ฃผ๊ณ  ์‹ถ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋ฅผ key๋กœ ํ‘œํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

<ProfilePage key={queryParams.id} />

์‚ฌ์šฉ์ž์˜ ํ”„๋กœํ•„ ํŽ˜์ด์ง€ ๋‚ด์—์„œ ์ด๋™ ์ค‘์ธ๋ฐ ๋ฌด์–ธ๊ฐ€๊ฐ€ ์ง€์—ฐ๋˜์—ˆ๋‹ค๊ณ  ๊ฐ€์ •ํ•ด ๋ณด์„ธ์š”. ํ•ด๋‹น ์—…๋ฐ์ดํŠธ๊ฐ€ transition์œผ๋กœ ๊ฐ์‹ธ์ ธ ์žˆ์œผ๋ฉด ์ด๋ฏธ ํ‘œ์‹œ๋œ ์ฝ˜ํ…์ธ ์— ๋Œ€ํ•œ Fallback์ด ํŠธ๋ฆฌ๊ฑฐ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ด๊ฒƒ์ด ์˜ˆ์ƒ๋˜๋Š” ๋™์ž‘์ž…๋‹ˆ๋‹ค.

ํ•˜์ง€๋งŒ ์ด์ œ ๋‘ ๊ฐœ์˜ ์„œ๋กœ ๋‹ค๋ฅธ ์‚ฌ์šฉ์ž ํ”„๋กœํ•„ ์‚ฌ์ด๋ฅผ ์ด๋™ํ•œ๋‹ค๊ณ  ๊ฐ€์ •ํ•ด ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค. ์ด ๊ฒฝ์šฐ Fallback์„ ํ‘œ์‹œํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด ํ•œ ์‚ฌ์šฉ์ž์˜ ํƒ€์ž„๋ผ์ธ์ด ๋‹ค๋ฅธ ์‚ฌ์šฉ์ž์˜ ํƒ€์ž„๋ผ์ธ๊ณผ ๋‹ค๋ฅธ ์ฝ˜ํ…์ธ ๋ผ๊ณ , ๊ฐ€์ •ํ•ด ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค. key๋ฅผ ์ง€์ •ํ•˜๋ฉด React๊ฐ€ ์„œ๋กœ ๋‹ค๋ฅธ ์‚ฌ์šฉ์ž์˜ ํ”„๋กœํ•„์„ ์„œ๋กœ ๋‹ค๋ฅธ ์ปดํฌ๋„ŒํŠธ๋กœ ์ทจ๊ธ‰ํ•˜๊ณ  ํƒ์ƒ‰ํ•˜๋Š” ๋™์•ˆ Suspense๋ฅผ ์žฌ์„ค์ •ํ•˜๋„๋ก ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. Suspense ํ†ตํ•ฉ ๋ผ์šฐํ„ฐ๋Š” ์ด ๋™์ž‘์„ ์ž๋™์œผ๋กœ ์ˆ˜ํ–‰ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.


์„œ๋ฒ„ ์—๋Ÿฌ ๋ฐ ์„œ๋ฒ„ ์ „์šฉ ์ฝ˜ํ…์ธ ์— ๋Œ€ํ•œ Fallback ์ œ๊ณต

์ŠคํŠธ๋ฆฌ๋ฐ ์„œ๋ฒ„ ๋ Œ๋”๋ง API ์ค‘ ํ•˜๋‚˜(๋˜๋Š” ์ด์— ์˜์กดํ•˜๋Š” ํ”„๋ ˆ์ž„์›Œํฌ)๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ, React๋Š” ์„œ๋ฒ„์˜ ์—๋Ÿฌ๋ฅผ ์ฒ˜๋ฆฌํ•˜๊ธฐ ์œ„ํ•ด <Suspense> ๋ฐ”์šด๋”๋ฆฌ๋„ ์‚ฌ์šฉํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์„œ๋ฒ„์—์„œ ์—๋Ÿฌ๋ฅผ ๋ฐœ์ƒ์‹œํ‚ค๋”๋ผ๋„ React๋Š” ์„œ๋ฒ„ ๋ Œ๋”๋ง์„ ์ค‘๋‹จํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋Œ€์‹ , ๊ทธ ์œ„์— ์žˆ๋Š” ๊ฐ€์žฅ ๊ฐ€๊นŒ์šด <Suspense> ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ฐพ์•„์„œ ๊ทธ Fallback(์˜ˆ: ์Šคํ”ผ๋„ˆ)์„ ์ƒ์„ฑ๋œ ์„œ๋ฒ„ HTML์— ํฌํ•จํ•ฉ๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž๋Š” ์ฒ˜์Œ์—๋Š” ์Šคํ”ผ๋„ˆ๋ฅผ ๋ณด๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

ํด๋ผ์ด์–ธํŠธ์—์„œ React๋Š” ๋™์ผํ•œ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋‹ค์‹œ ๋ Œ๋”๋งํ•˜๋ ค๊ณ  ์‹œ๋„ํ•ฉ๋‹ˆ๋‹ค. ํด๋ผ์ด์–ธํŠธ์—์„œ๋„ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด React๋Š” ์—๋Ÿฌ๋ฅผ ๋˜์ง€๊ณ  ๊ฐ€์žฅ ๊ฐ€๊นŒ์šด error boundary๋ฅผ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ํด๋ผ์ด์–ธํŠธ์—์„œ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•˜์ง€ ์•Š์œผ๋ฉด ์ฝ˜ํ…์ธ ๊ฐ€ ๊ฒฐ๊ตญ ์„ฑ๊ณต์ ์œผ๋กœ ๋ณด์˜€๊ธฐ ๋•Œ๋ฌธ์— React๋Š” ์‚ฌ์šฉ์ž์—๊ฒŒ ์—๋Ÿฌ๋ฅผ ๋ณด์—ฌ์ฃผ์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

์ด๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ผ๋ถ€ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์„œ๋ฒ„์—์„œ ๋ Œ๋”๋งํ•˜์ง€ ์•Š๋„๋ก ์„ ํƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋ ‡๊ฒŒ ํ•˜๋ ค๋ฉด ์„œ๋ฒ„ ํ™˜๊ฒฝ์—์„œ ์—๋Ÿฌ๋ฅผ ๋ฐœ์ƒ์‹œํ‚จ ๋‹ค์Œ <Suspense> ๋ฐ”์šด๋”๋ฆฌ๋กœ ๊ฐ์‹ธ์„œ ํ•ด๋‹น HTML์„ Fallback์œผ๋กœ ๋Œ€์ฒดํ•ฉ๋‹ˆ๋‹ค.

<Suspense fallback={<Loading />}>
<Chat />
</Suspense>

function Chat() {
if (typeof window === 'undefined') {
throw Error('Chat should only render on the client.');
}
// ...
}

์„œ๋ฒ„ HTML์— ๋กœ๋”ฉ UI๊ฐ€ ํฌํ•จ๋ฉ๋‹ˆ๋‹ค. ํด๋ผ์ด์–ธํŠธ์—์„œ๋Š” Chat ์ปดํฌ๋„ŒํŠธ๋กœ ๋Œ€์ฒด๋ฉ๋‹ˆ๋‹ค.


๋ฌธ์ œ ํ•ด๊ฒฐ

์—…๋ฐ์ดํŠธ ์ค‘์— UI๊ฐ€ Fallback์œผ๋กœ ๋Œ€์ฒด๋˜๋Š” ๊ฒƒ์„ ๋ฐฉ์ง€ํ•˜๋ ค๋ฉด ์–ด๋–ป๊ฒŒ ํ•ด์•ผ ํ•˜๋‚˜์š”?

ํ‘œ์‹œ๋˜๋Š” UI๋ฅผ Fallback์œผ๋กœ ๋Œ€์ฒดํ•˜๋ฉด ์‚ฌ์šฉ์ž ํ™˜๊ฒฝ์ด ๋ถˆ์•ˆ์ •ํ•ด์ง‘๋‹ˆ๋‹ค. ์ด๋Š” ์—…๋ฐ์ดํŠธ๋กœ ์ธํ•ด ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์ง€์—ฐ๋˜๊ณ  ๊ฐ€์žฅ ๊ฐ€๊นŒ์šด Suspense๊ฐ€ ์ด๋ฏธ ์‚ฌ์šฉ์ž์—๊ฒŒ ์ฝ˜ํ…์ธ ๋ฅผ ๋ณด์—ฌ์ฃผ๊ณ  ์žˆ์„ ๋•Œ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ด๋Ÿฐ ์ผ์ด ๋ฐœ์ƒํ•˜์ง€ ์•Š๋„๋ก ํ•˜๋ ค๋ฉด, startTransition์„ ์‚ฌ์šฉํ•˜์—ฌ ์—…๋ฐ์ดํŠธ๋ฅผ ๊ธด๊ธ‰ํ•˜์ง€ ์•Š์€ ๊ฒƒ์œผ๋กœ ์ฒ˜๋ฆฌํ•˜์„ธ์š”. transition์ด ์ง„ํ–‰๋˜๋Š” ๋™์•ˆ React๋Š” ์›์น˜ ์•Š๋Š” Fallback์ด ๋‚˜ํƒ€๋‚˜์ง€ ์•Š๋„๋ก ์ถฉ๋ถ„ํ•œ ๋ฐ์ดํ„ฐ๊ฐ€ ๋กœ๋“œ๋  ๋•Œ๊นŒ์ง€ ๊ธฐ๋‹ค๋ฆฝ๋‹ˆ๋‹ค.

function handleNextPageClick() {
// If this update suspends, don't hide the already displayed content
startTransition(() => {
setCurrentPage(currentPage + 1);
});
}

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ๊ธฐ์กด ์ฝ˜ํ…์ธ ๊ฐ€ ์ˆจ๊ฒจ์ง€์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ์ƒˆ๋กœ ๋ Œ๋”๋ง ๋œ Suspense๋Š” ์—ฌ์ „ํžˆ ์ฆ‰์‹œ Fallback์„ ๋ณด์—ฌ์ค˜์„œ UI๋ฅผ ์ฐจ๋‹จํ•˜์ง€ ์•Š๊ณ  ์‚ฌ์šฉ์ž๊ฐ€ ์ฝ˜ํ…์ธ ๋ฅผ ์ด์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

React๋Š” ๊ธด๊ธ‰ํ•˜์ง€ ์•Š์€ ์—…๋ฐ์ดํŠธ ์ค‘์—๋งŒ ์›์น˜ ์•Š๋Š” Fallback์„ ๋ฐฉ์ง€ํ•ฉ๋‹ˆ๋‹ค. ๊ธด๊ธ‰ํ•œ ์—…๋ฐ์ดํŠธ์˜ ๊ฒฐ๊ณผ์ธ ๊ฒฝ์šฐ ๋ Œ๋”๋ง์„ ์ง€์—ฐ์‹œํ‚ค์ง€ ์•Š์Šต๋‹ˆ๋‹ค. startTransition ๋˜๋Š” useDeferredValue์™€ ๊ฐ™์€ API๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

Router๊ฐ€ Suspense์™€ ํ†ตํ•ฉ๋œ ๊ฒฝ์šฐ, router๋Š” ์—…๋ฐ์ดํŠธ๋ฅผ ์ž๋™์œผ๋กœ startTransition์— ๋ž˜ํ•‘ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.