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(action)

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

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

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

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

startTransition ๋‚ด์—์„œ ํ˜ธ์ถœ๋˜๋Š” ํ•จ์ˆ˜๋ฅผ โ€œActionโ€์ด๋ผ๊ณ  ํ•ฉ๋‹ˆ๋‹ค.

startTransition์— ์ „๋‹ฌ๋œ ํ•จ์ˆ˜๋ฅผ โ€œActionโ€์ด๋ผ๊ณ  ํ•ฉ๋‹ˆ๋‹ค. ๊ด€๋ก€์ ์œผ๋กœ, startTransition ๋‚ด๋ถ€์—์„œ ํ˜ธ์ถœ๋˜๋Š” ๋ชจ๋“  ์ฝœ๋ฐฑ(์˜ˆ: ์ฝœ๋ฐฑ ํ”„๋กœํผํ‹ฐ)์˜ ์ด๋ฆ„์€ action์ด๊ฑฐ๋‚˜ โ€œActionโ€ ์ ‘๋ฏธ์‚ฌ๋ฅผ ํฌํ•จํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

function SubmitButton({ submitAction }) {
const [isPending, startTransition] = useTransition();

return (
<button
disabled={isPending}
onClick={() => {
startTransition(async () => {
await submitAction();
});
}}
>
Submit
</button>
);
}

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

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

๋ฐ˜ํ™˜๊ฐ’

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

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

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

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

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

  • startTransition์— ์ „๋‹ฌํ•˜๋Š” ํ•จ์ˆ˜๋Š” ์ฆ‰์‹œ ํ˜ธ์ถœ๋˜๋ฉฐ, ์‹คํ–‰ ์ค‘ ๋ฐœ์ƒํ•˜๋Š” ๋ชจ๋“  ์ƒํƒœ ์—…๋ฐ์ดํŠธ๋ฅผ Transition์œผ๋กœ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด setTimeout ๋‚ด์—์„œ ์ƒํƒœ๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๋ ค๊ณ  ํ•˜๋ฉด, ํ•ด๋‹น ์—…๋ฐ์ดํŠธ๋Š” Transition์œผ๋กœ ํ‘œ์‹œ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

  • ๋น„๋™๊ธฐ ์š”์ฒญ ์ดํ›„์˜ ์ƒํƒœ ์—…๋ฐ์ดํŠธ๋ฅผ ์ „ํ™˜์œผ๋กœ ํ‘œ์‹œํ•˜๋ ค๋ฉด, ๋ฐ˜๋“œ์‹œ ๋˜ ๋‹ค๋ฅธ startTransition์œผ๋กœ ๊ฐ์‹ธ์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ด๋Š” ์•Œ๋ ค์ง„ ์ œํ•œ ์‚ฌํ•ญ์œผ๋กœ ํ–ฅํ›„ ์ˆ˜์ •๋  ์˜ˆ์ •์ž…๋‹ˆ๋‹ค(๋ฌธ์ œ ํ•ด๊ฒฐ ์ฐธ์กฐ).

  • startTransition ํ•จ์ˆ˜๋Š” ์•ˆ์ •๋œ ์‹๋ณ„์„ฑ(stable identity)์„ ๊ฐ€์ง€๋ฏ€๋กœ Effect ์˜์กด์„ฑ์—์„œ ์ƒ๋žต๋˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋งŽ์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ํฌํ•จํ•ด๋„ Effect๊ฐ€ ์‹คํ–‰๋˜์ง€๋Š” ์•Š์Šต๋‹ˆ๋‹ค. linter๊ฐ€ ์˜์กด์„ฑ์„ ์ƒ๋žตํ•ด๋„ ์˜ค๋ฅ˜๋ฅผ ๋ฐœ์ƒ์‹œํ‚ค์ง€ ์•Š๋Š”๋‹ค๋ฉด ์ƒ๋žตํ•ด๋„ ์•ˆ์ „ํ•ฉ๋‹ˆ๋‹ค. ์ž์„ธํ•œ ๋‚ด์šฉ์€ Effect ์˜์กด์„ฑ ์ œ๊ฑฐ์— ๋Œ€ํ•œ ๋ฌธ์„œ๋ฅผ ์ฐธ๊ณ ํ•˜์„ธ์š”.

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

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

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

