useTransition์€ UI๋ฅผ ์ฐจ๋‹จํ•˜์ง€ ์•Š๊ณ  ์ƒํƒœ๋ฅผ ์—…๋ฐ์ดํŠธํ•  ์ˆ˜ ์žˆ๋Š” React Hook์ž…๋‹ˆ๋‹ค.

const [isPending, startTransition] = useTransition()

๋ ˆํผ๋Ÿฐ์Šค

useTransition()

์ปดํฌ๋„ŒํŠธ์˜ ์ตœ์ƒ์œ„ ์ˆ˜์ค€์—์„œ useTransition์„ ํ˜ธ์ถœํ•˜์—ฌ ์ผ๋ถ€ state ์—…๋ฐ์ดํŠธ๋ฅผ transition์œผ๋กœ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค.

import { useTransition } from 'react';

function TabContainer() {
const [isPending, startTransition] = useTransition();
// ...
}

์•„๋ž˜์—์„œ ๋” ๋งŽ์€ ์˜ˆ์‹œ๋ฅผ ํ™•์ธํ•˜์„ธ์š”.

๋งค๊ฐœ๋ณ€์ˆ˜

useTransition์€ ์–ด๋–ค ๋งค๊ฐœ๋ณ€์ˆ˜๋„ ๋ฐ›์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

๋ฐ˜ํ™˜๊ฐ’

useTransition์€ ์ •ํ™•ํžˆ ๋‘ ๊ฐœ์˜ ํ•ญ๋ชฉ์ด ์žˆ๋Š” ๋ฐฐ์—ด์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.

  1. isPending ํ”Œ๋ž˜๊ทธ๋Š” ๋Œ€๊ธฐ ์ค‘์ธ transition์ด ์žˆ๋Š”์ง€ ์•Œ๋ ค์ค๋‹ˆ๋‹ค.
  2. startTransition ํ•จ์ˆ˜๋Š” ์ƒํƒœ ์—…๋ฐ์ดํŠธ๋ฅผ transition์œผ๋กœ ํ‘œ์‹œํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ฃผ๋Š” ํ•จ์ˆ˜์ž…๋‹ˆ๋‹ค.

startTransition ํ•จ์ˆ˜

useTransition์ด ๋ฐ˜ํ™˜ํ•˜๋Š” startTransition ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด state ์—…๋ฐ์ดํŠธ๋ฅผ transition์œผ๋กœ ํ‘œ์‹œํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

function TabContainer() {
const [isPending, startTransition] = useTransition();
const [tab, setTab] = useState('about');

function selectTab(nextTab) {
startTransition(() => {
setTab(nextTab);
});
}
// ...
}

๋งค๊ฐœ๋ณ€์ˆ˜

  • scope: ํ•˜๋‚˜ ์ด์ƒ์˜ set ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ ์ผ๋ถ€ state๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๋Š” ํ•จ์ˆ˜์ž…๋‹ˆ๋‹ค. React๋Š” ๋งค๊ฐœ๋ณ€์ˆ˜ ์—†์ด scope๋ฅผ ์ฆ‰์‹œ ํ˜ธ์ถœํ•˜๊ณ  scope ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ๋™์•ˆ ๋™๊ธฐ์ ์œผ๋กœ ์˜ˆ์•ฝ๋œ ๋ชจ๋“  state ์—…๋ฐ์ดํŠธ๋ฅผ transition์œผ๋กœ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค. ์ด๋Š” non-blocking์ด๋ฉฐ ์›์น˜ ์•Š๋Š” ๋กœ๋”ฉ์„ ํ‘œ์‹œํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

๋ฐ˜ํ™˜๊ฐ’

