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์œผ๋กœ ๋ž˜ํ•‘ํ•  ๊ฒƒ์œผ๋กœ ์˜ˆ์ƒ๋ฉ๋‹ˆ๋‹ค.


Displaying an error to users with a error boundary

Canary

Error Boundary for useTransition is currently only available in Reactโ€™s canary and experimental channels. Learn more about Reactโ€™s release channels here.

If a function passed to startTransition throws an error, you can display an error to your user with an error boundary. To use an error boundary, wrap the component where you are calling the useTransition in an error boundary. Once the function passed to startTransition errors, the fallback for the error boundary will be displayed.

import { useTransition } from "react";
import { ErrorBoundary } from "react-error-boundary";

export function AddCommentContainer() {
  return (
    <ErrorBoundary fallback={<p>โš ๏ธSomething went wrong</p>}>
        <AddCommentButton />
    </ErrorBoundary>
  );
}

function addComment(comment) {
  // For demonstration purposes to show Error Boundary
  if(comment == null){
    throw Error('Example error')
  }
}

function AddCommentButton() {
  const [pending, startTransition] = useTransition();

  return (
    <button
      disabled={pending}
      onClick={() => {
        startTransition(() => {
          // Intentionally not passing a comment
          // so error gets thrown
          addComment();
        });
      }}>
        Add comment
      </button>
  );
}


Troubleshooting

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 ์—…๋ฐ์ดํŠธ ์˜ˆ์•ฝ ...
}
}