์‚ฌ์šฉ๋ฒ•

Actions์œผ๋กœ non-blocking ์—…๋ฐ์ดํŠธ ์ˆ˜ํ–‰

์ปดํฌ๋„ŒํŠธ ์ƒ๋‹จ์—์„œ useTransition์„ ํ˜ธ์ถœํ•˜์—ฌ Actions์„ ์ƒ์„ฑํ•˜๊ณ , ๋Œ€๊ธฐ ์ƒํƒœ์— ์ ‘๊ทผํ•˜์„ธ์š”.

import {useState, useTransition} from 'react';

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

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

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

Transition์„ ์‹œ์ž‘ํ•˜๋ ค๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์ด startTransition์— ํ•จ์ˆ˜๋ฅผ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค.

import {useState, useTransition} from 'react';
import {updateQuantity} from './api';

function CheckoutForm() {
const [isPending, startTransition] = useTransition();
const [quantity, setQuantity] = useState(1);

function onSubmit(newQuantity) {
startTransition(async function () {
const savedQuantity = await updateQuantity(newQuantity);
startTransition(() => {
setQuantity(savedQuantity);
});
});
}
// ...
}

startTransition์— ์ „๋‹ฌํ•œ ํ•จ์ˆ˜๋ฅผ โ€œActionโ€์ด๋ผ๊ณ  ํ•ฉ๋‹ˆ๋‹ค. Action ์•ˆ์—์„œ๋Š” state๋ฅผ ์—…๋ฐ์ดํŠธํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ํ•„์š”ํ•˜๋‹ค๋ฉด ๋ถ€์ˆ˜ ํšจ๊ณผ๋„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ์ž‘์—…์€ ํŽ˜์ด์ง€์˜ ์‚ฌ์šฉ์ž ์ƒํ˜ธ์ž‘์šฉ์„ ์ฐจ๋‹จํ•˜์ง€ ์•Š๊ณ  ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ์ฒ˜๋ฆฌ๋ฉ๋‹ˆ๋‹ค. ํ•˜๋‚˜์˜ Transition์—๋Š” ์—ฌ๋Ÿฌ Action์ด ํฌํ•จ๋  ์ˆ˜ ์žˆ๊ณ , Transition์ด ์ง„ํ–‰๋˜๋Š” ๋™์•ˆ์—๋„ UI๋Š” ๋ฐ˜์‘์„ฑ์„ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด ์‚ฌ์šฉ์ž๊ฐ€ ํƒญ์„ ํด๋ฆญํ•œ ๋’ค ๋งˆ์Œ์ด ๋ฐ”๋€Œ์–ด ๋‹ค๋ฅธ ํƒญ์„ ๋‹ค์‹œ ํด๋ฆญํ•˜๋”๋ผ๋„, ์ฒซ ๋ฒˆ์งธ ์—…๋ฐ์ดํŠธ๊ฐ€ ๋๋‚˜๊ธฐ๋ฅผ ๊ธฐ๋‹ค๋ฆฌ์ง€ ์•Š๊ณ  ๋‘ ๋ฒˆ์งธ ํด๋ฆญ์ด ์ฆ‰์‹œ ์ฒ˜๋ฆฌ๋ฉ๋‹ˆ๋‹ค.