startTransition์€ ์•„๋ฌด๊ฒƒ๋„ ๋ฐ˜ํ™˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

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

  • useTransition์€ Hook์ด๋ฏ€๋กœ ์ปดํฌ๋„ŒํŠธ๋‚˜ ์ปค์Šคํ…€ Hook ๋‚ด๋ถ€์—์„œ๋งŒ ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋‹ค๋ฅธ ๊ณณ(์˜ˆ์‹œ: ๋ฐ์ดํ„ฐ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ)์—์„œ transition์„ ์‹œ์ž‘ํ•ด์•ผ ํ•˜๋Š” ๊ฒฝ์šฐ, ๋…๋ฆฝํ˜• startTransition์„ ํ˜ธ์ถœํ•˜์„ธ์š”.

  • ํ•ด๋‹น state์˜ set ํ•จ์ˆ˜์— ์•ก์„ธ์Šคํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒฝ์šฐ์—๋งŒ ์—…๋ฐ์ดํŠธ๋ฅผ transition์œผ๋กœ ๋ž˜ํ•‘ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ผ๋ถ€ prop์ด๋‚˜ ์ปค์Šคํ…€ Hook ๊ฐ’์— ๋Œ€ํ•œ ์‘๋‹ต์œผ๋กœ transition์„ ์‹œ์ž‘ํ•˜๋ ค๋ฉด useDeferredValue๋ฅผ ์‚ฌ์šฉํ•ด ๋ณด์„ธ์š”.

  • startTransition์— ์ „๋‹ฌํ•˜๋Š” ํ•จ์ˆ˜๋Š” ๋™๊ธฐ์‹์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. React๋Š” ์ด ํ•จ์ˆ˜๋ฅผ ์ฆ‰์‹œ ์‹คํ–‰ํ•˜์—ฌ ์‹คํ–‰ํ•˜๋Š” ๋™์•ˆ ๋ฐœ์ƒํ•˜๋Š” ๋ชจ๋“  state ์—…๋ฐ์ดํŠธ๋ฅผ transition์œผ๋กœ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค. ๋‚˜์ค‘์— ๋” ๋งŽ์€ state ์—…๋ฐ์ดํŠธ๋ฅผ ์ˆ˜ํ–‰ํ•˜๋ ค๊ณ  ํ•˜๋ฉด(์˜ˆ์‹œ: timeout), transition์œผ๋กœ ํ‘œ์‹œ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

  • Transition์œผ๋กœ ํ‘œ์‹œ๋œ state ์—…๋ฐ์ดํŠธ๋Š” ๋‹ค๋ฅธ state ์—…๋ฐ์ดํŠธ์— ์˜ํ•ด ์ค‘๋‹จ๋ฉ๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, transition ๋‚ด์—์„œ ์ฐจํŠธ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์—…๋ฐ์ดํŠธํ•œ ๋‹ค์Œ ์ฐจํŠธ๊ฐ€ ๋‹ค์‹œ ๋ Œ๋”๋ง ๋˜๋Š” ๋„์ค‘์— ์ž…๋ ฅ์„ ์‹œ์ž‘ํ•˜๋ฉด React๋Š” ์ž…๋ ฅ ์—…๋ฐ์ดํŠธ๋ฅผ ์ฒ˜๋ฆฌํ•œ ํ›„ ์ฐจํŠธ ์ปดํฌ๋„ŒํŠธ์—์„œ ๋ Œ๋”๋ง ์ž‘์—…์„ ๋‹ค์‹œ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค.

  • Transition ์—…๋ฐ์ดํŠธ๋Š” ํ…์ŠคํŠธ ์ž…๋ ฅ์„ ์ œ์–ดํ•˜๋Š” ๋ฐ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.

  • ์ง„ํ–‰ ์ค‘์ธ transition์ด ์—ฌ๋Ÿฌ ๊ฐœ ์žˆ๋Š” ๊ฒฝ์šฐ, React๋Š” ํ˜„์žฌ transition์„ ํ•จ๊ป˜ ์ผ๊ด„ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค. ์ด๋Š” ํ–ฅํ›„ ๋ฆด๋ฆฌ์ฆˆ์—์„œ ์ œ๊ฑฐ๋  ๊ฐ€๋Šฅ์„ฑ์ด ๋†’์€ ์ œํ•œ ์‚ฌํ•ญ์ž…๋‹ˆ๋‹ค.


์‚ฌ์šฉ๋ฒ•

