useTransition
useTransition์ UI์ ์ผ๋ถ๋ฅผ ๋ฐฑ๊ทธ๋ผ์ด๋์์ ๋ ๋๋ง ํ ์ ์๋๋ก ํด์ฃผ๋ React Hook์
๋๋ค.
const [isPending, startTransition] = useTransition()- ๋ ํผ๋ฐ์ค
- ์ฌ์ฉ๋ฒ
- Troubleshooting
- Transition์์ ์ ๋ ฅ ์ ๋ฐ์ดํธ๊ฐ ์๋ํ์ง ์์ต๋๋ค
- React๊ฐ state ์ ๋ฐ์ดํธ๋ฅผ transition์ผ๋ก ์ฒ๋ฆฌํ์ง ์์ต๋๋ค
- React๋
await์ดํ์ ์ํ ์ ๋ฐ์ดํธ๋ฅผ Transition์ผ๋ก ์ฒ๋ฆฌํ์ง ์์ต๋๋ค. - ์ปดํฌ๋ํธ ์ธ๋ถ์์
useTransition์ ํธ์ถํ๊ณ ์ถ์ต๋๋ค startTransition์ ์ ๋ฌํ ํจ์๋ ์ฆ์ ์คํ๋ฉ๋๋ค- Transitions์์ ์ํ ์ ๋ฐ์ดํธ๊ฐ ์์๋๋ก ์ด๋ฃจ์ด์ง์ง ์์์
๋ ํผ๋ฐ์ค
useTransition()
์ปดํฌ๋ํธ์ ์ต์์ ์์ค์์ useTransition์ ํธ์ถํ์ฌ ์ผ๋ถ state ์
๋ฐ์ดํธ๋ฅผ Transition ์ผ๋ก ํ์ํฉ๋๋ค.
import { useTransition } from 'react';
function TabContainer() {
const [isPending, startTransition] = useTransition();
// ...
}์๋์์ ๋ ๋ง์ ์์๋ฅผ ํ์ธํ์ธ์.
๋งค๊ฐ๋ณ์
useTransition์ ์ด๋ค ๋งค๊ฐ๋ณ์๋ ๋ฐ์ง ์์ต๋๋ค.
๋ฐํ๊ฐ
useTransition์ ์ ํํ ๋ ๊ฐ์ ํญ๋ชฉ์ด ์๋ ๋ฐฐ์ด์ ๋ฐํํฉ๋๋ค.
isPendingํ๋๊ทธ๋ ๋๊ธฐ ์ค์ธ Transition์ด ์๋์ง ์๋ ค์ค๋๋ค.startTransitionํจ์๋ ์ ๋ฐ์ดํธ๋ฅผ Transition์ผ๋ก ํ์ํ ์ ์๊ฒ ํด์ฃผ๋ ํจ์์ ๋๋ค.
startTransition(action)
useTransition์ด ๋ฐํํ๋ startTransition ํจ์๋ฅผ ์ฌ์ฉํ๋ฉด ์
๋ฐ์ดํธ๋ฅผ Transition์ผ๋ก ํ์ํ ์ ์์ต๋๋ค.
function TabContainer() {
const [isPending, startTransition] = useTransition();
const [tab, setTab] = useState('about');
function selectTab(nextTab) {
startTransition(() => {
setTab(nextTab);
});
}
// ...
}๋งค๊ฐ๋ณ์
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์ ์ ํํ ๋ ๊ฐ์ ํญ๋ชฉ์ด ์๋ ๋ฐฐ์ด์ ๋ฐํํฉ๋๋ค.
isPendingํ๋๊ทธ๋ ๋๊ธฐ ์ค์ธ Transition ์ด ์๋์ง ์๋ ค์ค๋๋ค.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์ ์ฌ์ฉํ์ฌ ์ฆ๊ฐ์ ์ธ ํผ๋๋ฐฑ์ ์ ๊ณตํ ์ ์์ต๋๋ค.
์์ 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> ); }
๋๊ธฐ ์ํ๋ฅผ ์๊ฐ์ ์ผ๋ก ํํํ๊ธฐ
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 ์ ์ฌ์ฉํ๋ ๋ฐฉ๋ฒ์ ๋ํด ์์ธํ ์์๋ณด์ธ์.
Suspense-enabled ๋ผ์ฐํฐ ๊ตฌ์ถ
React ํ๋ ์์ํฌ๋ ๋ผ์ฐํฐ๋ฅผ ๊ตฌ์ถํ๋ ๊ฒฝ์ฐ ํ์ด์ง ํ์์ Transition ์ผ๋ก ํ์ํ๋ ๊ฒ์ด ์ข์ต๋๋ค.
function Router() {
const [page, setPage] = useState('/');
const [isPending, startTransition] = useTransition();
function navigate(url) {
startTransition(() => {
setPage(url);
});
}
// ...์ธ ๊ฐ์ง ์ด์ ๋ก ์ด ๋ฐฉ๋ฒ์ ๊ถ์ฅํฉ๋๋ค.
- Transition์ ์ค๋จํ ์ ์์ผ๋ฏ๋ก ์ฌ์ฉ์๋ ๋ฆฌ๋ ๋๋ง์ด ์๋ฃ๋ ๋๊น์ง ๊ธฐ๋ค๋ฆด ํ์ ์์ด ๋ฐ๋ก ํด๋ฆญํ ์ ์์ต๋๋ค.
- Transition์ ์์น ์๋ ๋ก๋ฉ ํ์๊ธฐ๋ฅผ ๋ฐฉ์งํ๋ฏ๋ก ์ฌ์ฉ์๊ฐ ํ์ ์ ๊ฐ์์ค๋ฌ์ด ์ด๋์ ๋ฐฉ์งํ ์ ์์ต๋๋ค.
- Transition์ ๋ชจ๋ ๋ณด๋ฅ ์ค์ธ ์์ ์ ๋๊ธฐํ๋ฏ๋ก ์ฌ์ฉ์๋ ์ฌ์ด๋ ์ดํํธ๊ฐ ์๋ฃ๋ ํ์ ์๋ก์ด ํ์ด์ง๋ฅผ ๋ณผ ์ ์์ต๋๋ค.
๋ค์์ 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>; }
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 ์ ์คํํ๋ ค๋ฉด ๋ ๊ฐ์ง ์ต์ ์ด ์์ต๋๋ค.
- ๋ ๊ฐ์ ๊ฐ๋ณ state ๋ณ์๋ฅผ ์ ์ธํ ์ ์์ต๋๋ค. ํ๋๋ ์ ๋ ฅ state(ํญ์ ๋๊ธฐ์ ์ผ๋ก ์ ๋ฐ์ดํธ๋จ) ์ฉ์ด๊ณ ๋ค๋ฅธ ํ๋๋ Transition ์ ์ ๋ฐ์ดํธํ state์ ๋๋ค. ์ด๋ฅผ ํตํด ๋๊ธฐ state๋ฅผ ์ฌ์ฉํ์ฌ ์ ๋ ฅ์ ์ ์ดํ๊ณ (์ ๋ ฅ๋ณด๋ค โ์ง์ฐโ๋๋) Transition state ๋ณ์๋ฅผ ๋๋จธ์ง ๋ ๋๋ง ๋ก์ง์ ์ ๋ฌํ ์ ์์ต๋๋ค.
- ๋๋ 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> ); }