์ง„ํ–‰ ์ค‘์ธ Transition์— ๋Œ€ํ•ด ์‚ฌ์šฉ์ž์—๊ฒŒ ํ”ผ๋“œ๋ฐฑ์„ ์ œ๊ณตํ•˜๊ธฐ ์œ„ํ•ด isPending ์ƒํƒœ๋Š” startTransition์„ ์ฒ˜์Œ ํ˜ธ์ถœํ•  ๋•Œ true๋กœ ์ „ํ™˜๋˜๋ฉฐ, ๋ชจ๋“  Action์ด ์™„๋ฃŒ๋˜์–ด ์ตœ์ข… ์ƒํƒœ๊ฐ€ ์‚ฌ์šฉ์ž์—๊ฒŒ ํ‘œ์‹œ๋  ๋•Œ๊นŒ์ง€ true ์ƒํƒœ๋ฅผ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค. Transition์€ Action ๋‚ด์˜ ์‚ฌ์ด๋“œ ์ดํŽ™ํŠธ๊ฐ€ ์™„๋ฃŒ๋˜๋„๋ก ๋ณด์žฅํ•˜์—ฌ ์›์น˜ ์•Š๋Š” ๋กœ๋”ฉ ํ‘œ์‹œ๊ธฐ๊ฐ€ ํ‘œ์‹œ๋˜์ง€ ์•Š๋„๋ก ํ•ฉ๋‹ˆ๋‹ค. ๋˜ํ•œ, Transition์ด ์ง„ํ–‰ ์ค‘์ผ ๋•Œ useOptimistic์„ ์‚ฌ์šฉํ•˜์—ฌ ์ฆ‰๊ฐ์ ์ธ ํ”ผ๋“œ๋ฐฑ์„ ์ œ๊ณตํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Action๊ณผ ์ผ๋ฐ˜ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ์˜ ์ฐจ์ด์ 

์˜ˆ์‹œ 1 of 2:
Action์—์„œ ์ˆ˜๋Ÿ‰ ์—…๋ฐ์ดํŠธ

์ด ์˜ˆ์‹œ์—์„œ updateQuantity ํ•จ์ˆ˜๋Š” ์นดํŠธ์— ์žˆ๋Š” ํ’ˆ๋ชฉ์˜ ์ˆ˜๋Ÿ‰์„ ์—…๋ฐ์ดํŠธํ•˜๊ธฐ ์œ„ํ•ด ์„œ๋ฒ„์— ์š”์ฒญํ•˜๋Š” ์‹œ๋ฎฌ๋ ˆ์ด์…˜์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. ์ด ํ•จ์ˆ˜๋Š” ์š”์ฒญ์„ ์™„๋ฃŒํ•˜๋Š” ๋ฐ ์ตœ์†Œ 1์ดˆ๊ฐ€ ์†Œ์š”๋˜๋„๋ก ์ธ์œ„์ ์œผ๋กœ ์†๋„๊ฐ€ ๋Šฆ์ถฐ์ ธ ์žˆ์Šต๋‹ˆ๋‹ค.

์ˆ˜๋Ÿ‰์„ ๋น ๋ฅด๊ฒŒ ์—ฌ๋Ÿฌ ๋ฒˆ ์—…๋ฐ์ดํŠธํ•˜๋ฉด, ์š”์ฒญ์ด ์ง„ํ–‰ ์ค‘์ธ ๋™์•ˆ์—๋Š” โ€œTotalโ€ ์ƒํƒœ๊ฐ€ ๋Œ€๊ธฐ ์ค‘์œผ๋กœ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์ตœ์ข… ์š”์ฒญ์ด ์™„๋ฃŒ๋œ ํ›„์—๋งŒ โ€œTotalโ€์ด ์—…๋ฐ์ดํŠธ๋ฉ๋‹ˆ๋‹ค. ์—…๋ฐ์ดํŠธ๊ฐ€ Action ๋‚ด์—์„œ ๋ฐœ์ƒํ•˜๊ธฐ ๋•Œ๋ฌธ์—, ์š”์ฒญ์ด ์ง„ํ–‰ ์ค‘์ธ ๋™์•ˆ์—๋„ โ€œquantityโ€์€ ๊ณ„์†ํ•ด์„œ ์—…๋ฐ์ดํŠธ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

import { useState, useTransition } from "react";
import { updateQuantity } from "./api";
import Item from "./Item";
import Total from "./Total";

export default function App({}) {
  const [quantity, setQuantity] = useState(1);
  const [isPending, startTransition] = useTransition();

  const updateQuantityAction = async newQuantity => {
    // transition์˜ ๋ณด๋ฅ˜ ์ค‘์ธ ์ƒํƒœ์— ์•ก์„ธ์Šคํ•˜๋ ค๋ฉด,
    // startTransition์„ ๋‹ค์‹œ ํ˜ธ์ถœํ•˜์„ธ์š”.
    startTransition(async () => {
      const savedQuantity = await updateQuantity(newQuantity);
      startTransition(() => {
        setQuantity(savedQuantity);
      });
    });
  };

  return (
    <div>
      <h1>Checkout</h1>
      <Item action={updateQuantityAction}/>
      <hr />
      <Total quantity={quantity} isPending={isPending} />
    </div>
  );
}