state ์—…๋ฐ์ดํŠธ๋ฅผ non-blocking transition์œผ๋กœ ํ‘œ์‹œ

์ปดํฌ๋„ŒํŠธ์˜ ์ตœ์ƒ์œ„ ๋ ˆ๋ฒจ์—์„œ useTransition์„ ํ˜ธ์ถœํ•˜์—ฌ state ์—…๋ฐ์ดํŠธ๋ฅผ non-blocking transitions์œผ๋กœ ํ‘œ์‹œํ•˜์„ธ์š”.

import { useState, useTransition } from 'react';

function TabContainer() {
const [isPending, startTransition] = useTransition();
// ...
}

useTransition์€ ์ •ํ™•ํžˆ ๋‘ ๊ฐœ์˜ ํ•ญ๋ชฉ์ด ์žˆ๋Š” ๋ฐฐ์—ด์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.

  1. ๋ณด๋ฅ˜ ์ค‘์ธ transition์ด ์žˆ๋Š”์ง€๋ฅผ ์•Œ๋ ค์ฃผ๋Š” isPending ํ”Œ๋ž˜๊ทธ์ž…๋‹ˆ๋‹ค.
  2. state ์—…๋ฐ์ดํŠธ๋ฅผ transition์œผ๋กœ ํ‘œ์‹œํ•  ์ˆ˜ ์žˆ๋Š” startTransition ํ•จ์ˆ˜์ž…๋‹ˆ๋‹ค.

๊ทธ ํ›„ ๋‹ค์Œ๊ณผ ๊ฐ™์ด state ์—…๋ฐ์ดํŠธ๋ฅผ transition์œผ๋กœ ํ‘œ์‹œํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

function TabContainer() {
const [isPending, startTransition] = useTransition();
const [tab, setTab] = useState('about');

function selectTab(nextTab) {
startTransition(() => {
setTab(nextTab);
});
}
// ...
}

Transition์„ ์‚ฌ์šฉํ•˜๋ฉด ๋Š๋ฆฐ ๋””๋ฐ”์ด์Šค์—์„œ๋„ ์‚ฌ์šฉ์ž ์ธํ„ฐํŽ˜์ด์Šค ์—…๋ฐ์ดํŠธ์˜ ๋ฐ˜์‘์„ฑ์„ ์œ ์ง€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Transition์„ ์‚ฌ์šฉํ•˜๋ฉด ๋ฆฌ๋ Œ๋”๋ง ๋„์ค‘์—๋„ UI๊ฐ€ ๋ฐ˜์‘์„ฑ์„ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด ์‚ฌ์šฉ์ž๊ฐ€ ํƒญ์„ ํด๋ฆญํ–ˆ๋‹ค๊ฐ€ ๋งˆ์Œ์ด ๋ฐ”๋€Œ์–ด ๋‹ค๋ฅธ ํƒญ์„ ํด๋ฆญํ•˜๋ฉด ์ฒซ ๋ฒˆ์งธ ๋ฆฌ๋ Œ๋”๋ง์ด ์™„๋ฃŒ๋  ๋•Œ๊นŒ์ง€ ๊ธฐ๋‹ค๋ฆด ํ•„์š” ์—†์ด ๋‹ค๋ฅธ ํƒญ์„ ํด๋ฆญํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

useTransition๊ณผ ์ผ๋ฐ˜ state ์—…๋ฐ์ดํŠธ์˜ ์ฐจ์ด์ 

์˜ˆ์ œ 1 of 2:
Transition์—์„œ ํ˜„์žฌ ํƒญ ์—…๋ฐ์ดํŠธ

์ด ์˜ˆ์‹œ์—์„œ๋Š” โ€œPostsโ€ ํƒญ์ด ์ธ์œ„์ ์œผ๋กœ ๋Š๋ ค์ง€๋„๋ก ํ•˜์—ฌ ๋ Œ๋”๋งํ•˜๋Š” ๋ฐ ์ตœ์†Œ 1์ดˆ๊ฐ€ ๊ฑธ๋ฆฌ๋„๋ก ํ–ˆ์Šต๋‹ˆ๋‹ค.

โ€œpostsโ€์„ ํด๋ฆญํ•œ ๋‹ค์Œ ๋ฐ”๋กœ โ€œContactโ€๋ฅผ ํด๋ฆญํ•ฉ๋‹ˆ๋‹ค. ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด โ€œPostsโ€์˜ ๋Š๋ฆฐ ๋ Œ๋”๋ง์ด ์ค‘๋‹จ๋ฉ๋‹ˆ๋‹ค. โ€œContactโ€ ํƒญ์ด ์ฆ‰์‹œ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค. ์ด state ์—…๋ฐ์ดํŠธ๋Š” transition์œผ๋กœ ํ‘œ์‹œ๋˜๋ฏ€๋กœ ๋Š๋ฆฌ๊ฒŒ ๋‹ค์‹œ ๋ Œ๋”๋งํ•ด๋„ ์‚ฌ์šฉ์ž ์ธํ„ฐํŽ˜์ด์Šค๊ฐ€ ๋ฉˆ์ถ”์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

import { useState, useTransition } from 'react';
import TabButton from './TabButton.js';
import AboutTab from './AboutTab.js';
import PostsTab from './PostsTab.js';
import ContactTab from './ContactTab.js';

export default function TabContainer() {
  const [isPending, startTransition] = useTransition();
  const [tab, setTab] = useState('about');

  function selectTab(nextTab) {
    startTransition(() => {
      setTab(nextTab);      
    });
  }

  return (
    <>
      <TabButton
        isActive={tab === 'about'}
        onClick={() => selectTab('about')}
      >
        About
      </TabButton>
      <TabButton
        isActive={tab === 'posts'}
        onClick={() => selectTab('posts')}
      >
        Posts (slow)
      </TabButton>
      <TabButton
        isActive={tab === 'contact'}
        onClick={() => selectTab('contact')}
      >
        Contact
      </TabButton>
      <hr />
      {tab === 'about' && <AboutTab />}
      {tab === 'posts' && <PostsTab />}
      {tab === 'contact' && <ContactTab />}
    </>
  );
}


Transition์—์„œ ์ƒ์œ„ ์ปดํฌ๋„ŒํŠธ ์—…๋ฐ์ดํŠธ

useTransition ํ˜ธ์ถœ์—์„œ๋„ ๋ถ€๋ชจ ์ปดํฌ๋„ŒํŠธ์˜ state๋ฅผ ์—…๋ฐ์ดํŠธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, ์•„๋ž˜์˜ TabButton ์ปดํฌ๋„ŒํŠธ๋Š” onClick ๋กœ์ง์„ transition์œผ๋กœ ๋ž˜ํ•‘ํ•ฉ๋‹ˆ๋‹ค.

export default function TabButton({ children, isActive, onClick }) {
const [isPending, startTransition] = useTransition();
if (isActive) {
return <b>{children}</b>
}
return (
<button onClick={() => {
startTransition(() => {
onClick();
});
}}>
{children}
</button>
);
}

๋ถ€๋ชจ ์ปดํฌ๋„ŒํŠธ๊ฐ€ onClick ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ ๋‚ด์—์„œ state๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๊ธฐ ๋•Œ๋ฌธ์— ํ•ด๋‹น state ์—…๋ฐ์ดํŠธ๋Š” transition์œผ๋กœ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค. ๊ทธ๋ ‡๊ธฐ ๋•Œ๋ฌธ์— ์•ž์˜ ์˜ˆ์‹œ์—์„œ์ฒ˜๋Ÿผ โ€œpostsโ€์„ ํด๋ฆญํ•œ ๋‹ค์Œ ๋ฐ”๋กœ โ€œContactโ€๋ฅผ ํด๋ฆญํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์„ ํƒํ•œ ํƒญ์„ ์—…๋ฐ์ดํŠธํ•˜๋Š” ๊ฒƒ์€ transition์œผ๋กœ ํ‘œ์‹œ๋˜๋ฏ€๋กœ ์‚ฌ์šฉ์ž ์ƒํ˜ธ์ž‘์šฉ์„ ์ฐจ๋‹จํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