์ด ์˜ˆ์‹œ๋Š” Actions์˜ ์ž‘๋™ ๋ฐฉ์‹์„ ๋ณด์—ฌ์ฃผ๊ธฐ ์œ„ํ•œ ๊ธฐ๋ณธ ์˜ˆ์‹œ์ด์ง€๋งŒ, ์ˆœ์„œ๋Œ€๋กœ ์™„๋ฃŒ๋˜๋Š” ์š”์ฒญ์€ ์ฒ˜๋ฆฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ˆ˜๋Ÿ‰์„ ์—ฌ๋Ÿฌ ๋ฒˆ ์—…๋ฐ์ดํŠธํ•˜๋Š” ๊ฒฝ์šฐ ์ด์ „ ์š”์ฒญ์ด ์™„๋ฃŒ๋œ ํ›„ ๋‚˜์ค‘์— ์š”์ฒญ์ด ์™„๋ฃŒ๋˜์–ด ์ˆ˜๋Ÿ‰์ด ์ˆœ์„œ๋Œ€๋กœ ์—…๋ฐ์ดํŠธ๋˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋Š” ์•Œ๋ ค์ง„ ์ œํ•œ ์‚ฌํ•ญ์œผ๋กœ ํ–ฅํ›„ ์ˆ˜์ •๋  ์˜ˆ์ •์ž…๋‹ˆ๋‹ค(๋ฌธ์ œ ํ•ด๊ฒฐ ์ฐธ์กฐ).

์ผ๋ฐ˜์ ์ธ ์‚ฌ์šฉ ์‚ฌ๋ก€๋ฅผ ์œ„ํ•ด React๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋‚ด์žฅ ์ถ”์ƒํ™” ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

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


์ปดํฌ๋„ŒํŠธ์—์„œ Action ํ”„๋กœํผํ‹ฐ๋ฅผ ๋…ธ์ถœํ•˜๊ธฐ

์ปดํฌ๋„ŒํŠธ์—์„œ action ํ”„๋กœํผํ‹ฐ๋ฅผ ๋…ธ์ถœ์‹œ์ผœ ๋ถ€๋ชจ ์ปดํฌ๋„ŒํŠธ์—์„œ Action์„ ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์˜ˆ๋ฅผ ๋“ค์–ด, ์ด TabButton ์ปดํฌ๋„ŒํŠธ๋Š” onClick์—์„œ ์‹คํ–‰๋  ๋กœ์ง์ด action prop์œผ๋กœ ๊ฐ์‹ธ์ ธ์žˆ์Šต๋‹ˆ๋‹ค.

export default function TabButton({ action, children, isActive }) {
const [isPending, startTransition] = useTransition();
if (isActive) {
return <b>{children}</b>
}
return (
<button onClick={() => {
startTransition(async () => {
// await the action that's passed in.
// This allows it to be either sync or async.
await action();
});
}}>
{children}
</button>
);
}

๋ถ€๋ชจ ์ปดํฌ๋„ŒํŠธ๊ฐ€ action ๋‚ด๋ถ€์—์„œ ์ƒํƒœ๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๊ธฐ ๋•Œ๋ฌธ์—, ํ•ด๋‹น ์ƒํƒœ ์—…๋ฐ์ดํŠธ๋Š” Transition์œผ๋กœ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค. ๊ทธ๋ ‡๊ธฐ ๋•Œ๋ฌธ์— โ€œPostsโ€์„ ํด๋ฆญํ•œ ํ›„ ์ฆ‰์‹œ โ€œContactโ€๋ฅผ ํด๋ฆญํ•ด๋„ ์‚ฌ์šฉ์ž ์ƒํ˜ธ์ž‘์šฉ์ด ์ฐจ๋‹จ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