import { useTransition } from 'react';

export default function TabButton({ children, isActive, onClick }) {
  const [isPending, startTransition] = useTransition();
  if (isActive) {
    return <b>{children}</b>
  }
  return (
    <button onClick={() => {
      startTransition(() => {
        onClick();
      });
    }}>
      {children}
    </button>
  );
}


Transition ์ค‘์— ๋ณด๋ฅ˜ ์ค‘์ธ ์‹œ๊ฐ์  state ํ‘œ์‹œ

useTransition์ด ๋ฐ˜ํ™˜ํ•˜๋Š” isPending boolean ๊ฐ’์„ ์‚ฌ์šฉํ•˜์—ฌ transition์ด ์ง„ํ–‰ ์ค‘์ž„์„ ์‚ฌ์šฉ์ž์—๊ฒŒ ํ‘œ์‹œํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด ํƒญ ๋ฒ„ํŠผ์€ ํŠน๋ณ„ํ•œ โ€œpendingโ€ ์‹œ๊ฐ์  ์ƒํƒœ๋ฅผ ๊ฐ€์งˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

function TabButton({ children, isActive, onClick }) {
const [isPending, startTransition] = useTransition();
// ...
if (isPending) {
return <b className="pending">{children}</b>;
}
// ...

์ด์ œ ํƒญ ๋ฒ„ํŠผ ์ž์ฒด๊ฐ€ ๋ฐ”๋กœ ์—…๋ฐ์ดํŠธ๋˜๋ฏ€๋กœ โ€œPostsโ€์„ ํด๋ฆญํ•˜๋Š” ๋ฐ˜์‘์ด ๋” ๋นจ๋ผ์ง„ ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

import { useTransition } from 'react';

export default function TabButton({ children, isActive, onClick }) {
  const [isPending, startTransition] = useTransition();
  if (isActive) {
    return <b>{children}</b>
  }
  if (isPending) {
    return <b className="pending">{children}</b>;
  }
  return (
    <button onClick={() => {
      startTransition(() => {
        onClick();
      });
    }}>
      {children}
    </button>
  );
}


์›์น˜ ์•Š๋Š” ๋กœ๋”ฉ ํ‘œ์‹œ๊ธฐ ๋ฐฉ์ง€

์ด ์˜ˆ์‹œ์—์„œ PostsTab ์ปดํฌ๋„ŒํŠธ๋Š” Suspense-enabled ๋ฐ์ดํ„ฐ ์†Œ์Šค๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ผ๋ถ€ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค. โ€œPostsโ€ ํƒญ์„ ํด๋ฆญํ•˜๋ฉด PostsTab ์ปดํฌ๋„ŒํŠธ๊ฐ€ suspends ๋˜์–ด ๊ฐ€์žฅ ๊ฐ€๊นŒ์šด ๋กœ๋”ฉ ํด๋ฐฑ์ด ๋‚˜ํƒ€๋‚ฉ๋‹ˆ๋‹ค.

import { Suspense, useState } from 'react';
import TabButton from './TabButton.js';
import AboutTab from './AboutTab.js';
import PostsTab from './PostsTab.js';
import ContactTab from './ContactTab.js';

export default function TabContainer() {
  const [tab, setTab] = useState('about');
  return (
    <Suspense fallback={<h1>๐ŸŒ€ Loading...</h1>}>
      <TabButton
        isActive={tab === 'about'}
        onClick={() => setTab('about')}
      >
        About
      </TabButton>
      <TabButton
        isActive={tab === 'posts'}
        onClick={() => setTab('posts')}
      >
        Posts
      </TabButton>
      <TabButton
        isActive={tab === 'contact'}
        onClick={() => setTab('contact')}
      >
        Contact
      </TabButton>
      <hr />
      {tab === 'about' && <AboutTab />}
      {tab === 'posts' && <PostsTab />}
      {tab === 'contact' && <ContactTab />}
    </Suspense>
  );
}

๋กœ๋”ฉ ํ‘œ์‹œ๊ธฐ๋ฅผ ํ‘œ์‹œํ•˜๊ธฐ ์œ„ํ•ด ์ „์ฒด ํƒญ ์ปจํ…Œ์ด๋„ˆ๋ฅผ ์ˆจ๊ธฐ๋ฉด ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์ด ์–ด์ƒ‰ํ•ด์ง‘๋‹ˆ๋‹ค. TabButton์— useTransition์„ ์ถ”๊ฐ€ํ•˜๋ฉด ํƒญ ๋ฒ„ํŠผ์— ๋ณด๋ฅ˜ ์ค‘์ธ ์ƒํƒœ๋ฅผ ํ‘œ์‹œํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

โ€œPostsโ€์„ ํด๋ฆญํ•˜๋ฉด ๋” ์ด์ƒ ์ „์ฒด ํƒญ ์ปจํ…Œ์ด๋„ˆ๊ฐ€ ์Šคํ”ผ๋„ˆ๋กœ ๋ฐ”๋€Œ์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

import { useTransition } from 'react';

export default function TabButton({ children, isActive, onClick }) {
  const [isPending, startTransition] = useTransition();
  if (isActive) {
    return <b>{children}</b>
  }
  if (isPending) {
    return <b className="pending">{children}</b>;
  }
  return (
    <button onClick={() => {
      startTransition(() => {
        onClick();
      });
    }}>
      {children}
    </button>
  );
}

Suspense์—์„œ transition์„ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด ์ž์„ธํžˆ ์•Œ์•„๋ณด์„ธ์š”.

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

Transition์€ ์ด๋ฏธ ํ‘œ์‹œ๋œ ์ฝ˜ํ…์ธ (์˜ˆ์‹œ: ํƒญ ์ปจํ…Œ์ด๋„ˆ)๋ฅผ ์ˆจ๊ธฐ์ง€ ์•Š์„ ๋งŒํผ๋งŒ โ€œ๋Œ€๊ธฐโ€ํ•ฉ๋‹ˆ๋‹ค. ๋งŒ์•ฝ Posts ํƒญ์— ์ค‘์ฒฉ๋œ <Suspense> ๊ฒฝ๊ณ„๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ transition์€ ์ด๋ฅผ โ€œ๋Œ€๊ธฐโ€ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.


Suspense-enabled ๋ผ์šฐํ„ฐ ๊ตฌ์ถ•

React ํ”„๋ ˆ์ž„์›Œํฌ๋‚˜ ๋ผ์šฐํ„ฐ๋ฅผ ๊ตฌ์ถ•ํ•˜๋Š” ๊ฒฝ์šฐ ํŽ˜์ด์ง€ ํƒ์ƒ‰์„ transition์œผ๋กœ ํ‘œ์‹œํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

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

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

๋‘ ๊ฐ€์ง€ ์ด์œ ๋กœ ์ด ๋ฐฉ๋ฒ•์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

๋‹ค์Œ์€ ํƒ์ƒ‰์„ ์œ„ํ•ด 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>;
}

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

Suspense-enabled ๋ผ์šฐํ„ฐ๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ ํƒ์ƒ‰ ์—…๋ฐ์ดํŠธ๋ฅผ transition์œผ๋กœ ๋ž˜ํ•‘ํ•  ๊ฒƒ์œผ๋กœ ์˜ˆ์ƒ๋ฉ๋‹ˆ๋‹ค.


๋ฌธ์ œ ํ•ด๊ฒฐ

Transition์—์„œ ์ž…๋ ฅ ์—…๋ฐ์ดํŠธ๊ฐ€ ์ž‘๋™ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค

์ž…๋ ฅ์„ ์ œ์–ดํ•˜๋Š” state ๋ณ€์ˆ˜์—๋Š” transition์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.

const [text, setText] = useState('');
// ...
function handleChange(e) {
// โŒ ์ œ์–ด๋œ ์ž…๋ ฅ state์— transition์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.
startTransition(() => {
setText(e.target.value);
});
}
// ...
return <input value={text} onChange={handleChange} />;

์ด๋Š” transition์ด non-blocking์ด์ง€๋งŒ, ๋ณ€๊ฒฝ ์ด๋ฒคํŠธ์— ๋Œ€ํ•œ ์‘๋‹ต์œผ๋กœ ์ž…๋ ฅ์„ ์—…๋ฐ์ดํŠธํ•˜๋Š” ๊ฒƒ์€ ๋™๊ธฐ์ ์œผ๋กœ ์ด๋ฃจ์–ด์ ธ์•ผ ํ•˜๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. ์ž…๋ ฅ์— ๋Œ€ํ•œ ์‘๋‹ต์œผ๋กœ transition์„ ์‹คํ–‰ํ•˜๋ ค๋ฉด ๋‘ ๊ฐ€์ง€ ์˜ต์…˜์ด ์žˆ์Šต๋‹ˆ๋‹ค.

  1. ๋‘ ๊ฐœ์˜ ๊ฐœ๋ณ„ state ๋ณ€์ˆ˜๋ฅผ ์„ ์–ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ•˜๋‚˜๋Š” ์ž…๋ ฅ state(ํ•ญ์ƒ ๋™๊ธฐ์ ์œผ๋กœ ์—…๋ฐ์ดํŠธ๋จ) ์šฉ์ด๊ณ  ๋‹ค๋ฅธ ํ•˜๋‚˜๋Š” transition์‹œ ์—…๋ฐ์ดํŠธํ•  state์ž…๋‹ˆ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด ๋™๊ธฐ state๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ž…๋ ฅ์„ ์ œ์–ดํ•˜๊ณ  (์ž…๋ ฅ๋ณด๋‹ค โ€œ์ง€์—ฐโ€๋˜๋Š”) transition state ๋ณ€์ˆ˜๋ฅผ ๋‚˜๋จธ์ง€ ๋ Œ๋”๋ง ๋กœ์ง์— ์ „๋‹ฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  2. ๋˜๋Š” state ๋ณ€์ˆ˜๊ฐ€ ํ•˜๋‚˜ ์žˆ๊ณ  ์‹ค์ œ ๊ฐ’๋ณด๋‹ค โ€œ์ง€์—ฐโ€๋˜๋Š” useDeferredValue๋ฅผ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋ฉด non-blocking ๋ฆฌ๋ Œ๋”๋ง์ด ์ƒˆ๋กœ์šด ๊ฐ’์„ ์ž๋™์œผ๋กœ โ€œ๋”ฐ๋ผ์žก๊ธฐโ€ ์œ„ํ•ด ํŠธ๋ฆฌ๊ฑฐ๋ฉ๋‹ˆ๋‹ค.