import { useTransition } from 'react';

export default function TabButton({ action, children, isActive }) {
  const [isPending, startTransition] = useTransition();
  if (isActive) {
    return <b>{children}</b>
  }
  if (isPending) {
    return <b className="pending">{children}</b>;
  }
  return (
    <button onClick={async () => {
      startTransition(async () => {
        // await the action that's passed in.
        // This allows it to be either sync or async.
        await action();
      });
    }}>
      {children}
    </button>
  );
}

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

When exposing an action prop from a component, you should await it inside the transition.

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด action ์ฝœ๋ฐฑ์ด ๋™๊ธฐ์ ์ด๋“  ๋น„๋™๊ธฐ์ ์ด๋“  ์ƒ๊ด€์—†์ด ์ž‘๋™ํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, action ๋‚ด๋ถ€์˜ await์„ ์ถ”๊ฐ€์ ์ธ startTransition์œผ๋กœ ๊ฐ์Œ€ ํ•„์š”๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.


๋Œ€๊ธฐ ์ƒํƒœ๋ฅผ ์‹œ๊ฐ์ ์œผ๋กœ ํ‘œํ˜„ํ•˜๊ธฐ

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

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

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

import { useTransition } from 'react';

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


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

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

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'}
        action={() => setTab('about')}
      >
        About
      </TabButton>
      <TabButton
        isActive={tab === 'posts'}
        action={() => setTab('posts')}
      >
        Posts
      </TabButton>
      <TabButton
        isActive={tab === 'contact'}
        action={() => setTab('contact')}
      >
        Contact
      </TabButton>
      <hr />
      {tab === 'about' && <AboutTab />}
      {tab === 'posts' && <PostsTab />}
      {tab === 'contact' && <ContactTab />}
    </Suspense>
  );
}

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

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

import { useTransition } from 'react';

export default function TabButton({ action, children, isActive }) {
  const [isPending, startTransition] = useTransition();
  if (isActive) {
    return <b>{children}</b>
  }
  if (isPending) {
    return <b className="pending">{children}</b>;
  }
  return (
    <button onClick={() => {
      startTransition(async () => {
        await action();
      });
    }}>
      {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);
});
}
// ...

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

๋‹ค์Œ์€ navigation์— Transitions๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฐ„๋‹จํ•œ ๋ผ์šฐํ„ฐ ์˜ˆ์‹œ์ž…๋‹ˆ๋‹ค.

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


Error boundary๋กœ ์‚ฌ์šฉ์ž์—๊ฒŒ ์˜ค๋ฅ˜ ํ‘œ์‹œํ•˜๊ธฐ

startTransition์— ์ „๋‹ฌ๋œ ํ•จ์ˆ˜์—์„œ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด error boundary๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์‚ฌ์šฉ์ž์—๊ฒŒ ์˜ค๋ฅ˜๋ฅผ ํ‘œ์‹œํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. error boundary๋ฅผ ์‚ฌ์šฉํ•˜๋ ค๋ฉด useTransition์„ ํ˜ธ์ถœํ•˜๋Š” ์ปดํฌ๋„ŒํŠธ๋ฅผ error boundary๋กœ ๊ฐ์‹ธ๋ฉด ๋ฉ๋‹ˆ๋‹ค. startTransition์— ์ „๋‹ฌ๋œ ํ•จ์ˆ˜์—์„œ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด error boundary์˜ Fallback์ด ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค.

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 new Error("Example Error: An error thrown to trigger error boundary");
  }
}

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์— ์ „๋‹ฌํ•˜๋Š” ํ•จ์ˆ˜๋Š” ๋™๊ธฐ์‹์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. You canโ€™t mark an update as a Transition like this:

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

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

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

React๋Š” await ์ดํ›„์˜ ์ƒํƒœ ์—…๋ฐ์ดํŠธ๋ฅผ Transition์œผ๋กœ ์ฒ˜๋ฆฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