React๊ฐ€ state ์—…๋ฐ์ดํŠธ๋ฅผ transition์œผ๋กœ ์ฒ˜๋ฆฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค

state ์—…๋ฐ์ดํŠธ๋ฅผ transition์œผ๋กœ ๋ž˜ํ•‘ํ•  ๋•Œ๋Š” startTransition ํ˜ธ์ถœ ๋„์ค‘์— ๋ฐœ์ƒํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

startTransition(() => {
// โœ… startTransition ํ˜ธ์ถœ *๋„์ค‘* state ์„ค์ •
setPage('/about');
});

startTransition์— ์ „๋‹ฌํ•˜๋Š” ํ•จ์ˆ˜๋Š” ๋™๊ธฐ์‹์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

์•„๋ž˜์™€ ๊ฐ™์€ ์—…๋ฐ์ดํŠธ๋Š” transition์œผ๋กœ ํ‘œ์‹œํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.

startTransition(() => {
// โŒ startTransition ํ˜ธ์ถœ *ํ›„์—* state ์„ค์ •
setTimeout(() => {
setPage('/about');
}, 1000);
});

๋Œ€์‹  ๋‹ค์Œ๊ณผ ๊ฐ™์ด ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

setTimeout(() => {
startTransition(() => {
// โœ… startTransition ํ˜ธ์ถœ *๋„์ค‘* state ์„ค์ •
setPage('/about');
});
}, 1000);

๋งˆ์ฐฌ๊ฐ€์ง€๋กœ ์—…๋ฐ์ดํŠธ๋ฅผ ์ด์™€ ๊ฐ™์€ transition์œผ๋กœ ํ‘œ์‹œํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.

startTransition(async () => {
await someAsyncFunction();
// โŒ startTransition ํ˜ธ์ถœ *ํ›„์—* state ์„ค์ •
setPage('/about');
});

ํ•˜์ง€๋งŒ ์ด ๋ฐฉ๋ฒ•์ด ๋Œ€์‹  ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค.

await someAsyncFunction();
startTransition(() => {
// โœ… startTransition ํ˜ธ์ถœ *๋„์ค‘* state ์„ค์ •
setPage('/about');
});

์ปดํฌ๋„ŒํŠธ ์™ธ๋ถ€์—์„œ useTransition์„ ํ˜ธ์ถœํ•˜๊ณ  ์‹ถ์Šต๋‹ˆ๋‹ค

Hook์ด๊ธฐ ๋•Œ๋ฌธ์— ์ปดํฌ๋„ŒํŠธ ์™ธ๋ถ€์—์„œ useTransition์„ ํ˜ธ์ถœํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ์ด ๊ฒฝ์šฐ ๋Œ€์‹  ๋…๋ฆฝํ˜• startTransition ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜์„ธ์š”. ๋™์ผํ•œ ๋ฐฉ์‹์œผ๋กœ ์ž‘๋™ํ•˜์ง€๋งŒ isPending ํ‘œ์‹œ๊ธฐ๋ฅผ ์ œ๊ณตํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.


startTransition์— ์ „๋‹ฌํ•œ ํ•จ์ˆ˜๋Š” ์ฆ‰์‹œ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค

์ด ์ฝ”๋“œ๋ฅผ ์‹คํ–‰ํ•˜๋ฉด 1, 2, 3์ด ์ถœ๋ ฅ๋ฉ๋‹ˆ๋‹ค.

console.log(1);
startTransition(() => {
console.log(2);
setPage('/about');
});
console.log(3);

1, 2, 3์„ ์ถœ๋ ฅํ•  ๊ฒƒ์œผ๋กœ ์˜ˆ์ƒ๋ฉ๋‹ˆ๋‹ค. startTransition์— ์ „๋‹ฌํ•œ ํ•จ์ˆ˜๋Š” ์ง€์—ฐ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋ธŒ๋ผ์šฐ์ € setTimeout๊ณผ ๋‹ฌ๋ฆฌ ๋‚˜์ค‘์— ์ฝœ๋ฐฑ์„ ์‹คํ–‰ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. React๋Š” ํ•จ์ˆ˜๋ฅผ ์ฆ‰์‹œ ์‹คํ–‰ํ•˜์ง€๋งŒ, ํ•จ์ˆ˜๊ฐ€ ์‹คํ–‰๋˜๋Š” ๋™์•ˆ ์˜ˆ์•ฝ๋œ ๋ชจ๋“  ์ƒํƒœ ์—…๋ฐ์ดํŠธ๋Š” ํŠธ๋žœ์ง€์…˜์œผ๋กœ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค. ์•„๋ž˜์™€ ๊ฐ™์ด ์ž‘๋™ํ•œ๋‹ค๊ณ  ์ƒ์ƒํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค.

// React ์ž‘๋™ ๋ฐฉ์‹์˜ ๊ฐ„์†Œํ™”๋œ ๋ฒ„์ „

let isInsideTransition = false;

function startTransition(scope) {
isInsideTransition = true;
scope();
isInsideTransition = false;
}

function setState() {
if (isInsideTransition) {
// ... transition state ์—…๋ฐ์ดํŠธ ์˜ˆ์•ฝ ...
} else {
// ... ๊ธด๊ธ‰ state ์—…๋ฐ์ดํŠธ ์˜ˆ์•ฝ ...
}
}