startTransition ํ•จ์ˆ˜ ๋‚ด๋ถ€์—์„œ await๋ฅผ ์‚ฌ์šฉํ•  ๊ฒฝ์šฐ, await ์ดํ›„์— ๋ฐœ์ƒํ•˜๋Š” ์ƒํƒœ ์—…๋ฐ์ดํŠธ๋Š” Transition์œผ๋กœ ์ฒ˜๋ฆฌ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๊ฐ await ์ดํ›„์— ๋ฐœ์ƒํ•˜๋Š” ์ƒํƒœ ์—…๋ฐ์ดํŠธ๋ฅผ ๋ณ„๋„์˜ startTransition ํ˜ธ์ถœ๋กœ ๊ฐ์‹ธ์•ผ ํ•ฉ๋‹ˆ๋‹ค.

startTransition(async () => {
await someAsyncFunction();
// โŒ await ์ดํ›„์— startTransition์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š์Œ
setPage('/about');
});

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

startTransition(async () => {
await someAsyncFunction();
// โœ… await *์ดํ›„์—* startTransition์„ ์‚ฌ์šฉ
startTransition(() => {
setPage('/about');
});
});

์ด๋Š” JavaScript์˜ ํ•œ๊ณ„๋กœ ์ธํ•ด React๊ฐ€ AsyncContext์˜ ๋ฒ”์œ„๋ฅผ ์žƒ๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. ํ–ฅํ›„ AsyncContext๊ฐ€ ์ง€์›๋˜๋ฉด ์ด๋Ÿฌํ•œ ์ œํ•œ ์‚ฌํ•ญ์€ ํ•ด๊ฒฐ๋  ๊ฒƒ์ž…๋‹ˆ๋‹ค.


์ปดํฌ๋„ŒํŠธ ์™ธ๋ถ€์—์„œ 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๋Š” ํ•จ์ˆ˜๋ฅผ ์ฆ‰์‹œ ์‹คํ–‰ํ•˜์ง€๋งŒ, ํ•จ์ˆ˜๊ฐ€ ์‹คํ–‰๋˜๋Š” ๋™์•ˆ ์˜ˆ์•ฝ๋œ ๋ชจ๋“  ์ƒํƒœ ์—…๋ฐ์ดํŠธ๋Š” Transition ์œผ๋กœ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค. ์•„๋ž˜์™€ ๊ฐ™์ด ์ž‘๋™ํ•œ๋‹ค๊ณ  ์ƒ์ƒํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค.

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

let isInsideTransition = false;

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

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

Transitions์—์„œ ์ƒํƒœ ์—…๋ฐ์ดํŠธ๊ฐ€ ์ˆœ์„œ๋Œ€๋กœ ์ด๋ฃจ์–ด์ง€์ง€ ์•Š์•„์š”

startTransition ๋‚ด๋ถ€์—์„œ await๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์ƒํƒœ ์—…๋ฐ์ดํŠธ๊ฐ€ ์ˆœ์„œ๋Œ€๋กœ ๋ฐœ์ƒํ•˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ด ์˜ˆ์‹œ์—์„œ updateQuantity ํ•จ์ˆ˜๋Š” ์นดํŠธ์— ์žˆ๋Š” ํ’ˆ๋ชฉ์˜ ์ˆ˜๋Ÿ‰์„ ์—…๋ฐ์ดํŠธํ•˜๊ธฐ ์œ„ํ•ด ์„œ๋ฒ„์— ์š”์ฒญํ•˜๋Š” ์‹œ๋ฎฌ๋ ˆ์ด์…˜์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. ๋˜ํ•œ ๋„คํŠธ์›Œํฌ ์š”์ฒญ์—์„œ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒฝ์Ÿ ์ƒํƒœ(race condition)๋ฅผ ์žฌํ˜„ํ•˜๋„๋ก ๋‹ค๋ฅธ ์š”์ฒญ๋“ค์ด ์ด์ „ ์š”์ฒญ๋ณด๋‹ค ๋Šฆ๊ฒŒ ์™„๋ฃŒ๋˜๋„๋ก ์ธ์œ„์ ์œผ๋กœ ์‘๋‹ต ์ˆœ์„œ๋ฅผ ์กฐ์ •ํ•ฉ๋‹ˆ๋‹ค.

์ˆ˜๋Ÿ‰์„ ํ•œ ๋ฒˆ ์—…๋ฐ์ดํŠธํ•œ ํ›„, ๋น ๋ฅด๊ฒŒ ์—ฌ๋Ÿฌ ๋ฒˆ ์—…๋ฐ์ดํŠธ๋ฅผ ์‹œ๋„ํ•ด ๋ณด์„ธ์š”. ๊ทธ๋Ÿฌ๋ฉด ์ž˜๋ชป๋œ ์ดํ•ฉ์ด ํ‘œ์‹œ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค

import { useState, useTransition } from "react";
import { updateQuantity } from "./api";
import Item from "./Item";
import Total from "./Total";

export default function App({}) {
  const [quantity, setQuantity] = useState(1);
  const [isPending, startTransition] = useTransition();
  // ์‹ค์ œ ์ˆ˜๋Ÿ‰์„ ๋ณ„๋„์˜ state์— ์ €์žฅํ•˜์—ฌ ๋ถˆ์ผ์น˜๋ฅผ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค.
  const [clientQuantity, setClientQuantity] = useState(1);

  const updateQuantityAction = newQuantity => {
    setClientQuantity(newQuantity);

    // ํŠธ๋žœ์ง€์…˜์˜ ๋Œ€๊ธฐ ์ƒํƒœ์— ์ ‘๊ทผํ•˜๊ธฐ ์œ„ํ•ด startTransition์„ ๋‹ค์‹œ ๊ฐ์Œ‰๋‹ˆ๋‹ค.
    startTransition(async () => {
      const savedQuantity = await updateQuantity(newQuantity);
      startTransition(() => {
        setQuantity(savedQuantity);
      });
    });
  };

  return (
    <div>
      <h1>Checkout</h1>
      <Item action={updateQuantityAction}/>
      <hr />
      <Total clientQuantity={clientQuantity} savedQuantity={quantity} isPending={isPending} />
    </div>
  );
}

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

์ด๊ฒƒ์€ ์˜ˆ์ƒ๋œ ๋™์ž‘์ž…๋‹ˆ๋‹ค. Transition ๋‚ด์—์„œ์˜ ์•ก์…˜์€ ์‹คํ–‰ ์ˆœ์„œ๋ฅผ ๋ณด์žฅํ•˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. ์ผ๋ฐ˜์ ์ธ ์‚ฌ์šฉ ์‚ฌ๋ก€์—์„œ๋Š” React๊ฐ€ useActionState๋‚˜ <form> actions๊ณผ ๊ฐ™์€ ๋” ๋†’์€ ์ˆ˜์ค€์˜ ์ถ”์ƒํ™”๋ฅผ ์ œ๊ณตํ•˜์—ฌ ์ˆœ์„œ๋ฅผ ์ฒ˜๋ฆฌํ•ด ์ค๋‹ˆ๋‹ค. ๊ณ ๊ธ‰ ์‚ฌ์šฉ ์‚ฌ๋ก€์—์„œ๋Š” ์ด ๋ฌธ์ œ๋ฅผ ์ฒ˜๋ฆฌํ•˜๊ธฐ ์œ„ํ•ด ์ž์ฒด์ ์ธ ํ์ž‰(queuing) ๋ฐ ์ทจ์†Œ ๋กœ์ง์„ ๊ตฌํ˜„ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

Example of useActionState handling execution order:

import { useState, useActionState } from "react";
import { updateQuantity } from "./api";
import Item from "./Item";
import Total from "./Total";

export default function App({}) {
  // Store the actual quantity in separate state to show the mismatch.
  const [clientQuantity, setClientQuantity] = useState(1);
  const [quantity, updateQuantityAction, isPending] = useActionState(
    async (prevState, payload) => {
      setClientQuantity(payload);
      const savedQuantity = await updateQuantity(payload);
      return savedQuantity; // Return the new quantity to update the state
    },
    1 // Initial quantity
  );

  return (
    <div>
      <h1>Checkout</h1>
      <Item action={updateQuantityAction}/>
      <hr />
      <Total clientQuantity={clientQuantity} savedQuantity={quantity} isPending={isPending} />
    </div>
  );
}