๋ณธ๋ฌธ์œผ๋กœ ๊ฑด๋„ˆ๋›ฐ๊ธฐ

๐Ÿ‘‰ React 18 ๋ณ€๊ฒฝ์ 

๐ŸŽˆ useIdโ€‹

useId๋Š” ํด๋ผ์ด์–ธํŠธ์™€ ์„œ๋ฒ„๊ฐ„์˜ hydration์˜ mismatch๋ฅผ ํ”ผํ•˜๋ฉด์„œ ์œ ๋‹ˆํฌํ•œ ์•„์ด๋””๋ฅผ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋Š” ์ƒˆ๋กœ์šด hook์ž…๋‹ˆ๋‹ค. ์ด๋Š” ์ฃผ๋กœ ๊ณ ์œ ํ•œ id๊ฐ€ ํ•„์š”ํ•œ ์ ‘๊ทผ์„ฑ API์™€ ์‚ฌ์šฉ๋˜๋Š” ์ปดํฌ๋„ŒํŠธ์— ์œ ์šฉํ•  ๊ฒƒ์œผ๋กœ ๊ธฐ๋Œ€๋ฉ๋‹ˆ๋‹ค.

์•„์ด๋””๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ ํŠธ๋ฆฌ ๋‚ด๋ถ€์˜ ๋…ธ๋“œ์˜ ์œ„์น˜๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” base 32 ๋ฌธ์ž์—ด์ž…๋‹ˆ๋‹ค. ํŠธ๋ฆฌ๊ฐ€ ์—ฌ๋Ÿฌ children์œผ๋กœ ๋ถ„๊ธฐ๋  ๋•Œ๋งˆ๋‹ค, ํ˜„์žฌ ๋ ˆ๋ฒจ์—์„œ ์ž์‹ ์ˆ˜์ค€์„ ๋‚˜ํƒ€๋‚ด๋Š” ๋น„ํŠธ๋ฅผ ์‹œํ€ธ์Šค ์™ผ์ชฝ์— ์ถ”๊ฐ€ํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค,.

useId๋Š” ๋ชฉ๋ก์—์„œ ํ‚ค๋ฅผ ์ƒ์„ฑํ•˜๊ธฐ ์œ„ํ•œ ๊ฒƒ์ด ์•„๋‹™๋‹ˆ๋‹ค. ํ‚ค๋Š” ๋ฐ์ดํ„ฐ์—์„œ ์ƒ์„ฑ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๐ŸŽˆ useTransitionโ€‹

์ด ๋‘ ๋ฉ”์†Œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์ผ๋ถ€ ์ƒํƒœ ์—…๋ฐ์ดํŠธ๋ฅผ ๊ธด๊ธ‰ํ•˜์ง€ ์•Š์€ ๊ฒƒ(not urgent)๋กœ ํ‘œ์‹œํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๊ฒƒ์œผ๋กœ ํ‘œ์‹œ๋˜์ง€ ์•Š์€ ์ƒํƒœ ์—…๋ฐ์ดํŠธ๋Š” ๊ธด๊ธ‰ํ•œ ๊ฒƒ์œผ๋กœ ๊ฐ„์ฃผ๋ฉ๋‹ˆ๋‹ค. ๊ธด๊ธ‰ํ•œ ์ƒํƒœ ์—…๋ฐ์ดํŠธ๊ฐ€ ๊ธด๊ธ‰ํ•˜์ง€ ์•Š์€ ์ƒํƒœ ์—…๋ฐ์ดํŠธ์„ ์ค‘๋‹จํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ƒํƒœ ์—…๋ฐ์ดํŠธ๋ฅผ ๊ธด๊ธ‰ํ•œ ๊ฒƒ๊ณผ ๊ธด๊ธ‰ํ•˜์ง€ ์•Š์€ ๊ฒƒ์œผ๋กœ ๋‚˜๋ˆ„์–ด ๊ฐœ๋ฐœ์ž์—๊ฒŒ ๋ Œ๋”๋ง ์„ฑ๋Šฅ์„ ํŠœ๋‹ํ•˜๋Š”๋ฐ ๋งŽ์€ ์ž์œ ๋ฅผ ์ฃผ์—ˆ๋‹ค๊ณ  ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

function App() {
const [resource, setResource] = useState(initialResource)
const [isPending, startTransition] = useTransition({ timeoutMs: 3000 })
return (
<>
<button
disabled={isPending}
onClick={() => {
startTransition(() => {
const nextUserId = getNextId(resource.userId)
setResource(fetchProfileData(nextUserId))
})
}}
>
Next
</button>
{isPending ? 'Loading...' : null}
<ProfilePage resource={resource} />
</>
)
}
  • startTransition๋Š” ํ•จ์ˆ˜๋กœ, ๋ฆฌ์•กํŠธ์— ์–ด๋–ค ์ƒํƒœ๋ณ€ํ™”๋ฅผ ์ง€์—ฐ์‹œํ‚ค๊ณ  ์‹ถ์€์ง€ ์ง€์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • isPending์€ ์ง„ํ–‰ ์—ฌ๋ถ€๋กœ, ํŠธ๋žœ์ง€์…˜์ด ์ง„ํ–‰์ค‘์ธ์ง€ ์•Œ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • timeoutMs ํ”„๋กœํผํ‹ฐ๋Š” ํŠธ๋žœ์ง€์…˜์ด ์™„๋ฃŒ๋  ๋•Œ๊นŒ์ง€ ์–ผ๋งˆ๋‚˜ ์˜ค๋žซ๋™์•ˆ ๊ธฐ๋‹ค๋ฆด ๊ฒƒ์ธ์ง€ ๊ฒฐ์ •ํ•ฉ๋‹ˆ๋‹ค. {timeoutMs: 3000} ๋ฅผ ์ „๋‹ฌํ•œ๋‹ค๋ฉด โ€œ๋‹ค์Œ ํ”„๋กœํ•„์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ๋ฐ 3์ดˆ๋ณด๋‹ค ์˜ค๋ž˜ ๊ฑธ๋ฆฐ๋‹ค๋ฉด ๋กœ๋”ฉ ์ƒํƒœ๋ฅผ ๋ณด์—ฌ์ฃผ๊ณ  ๊ทธ์ „๊นŒ์ง„ ๊ณ„์† ์ด์ „ ํ™”๋ฉด์„ ๋ณด์—ฌ์ค˜๋„ ๊ดœ์ฐฎ์•„โ€๋ผ๋Š” ์˜๋ฏธ์ž…๋‹ˆ๋‹ค.

useTransition ๊ฐ™์€ API๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์›ํ•˜๋Š” ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์— ์ดˆ์ ์„ ๋งž์ถœ ์ˆ˜ ์žˆ๊ณ  ์–ด๋–ป๊ฒŒ ๊ตฌํ˜„ํ–ˆ๋Š”์ง€ ์ƒ๊ฐํ•˜์ง€ ์•Š์•„๋„ ๋ฉ๋‹ˆ๋‹ค.

const initialResource = fetchUserAndPosts();

function ProfilePage() {
const [resource, setResource] = useState(initialResource);

function handleRefreshClick() {
setResource(fetchUserAndPosts());
}

return (
<Suspense fallback={<h1>Loading profile...</h1>}>
<ProfileDetails resource={resource} />
<button onClick={handleRefreshClick}>
Refresh
</button>
<Suspense fallback={<h1>Loading posts...</h1>}>
<ProfileTimeline resource={resource} />
</Suspense>
</Suspense>
);
}

์ด ์˜ˆ์‹œ์—์„  ํŽ˜์ด์ง€๊ฐ€ ๋กœ๋“œ๋˜๊ฑฐ๋‚˜ โ€œRefreshโ€ ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅผ ๋•Œ ๋งˆ๋‹ค ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค.
fetchUserAndPosts()์˜ ๋ฐ˜ํ™˜๊ฐ’์„ ์ƒํƒœ์— ์ €์žฅํ•˜์—ฌ ํ•˜์œ„ ์ปดํฌ๋„ŒํŠธ๋“ค์ด ์š”์ฒญ์—์„œ ๊ฐ€์ ธ์˜จ ๋ฐ์ดํ„ฐ๋ฅผ ์ฝ์„ ์ˆ˜ ์žˆ๊ฒŒ ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.
<ProfileDetails> ๋ฐ <ProfileTimeline> ์ปดํฌ๋„ŒํŠธ๋Š” ์ƒˆ๋กœ์šด ๋ฐ์ดํ„ฐ๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ์ƒˆ๋กœ์šด ๋ฆฌ์†Œ์Šค prop์„ ์ˆ˜์‹ ํ•˜๊ณ  ์•„์ง ์‘๋‹ต์ด ์—†๊ธฐ ๋•Œ๋ฌธ์— "suspend"๋˜๊ณ  fallback์ด ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค.
ํ•˜์ง€๋งŒ ์œ„ ๊ฒฝํ—˜์€ ์ž์—ฐ์Šค๋Ÿฝ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์šฐ๋ฆฌ๋Š” ํ•œ ํŽ˜์ด์ง€๋ฅผ ๋ธŒ๋ผ์šฐ์ง•ํ•˜๊ณ  ์žˆ์—ˆ๋Š”๋ฐ ๋ฒ„ํŠผ์„ ํด๋ฆญํ•œ ์งํ›„์— ๋ฐ”๋กœ ๋กœ๋”ฉ ์ƒํƒœ๋กœ ์ „ํ™˜๋˜์–ด ์‚ฌ์šฉ์ž๋ฅผ ํ˜ผ๋ž€์Šค๋Ÿฝ๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค. ์ด์ „์ฒ˜๋Ÿผ, ์˜๋„์น˜ ์•Š์€ ๋กœ๋”ฉ ์ƒํƒœ๋ฅผ ์ˆจ๊ธฐ๊ธฐ ์œ„ํ•ด์„œ ์ƒํƒœ ๊ฐฑ์‹ ์„ ํŠธ๋žœ์ง€์…˜์— ๋ž˜ํ•‘ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

function ProfilePage() {
const [isPending, startTransition] = useTransition({
// Wait 10 seconds before fallback
timeoutMs: 10000
});
const [resource, setResource] = useState(initialResource);

function handleRefreshClick() {
startTransition(() => {
setResource(fetchProfileData());
});
}

return (
<Suspense fallback={<h1>Loading profile...</h1>}>
<ProfileDetails resource={resource} />
<button
onClick={handleRefreshClick}
disabled={isPending}
>
{isPending ? "Refreshing..." : "Refresh"}
</button>
<Suspense fallback={<h1>Loading posts...</h1>}>
<ProfileTimeline resource={resource} />
</Suspense>
</Suspense>
);
}

โ€œRefreshโ€ ๋ฒ„ํŠผ์„ ํด๋ฆญํ•ด๋„ ์šฐ๋ฆฌ๊ฐ€ ๋ธŒ๋ผ์šฐ์ง•ํ•˜๊ณ  ์žˆ๋Š” ํŽ˜์ด์ง€๊ฐ€ ์‚ฌ๋ผ์ง€์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์šฐ๋ฆฌ๋Š” ์ธ๋ผ์ธ์œผ๋กœ ๋ญ”๊ฐ€ ๋กœ๋”ฉ๋˜๊ณ  ์žˆ๋‹ค๋Š” ๊ฒƒ์„ ๋ณด๊ณ  ๋ฐ์ดํ„ฐ๊ฐ€ ์ค€๋น„๋œ ์ดํ›„์— ์ƒˆ๋กœ์šด ๋ฐ์ดํ„ฐ๊ฐ€ ๋ณด์ž…๋‹ˆ๋‹ค.

์ด์ œ useTransition์˜ ํ•„์š”์„ฑ์ด ๋งค์šฐ ์ผ๋ฐ˜์ ์ด๋ผ๋Š” ๊ฑธ ์•Œ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž๊ฐ€ ์ƒํ˜ธ์ž‘์šฉํ•˜๋Š” ๋Œ€์ƒ์„ ์‹ค์ˆ˜๋กœ ์ˆจ๊ธฐ์ง€ ์•Š๋„๋ก ์ปดํฌ๋„ŒํŠธ๋ฅผ ์„œ์ŠคํŽœ๋“œ ์ƒํƒœ๋กœ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋Š” ๋Œ€๋ถ€๋ถ„ ๋ฒ„ํŠผ ํด๋ฆญ์ด๋‚˜ ์ƒํ˜ธ์ž‘์šฉ์€ useTransition์œผ๋กœ ๋ž˜ํ•‘ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

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

function Button({ children, onClick }) {
const [isPending, startTransition] = useTransition({
timeoutMs: 10000
});

function handleClick() {
startTransition(() => {
onClick();
});
}

const spinner = (
<span className="DelayedSpinner">
{/* ... */}
</span>
);

return (
<>
<button
onClick={handleClick}
disabled={isPending}
>
{children}
</button>
{isPending ? spinner : null}
</>
);
}

๋ช…์‹ฌํ•˜์„ธ์š”. ๋ฒ„ํŠผ์€ ์–ด๋–ค ์ƒํƒœ๋ฅผ ๊ฐฑ์‹ ํ•˜๋˜์ง€ ๊ด€์—ฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ด๊ฒƒ์€ onClick ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ์—์„œ ๋ฐœ์ƒํ•˜๋Š” ๋ชจ๋“  ์ƒํƒœ ๊ฐฑ์‹ ์„ transition์— ํฌํ•จํ•ฉ๋‹ˆ๋‹ค. ์ด์ œ <Button>์ด ํŠธ๋žœ์ง€์…˜ ์„ค์ •์„ ๋Œ€์‹ ํ•ด ์ฃผ๊ธฐ ๋•Œ๋ฌธ์— <ProfilePage> ์ปดํฌ๋„ŒํŠธ์— ํŠธ๋žœ์ง€์…˜ ์„ค์ •์„ ํ•ด์ค„ ํ•„์š”๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.

function ProfilePage() {
const [resource, setResource] = useState(initialResource);

function handleRefreshClick() {
setResource(fetchProfileData());
}

return (
<Suspense fallback={<h1>Loading profile...</h1>}>
<ProfileDetails resource={resource} />
<Button onClick={handleRefreshClick}>
Refresh
</Button>
<Suspense fallback={<h1>Loading posts...</h1>}>
<ProfileTimeline resource={resource} />
</Suspense>
</Suspense>
);
}

๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜๋ฉด ํŠธ๋žœ์ง€์…˜์ด ์‹œ์ž‘๋˜๊ณ  ๊ทธ ์•ˆ์— props.onClick() ์ด ํ˜ธ์ถœ๋˜์„œ <ProfilePage> ์ปดํฌ๋„ŒํŠธ์—์„œ handleRefreshClick ํ•จ์ˆ˜๊ฐ€ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค. ์ƒˆ๋กœ์šด ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๊ธฐ ์‹œ์ž‘ํ•˜์ง€๋งŒ ํŠธ๋žœ์ง€์…˜ ๋‚ด๋ถ€๋ผ์„œ ํด๋ฐฑ์ด ๋ณด์—ฌ์ง€์ง€ ์•Š์œผ๋ฉฐ useTransition ํ˜ธ์ถœ์— ์ง€์ •๋œ 10์ดˆ๊ฐ€ ์ง€๋‚˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ํŠธ๋žœ์ง€์…˜์ด ๋ณด๋ฅ˜์ค‘์ธ ๋™์•ˆ ๋ฒ„ํŠผ์— ์ธ๋ผ์ธ์œผ๋กœ ๋กœ๋”ฉ ์ธ๋””์ผ€์ดํ„ฐ๋ฅผ ๋ด…๋‹ˆ๋‹ค.

์ด์ œ ์ปจ์ปค๋ŸฐํŠธ ๋ชจ๋“œ๊ฐ€ ์ปดํฌ๋„ŒํŠธ์˜ ๊ฒฉ๋ฆฌ ์ˆ˜์ค€ ๋ฐ ๋ชจ๋“ˆ์„ฑ์„ ํฌ์ƒํ•˜์ง€ ์•Š๊ณ ๋„ ์šฐ์ˆ˜ํ•œ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ๋งŒ๋“œ๋Š”์ง€ ๋ฐฐ์› ์Šต๋‹ˆ๋‹ค. React๋Š” ํŠธ๋žœ์ง€์…˜์„ ์กฐ์ •ํ•ฉ๋‹ˆ๋‹ค.

๐ŸŽˆ useDeferredValueโ€‹

useDeferredValue๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด, ํŠธ๋ฆฌ์—์„œ ๊ธ‰ํ•˜์ง€ ์•Š์€ ๋ถ€๋ถ„์˜ ์žฌ๋žœ๋”๋ง์„ ์ง€์—ฐํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋Š” debounce์™€ ๋น„์Šทํ•˜์ง€๋งŒ, ๋ช‡๊ฐ€์ง€ ๋” ์žฅ์ ์ด ์žˆ์Šต๋‹ˆ๋‹ค. ๊ณ ์ •๋œ ์ง€์—ฐ์‹œ๊ฐ„์ด ์—†์œผ๋ฏ€๋กœ, ๋ฆฌ์•กํŠธ๋Š” ์ฒซ๋ฒˆ์งธ ๋ Œ๋”๋ง์ด ๋ฐ˜์˜๋˜๋Š” ์ฆ‰์‹œ ์ง€์—ฐ ๋ Œ๋”๋ง์„ ์‹œ๋„ํ•ฉ๋‹ˆ๋‹ค. ์ด ์ง€์—ฐ๋œ ๋ Œ๋”๋ง์€ ์ธํ„ฐ๋ŸฝํŠธ๊ฐ€ ๊ฐ€๋Šฅํ•˜๋ฉฐ, ์‚ฌ์šฉ์ž ์ž…๋ ฅ์„ ์ฐจ๋‹จํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

const deferredValue = useDeferredValue(value);

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

์ด hook์€ ๋””๋ฐ”์šด์‹ฑ ๋˜๋Š” throttling์„ ์‚ฌ์šฉํ•˜์—ฌ ์—…๋ฐ์ดํŠธ๋ฅผ ์—ฐ๊ธฐํ•˜๋Š” user-space hooks์™€ ์œ ์‚ฌํ•ฉ๋‹ˆ๋‹ค.

useDeferredValue ์‚ฌ์šฉ์˜ ์ด์ ์€ React๊ฐ€ ๋‹ค๋ฅธ ์ž‘์—…์ด ์™„๋ฃŒ๋˜๋Š” ์ฆ‰์‹œ ์—…๋ฐ์ดํŠธ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•˜๊ณ (์ž„์˜์˜ ์‹œ๊ฐ„์„ ๊ธฐ๋‹ค๋ฆฌ๋Š” ๋Œ€์‹ ) startTransition๊ณผ ๊ฐ™์ด ์ง€์—ฐ๋œ ๊ฐ’์ด ๊ธฐ์กด ์ฝ˜ํ…์ธ ์— ๋Œ€ํ•œ ์˜ˆ๊ธฐ์น˜ ์•Š์€ ๋Œ€์ฒด๋ฅผ ํŠธ๋ฆฌ๊ฑฐํ•˜์ง€ ์•Š๊ณ  ์ผ์‹œ ์ค‘๋‹จ๋  ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

Memoizing deferred childrenโ€‹

useDeferredValue๋Š” ์ „๋‹ฌํ•œ ๊ฐ’๋งŒ ์—ฐ๊ธฐํ•ฉ๋‹ˆ๋‹ค. ๊ธด๊ธ‰(urgent) ์—…๋ฐ์ดํŠธ ์ค‘์— ์ž์‹ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋‹ค์‹œ ๋ Œ๋”๋ง๋˜๋Š” ๊ฒƒ์„ ๋ฐฉ์ง€ํ•˜๋ ค๋ฉด ํ•ด๋‹น ์ปดํฌ๋„ŒํŠธ๋„ React.memo ๋˜๋Š” React.useMemo๋กœ memoizeํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

function Typeahead() {
const query = useSearchQuery('');
const deferredQuery = useDeferredValue(query);

// Memoizing tells React to only re-render when deferredQuery changes,
// not when query changes.
const suggestions = useMemo(() =>
<SearchSuggestions query={deferredQuery} />,
[deferredQuery]
);

return (
<>
<SearchInput query={query} />
<Suspense fallback="Loading results...">
{suggestions}
</Suspense>
</>
);
}

์ž์‹์„ memoizeํ•˜๋ฉด React๋Š” query๊ฐ€ ๋ณ€๊ฒฝ๋  ๋•Œ๊ฐ€ ์•„๋‹ˆ๋ผ deferredQuery๊ฐ€ ๋ณ€๊ฒฝ๋  ๋•Œ๋งŒ ๋‹ค์‹œ ๋ Œ๋”๋งํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค.
์ด ์ฃผ์˜ ์‚ฌํ•ญ์€ useDeferredValue์—๋งŒ ์žˆ๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ฉฐ ๋””๋ฐ”์šด์‹ฑ ๋˜๋Š” throttling์„ ์‚ฌ์šฉํ•˜๋Š” ์œ ์‚ฌํ•œ hook์— ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ๊ณผ ๋™์ผํ•œ ํŒจํ„ด์ž…๋‹ˆ๋‹ค.

๐ŸŽˆ useSyncExternalStore(Library Hooks)โ€‹

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

const state = useSyncExternalStore(subscribe, getSnapshot[, getServerSnapshot]);

useSyncExternalStore์€ ์„ ํƒ์  hydration ๋ฐ ์‹œ๊ฐ„ ๋ถ„ํ• ๊ณผ ๊ฐ™์€ concurrent rendering ๊ธฐ๋Šฅ๊ณผ ํ˜ธํ™˜๋˜๋Š” ๋ฐฉ์‹์œผ๋กœ ์™ธ๋ถ€ ๋ฐ์ดํ„ฐ ์†Œ์Šค์—์„œ ์ฝ๊ณ  subscribingํ•˜๋Š” ๋ฐ ๊ถŒ์žฅ๋˜๋Š” hook์ž…๋‹ˆ๋‹ค.

์™ธ๋ถ€ ๋ฐ์ดํ„ฐ์— ๋Œ€ํ•œ ์›๋ณธ์— ๋Œ€ํ•œ subscription์„ ํ•„์š”๋กœ ํ•  ๋•Œ ๋” ์ด์ƒ useEffect๊ฐ€ ํ•„์š”ํ•˜์ง€ ์•Š๊ณ , ์ด๋Š” ๋ฆฌ์•กํŠธ ์™ธ๋ถ€ ์ƒํƒœ์™€ ํ†ตํ•ฉ๋˜๋Š” ๋ชจ๋“  ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์— ๊ถŒ์žฅ๋œ๋‹ค.

์ด ๋ฉ”์„œ๋“œ๋Š” store ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•˜๊ณ  ์„ธ ๊ฐ€์ง€ ์ธ์ˆ˜๋ฅผ ํ—ˆ์šฉํ•ฉ๋‹ˆ๋‹ค.

  • subscribe: ์Šคํ† ์–ด๊ฐ€ ๋ณ€๊ฒฝ๋  ๋•Œ๋งˆ๋‹ค ํ˜ธ์ถœ๋˜๋Š” ์ฝœ๋ฐฑ์„ ๋“ฑ๋กํ•˜๋Š” ํ•จ์ˆ˜์ž…๋‹ˆ๋‹ค.
  • getSnapshot: store์˜ ํ˜„์žฌ ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•˜๋Š” ํ•จ์ˆ˜์ž…๋‹ˆ๋‹ค.
  • getServerSnapshot: ์„œ๋ฒ„ ๋ Œ๋”๋ง ์ค‘์— ์‚ฌ์šฉ๋œ ์Šค๋ƒ…์ƒท์„ ๋ฐ˜ํ™˜ํ•˜๋Š” ํ•จ์ˆ˜์ž…๋‹ˆ๋‹ค.

๊ฐ€์žฅ ๊ธฐ๋ณธ์ ์ธ ์˜ˆ๋Š” ๋‹จ์ˆœํžˆ ์ „์ฒด store๋ฅผ subscriptionํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

const state = useSyncExternalStore(store.subscribe, store.getSnapshot);

๋‹ค์Œ๊ณผ ๊ฐ™์ด ํŠน์ • ํ•„๋“œ๋ฅผ ๊ตฌ๋…ํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.

const selectedField = useSyncExternalStore(
store.subscribe,
() => store.getSnapshot().selectedField,
);

์„œ๋ฒ„ ๋ Œ๋”๋ง ์‹œ ์„œ๋ฒ„์—์„œ ์‚ฌ์šฉํ•˜๋Š” ์Šคํ† ์–ด ๊ฐ’์„ ์ง๋ ฌํ™”ํ•˜์—ฌ useSyncExternalStore์— ์ œ๊ณตํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. React๋Š” ์„œ๋ฒ„ ๋ถˆ์ผ์น˜๋ฅผ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด hydration ์ค‘์— ์ด ์Šค๋ƒ…์ƒท์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

const selectedField = useSyncExternalStore(
store.subscribe,
() => store.getSnapshot().selectedField,
() => INITIAL_SERVER_SNAPSHOT.selectedField,
);
  • External Store: ์™ธ๋ถ€ ์Šคํ† ์–ด๋ผ๋Š” ๊ฒƒ์€ ์šฐ๋ฆฌ๊ฐ€ subscribeํ•˜๋Š” ๋ฌด์–ธ๊ฐ€๋ฅผ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด ๋ฆฌ๋•์Šค ์Šคํ† ์–ด, ๊ธ€๋กœ๋ฒŒ ๋ณ€์ˆ˜, dom ์ƒํƒœ ๋“ฑ์ด ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • Internal Store: props, context, useState, useReducer ๋“ฑ ๋ฆฌ์•กํŠธ๊ฐ€ ๊ด€๋ฆฌํ•˜๋Š” ์ƒํƒœ๋ฅผ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค.
  • Tearing: ์‹œ๊ฐ์ ์ธ ๋น„์ผ์น˜๋ฅผ ์˜๋ฏธํ•œ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, ํ•˜๋‚˜์˜ ์ƒํƒœ์— ๋Œ€ํ•ด UI๊ฐ€ ์—ฌ๋Ÿฌ ์ƒํƒœ๋กœ ๋ณด์—ฌ์ง€๊ณ  ์žˆ๋Š”, (= ๊ฐ ์ปดํฌ๋„ŒํŠธ ๋ณ„๋กœ ์—…๋ฐ์ดํŠธ ์†๋„๊ฐ€ ๋‹ฌ๋ผ์„œ ๋ฐœ์ƒํ•˜๋Š”) UI๊ฐ€ ์ฐข์–ด์ง„ ์ƒํƒœ๋ฅผ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค.

๋ฆฌ์•กํŠธ 18 ์ด์ „์—๋Š” ์ด๋Ÿฌํ•œ ๋ฌธ์ œ๊ฐ€ ์—†์—ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ๋ฆฌ์•กํŠธ 18๋ถ€ํ„ฐ ๋„์ž…๋œ concurrent ๋ Œ๋”๋ง์ด ๋“ฑ์žฅํ•˜๋ฉฐ์„œ ๋ Œ๋”๋ง์ด ๋ Œ๋”๋ง์„ ์ž ์‹œ ์ผ์‹œ์ค‘์ง€ํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋˜๋ฉด์„œ ์ด ๋ฌธ์ œ๊ฐ€ ๋Œ€๋‘๋˜๊ธฐ ์‹œ์ž‘ํ–ˆ๋‹ค. ์ผ์‹œ์ค‘์ง€๊ฐ€ ๋ฐœ์ƒํ•˜๋Š” ์‚ฌ์ด์— ์—…๋ฐ์ดํŠธ๋Š” ๋ Œ๋”๋ง์— ์‚ฌ์šฉ๋˜๋Š” ๋ฐ์ดํ„ฐ์™€ ์ด์™€ ๊ด€๋ จ๋œ ๋ณ€๊ฒฝ์‚ฌํ•ญ์„ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ๊ฒŒ ๋˜์—ˆ๋‹ค. ์ด๋กœ ์ธํ•ด UI๋Š” ๋™์ผํ•œ ๋ฐ์ดํ„ฐ์— ๋‹ค๋ฅธ ๊ฐ’์„ ํ‘œ์‹œํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋˜๋ฒ„๋ ธ๋‹ค.

์•„๋ž˜์™€ ๊ฐ™์ด ๊ธฐ์กด์˜ ๋™๊ธฐ ๋ Œ๋”๋ง ์‹œ์—๋Š” UI๋Š” ํ•ญ์ƒ ์ผ๊ด€์„ฑ์„ ์œ ์ง€ํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

synchronous-rendering

๊ทธ๋Ÿฌ๋‚˜ concurrent ๋ Œ๋”๋ง์—์„œ๋Š” ์ดˆ๊ธฐ์—๋Š” ์•„๋ž˜ ๊ทธ๋ฆผ์ฒ˜๋Ÿผ ํŒŒ๋ž€์ƒ‰์ž…๋‹ˆ๋‹ค. ๋ฆฌ์•กํŠธ๋Š” ์™ธ๋ถ€ ์Šคํ† ์–ด๊ฐ€ ๋ฐ”๋€Œ๋ฉด์„œ ๋นจ๊ฐ„์ƒ‰์œผ๋กœ ์—…๋ฐ์ดํŠธ ํ•ฉ๋‹ˆ๋‹ค. ๋ฆฌ์•กํŠธ๋Š” ๊ณ„์†ํ•ด์„œ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋นจ๊ฐ„์ƒ‰์œผ๋กœ ๋ฐ”๊พธ๋ ค๊ณ  ์‹œ๋„ํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์ด ๊ณผ์ •์—์„œ ๋ฐœ์ƒํ•˜๋Š” UI์˜ ๋ถˆ์ผ์น˜๋ฅผ tearing์ด๋ผ๊ณ  ํ•ฉ๋‹ˆ๋‹ค.

concurrent-rendering

import React, { useState, useEffect, useCallback } from 'react'

// library code

const createStore = (initialState) => {
let state = initialState
const getState = () => state
const listeners = new Set()
const setState = (fn) => {
state = fn(state)
listeners.forEach((l) => l())
}
const subscribe = (listener) => {
listeners.add(listener)
return () => listeners.delete(listener)
}
return { getState, setState, subscribe }
}

const useStore = (store, selector) => {
const [state, setState] = useState(() => selector(store.getState()))
useEffect(() => {
const callback = () => setState(selector(store.getState()))
const unsubscribe = store.subscribe(callback)
callback()
return unsubscribe
}, [store, selector])
return state
}

//Application code

const store = createStore({ count: 0, text: 'hello' })

const Counter = () => {
const count = useStore(
store,
useCallback((state) => state.count, []),
)
const inc = () => {
store.setState((prev) => ({ ...prev, count: prev.count + 1 }))
}
return (
<div>
{count} <button onClick={inc}>+1</button>
</div>
)
}

const TextBox = () => {
const text = useStore(
store,
useCallback((state) => state.text, []),
)
const setText = (event) => {
store.setState((prev) => ({ ...prev, text: event.target.value }))
}
return (
<div>
<input value={text} onChange={setText} className="full-width" />
</div>
)
}

const App = () => {
return (
<div className="container">
<Counter />
<Counter />
<TextBox />
<TextBox />
</div>
)
}

useState, useEffect๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๋Š” useStore hook์„ useSyncExternalStore๋กœ ๋ณ€๊ฒฝํ•ด๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

import { useSyncExternalStore } from 'react'

const useStore = (store, selector) => {
return useSyncExternalStore(
store.subscribe,
useCallback(() => selector(store.getState(), [store, selector])),
)
}

์ฝ”๋“œ๊ฐ€ ํ›จ์”ฌ ๊ฐ„๊ฒฐํ•ด์กŒ์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋ฉด ์–ด๋–ค ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋“ค์ด ์ด๋Ÿฌํ•œ concurrent rendering์— ์˜ํ–ฅ์„ ๋ฐ›์„๊นŒ?

๋ Œ๋”๋ง ์ค‘์— ์™ธ๋ถ€ ๊ฐ€๋ณ€ ๋ฐ์ดํ„ฐ์— ์ ‘๊ทผํ•˜์ง€ ์•Š๊ณ , react props, state, context ๋งŒ์„ ์‚ฌ์šฉํ•˜์—ฌ ์ •๋ณด๋ฅผ ์ „๋‹ฌํ•˜๋Š” ์ปดํฌ๋„ŒํŠธ์™€ ํ›…๋งŒ ๊ฐ€์ง€๊ณ  ์žˆ๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ผ๋ฉด ์˜ํ–ฅ์„ ๋ฐ›์ง€ ์•Š์„ ๊ฒƒ์ž…๋‹ˆ๋‹ค.
๋ฐ์ดํ„ฐ fetch, ์ƒํƒœ๊ด€๋ฆฌ, redux, mobx, relay ๋“ฑ์€ ์˜ํ–ฅ์„ ๋ฐ›์„ ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์ด๋Š” ๋ฆฌ์•กํŠธ ์™ธ๋ถ€์— ์ƒํƒœ๋ฅผ ์ €์žฅํ•˜๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. concurrent ๋ Œ๋”๋ง ์‹œ์—๋Š” react๊ฐ€ ๋ชจ๋ฅด๊ฒŒ ๋ Œ๋”๋ง ์ค‘์— ์ด๋Ÿฌํ•œ ๊ฐ’์ด ์—…๋ฐ์ดํŠธ ๋  ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.

์ฐธ๊ณ : https://github.com/reactwg/react-18/discussions/86
์ฐธ๊ณ : https://www.youtube.com/watch?v=oPfSC5bQPR8&t=694s&ab_channel=ReactConf2021 getSnapShot์€ ์บ์‹œ๋œ ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. getSnapshot์ด ์—ฐ์†์œผ๋กœ ์—ฌ๋Ÿฌ ๋ฒˆ ํ˜ธ์ถœ๋˜๋ฉด ๊ทธ ์‚ฌ์ด์— ์Šคํ† ์–ด ์—…๋ฐ์ดํŠธ๊ฐ€ ์—†๋Š” ํ•œ ์ •ํ™•ํžˆ ๋™์ผํ•œ ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๐ŸŽˆ useInsertionEffect(Library Hooks)โ€‹

useInsertionEffect(didUpdate);

signature๋Š” useEffect์™€ ๋™์ผํ•˜์ง€๋งŒ, ๋ชจ๋“  DOM ๋ณ€ํ˜• ์ „์— ๋™๊ธฐ์ ์œผ๋กœ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค. useLayoutEffect์—์„œ ๋ ˆ์ด์•„์›ƒ์„ ์ฝ๊ธฐ ์ „์— DOM์— ์Šคํƒ€์ผ์„ ์‚ฝ์ž…ํ•˜๋ ค๋ฉด ์ด๊ฒƒ์„ ์‚ฌ์šฉํ•˜์‹ญ์‹œ์˜ค. ์ด hook๋Š” ๋ฒ”์œ„๊ฐ€ ์ œํ•œ๋˜์–ด ์žˆ์œผ๋ฏ€๋กœ ์ด hook๋Š” refs์— ์ ‘๊ทผํ•  ์ˆ˜ ์—†์œผ๋ฉฐ ์—…๋ฐ์ดํŠธ๋ฅผ ์˜ˆ์•ฝํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.

useInsertionEffect๋Š” css-in-js ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์ž‘์„ฑ์ž๋กœ ์ œํ•œ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๋Œ€์‹  useEffect ๋˜๋Š” useLayoutEffect๋ฅผ ์‚ฌ์šฉํ•˜์„ธ์š”.

Breaking Changeโ€‹

๐ŸŽˆ Automatic batchingโ€‹

https://github.com/reactwg/react-18/discussions/21

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

React Batch ์—…๋ฐ์ดํŠธ ๋ฐฉ์‹์„ ๋ณ€๊ฒฝํ•˜์—ฌ ์ž๋™์œผ๋กœ ๋” ๋งŽ์€ ๋ฐฐ์น˜๋ฅผ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ๋„๋ก ์„ฑ๋Šฅ์ด ํ–ฅ์ƒ๋˜์—ˆ๋‹ค. ์—ฌ๊ธฐ์„œ batching์ด๋ž€ ์—ฌ๋Ÿฌ ์ƒํƒœ ์—…๋ฐ์ดํŠธ๋ฅผ ํ•˜๋‚˜์˜ ๋ฆฌ๋ Œ๋”๋ง์œผ๋กœ ์ฒ˜๋ฆฌํ•˜์—ฌ ์„ฑ๋Šฅ์„ ํ–ฅ์ƒ์‹œํ‚ค๋Š” ๋ฐฉ๋ฒ•์ž…๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, ๋ฒ„ํŠผ ํ•˜๋‚˜ ํด๋ฆญ์ด ๋‘๊ฐœ์˜ ์ƒํƒœ๋ฅผ ์—…๋ฐ์ดํŠธ (useState๊ฐ€ ๋‘๋ฒˆ ์ˆ˜ํ–‰) ํ•œ๋‹ค๋ฉด, ๋ฆฌ์•กํŠธ๋Š” ์ด๋ฅผ ํ•˜๋‚˜์˜ ๋ฆฌ๋ Œ๋”๋ง์œผ๋กœ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•ด์ฃผ๋Š” ๊ฒƒ์„ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค.

๊ทธ๋Ÿฌ๋‚˜ ๋ฆฌ์•กํŠธ๋Š” ์–ธ์ œ ์—…๋ฐ์ดํŠธ๋ฅผ ๋ฐฐ์น˜๋กœ ์ฒ˜๋ฆฌํ–ˆ๋Š”์ง€๊ฐ€ ์ผ๊ด€์„ฑ์žˆ๊ฒŒ ์ด๋ค„์ง€๊ณ  ์žˆ์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด ๋ฐ์ดํ„ฐ๋ฅผ fetch ํ•œ ๋‹ค์Œ, handleClick ์—์„œ ์ƒํƒœ๋ฅผ ์—…๋ฐ์ดํŠธ ํ•˜๋Š” ๊ฒฝ์šฐ, ๋ฆฌ์•กํŠธ๋Š” ์—…๋ฐ์ดํŠธ๋ฅผ ๋ฐฐ์น˜ํ•˜์ง€ ์•Š๊ณ  ๊ฐœ๋ณ„ ์—…๋ฐ์ดํŠธ ๋‘๊ฐœ๋ฅผ ์ˆ˜ํ–‰ํ•˜๊ณค ํ–ˆ์—ˆ์Šต๋‹ˆ๋‹ค. ๊ทธ ์ด์œ ๋Š” ๋ธŒ๋ผ์šฐ์ € ์ด๋ฒคํŠธ ์ค‘์—๋Š” ๋ฐฐ์น˜๋กœ ์ผ๊ด„ ์ฒ˜๋ฆฌ ํ•˜์ง€๋งŒ, ์ด๋ฒคํŠธ๊ฐ€ ์ด๋ฏธ ์ฒ˜๋ฆฌ๋œ ํ›„(์ฝœ๋ฐฑ)์—์„œ ์ƒํƒœ๋ฅผ ์—…๋ฐ์ดํŠธ ์ฒ˜๋ฆฌํ•˜๊ณ  ์žˆ์—ˆ๊ธฐ ๋•Œ๋ฌธ์ด์˜€์Šต๋‹ˆ๋‹ค.

๋ฆฌ์•กํŠธ 18์˜ automatic batching์€ createRoot๊ฐ€ ์žˆ๋Š” React 18๋ถ€ํ„ฐ ๋ชจ๋“  ์—…๋ฐ์ดํŠธ๋Š” ์ถœ์ฒ˜์— ๊ด€๊ณ„์—†์ด ์ž๋™์œผ๋กœ ์ผ๊ด„ ์ฒ˜๋ฆฌ๋ฉ๋‹ˆ๋‹ค. ์–ด๋””์—์„œ ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœ์ƒํ–ˆ๋Š”์ง€์™€ ์ƒ๊ด€์—†์ด ์ž๋™์œผ๋กœ ๋ชจ๋“  ์—…๋ฐ์ดํŠธ๊ฐ€ ๋ฐฐ์น˜๋˜์–ด ์ด๋ค„์ง‘๋‹ˆ๋‹ค.

์ฆ‰, timeouts, promises, native ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ ๋˜๋Š” ๊ธฐํƒ€ ์ด๋ฒคํŠธ ๋‚ด๋ถ€์˜ ์—…๋ฐ์ดํŠธ๋Š” React ์ด๋ฒคํŠธ ๋‚ด๋ถ€์˜ ์—…๋ฐ์ดํŠธ์™€ ๋™์ผํ•œ ๋ฐฉ์‹์œผ๋กœ ์ผ๊ด„ ์ฒ˜๋ฆฌ๋ฉ๋‹ˆ๋‹ค. ์ด๋กœ ์ธํ•ด ๋ Œ๋”๋ง ์ž‘์—…์ด ์ค„์–ด๋“ค๊ณ  ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ์„ฑ๋Šฅ์ด ํ–ฅ์ƒ๋  ๊ฒƒ์œผ๋กœ ๊ธฐ๋Œ€ํ•ฉ๋‹ˆ๋‹ค.

function App() {
const [count, setCount] = useState(0)
const [flag, setFlag] = useState(false)

function handleClick() {
fetchSomething().then(() => {
// React 18 and later DOES batch these:
setCount((c) => c + 1)
setFlag((f) => !f)
// React will only re-render once at the end (that's batching!)
})
}

return (
<div>
<button onClick={handleClick}>Next</button>
<h1 style={{ color: flag ? 'blue' : 'black' }}>{count}</h1>
</div>
)
}

๋งŒ์•ฝ ์ด๋Ÿฌํ•œ ๋™์ž‘์„ ์›์น˜ ์•Š๋Š”๋‹ค๋ฉด flushSync๋ฅผ ์“ฐ๋ฉด ๋ฉ๋‹ˆ๋‹ค.

import { flushSync } from 'react-dom'; // Note: react-dom, not react

function handleClick() {
flushSync(() => {
setCounter(c => c + 1);
});
// React has updated the DOM by now
flushSync(() => {
setFlag(f => !f);
});
// React has updated the DOM by now
}

๐ŸŽˆ New Suspense Featuresโ€‹

Suspense๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์•„์ง ํ‘œ์‹œํ•  ์ค€๋น„๊ฐ€ ๋˜์ง€ ์•Š์€ ๊ฒฝ์šฐ ์ปดํฌ๋„ŒํŠธ ํŠธ๋ฆฌ์˜ ์ผ๋ถ€์— ๋Œ€ํ•œ ๋กœ๋“œ ์ƒํƒœ๋ฅผ ์„ ์–ธ์ ์œผ๋กœ ์ง€์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

<Suspense fallback={<Spinner />}>
<Comments />
</Suspense>

Suspense๋Š” React ํ”„๋กœ๊ทธ๋ž˜๋ฐ ๋ชจ๋ธ์—์„œ "UI ๋กœ๋”ฉ ์ƒํƒœ"๋ฅผ ์ผ๊ธ‰ ์„ ์–ธ์  ๊ฐœ๋…(first-class declarative concept)์œผ๋กœ ๋งŒ๋“ญ๋‹ˆ๋‹ค.

์šฐ๋ฆฌ๋Š” ๋ช‡ ๋…„ ์ „์— ์ œํ•œ๋œ ๋ฒ„์ „์˜ Suspense๋ฅผ ๋„์ž…ํ–ˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ์ง€์›๋˜๋Š” ์œ ์ผํ•œ ์‚ฌ์šฉ ์‚ฌ๋ก€๋Š” React.lazy๋กœ ์ฝ”๋“œ ๋ถ„ํ• (splitting)์ด์—ˆ๊ณ  ์„œ๋ฒ„์—์„œ ๋ Œ๋”๋งํ•  ๋•Œ ์ „ํ˜€ ์ง€์›๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.

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

์ž์„ธํ•œ ๋‚ด์šฉ์€ React 18์˜ Suspense์— ๋Œ€ํ•œ RFC๋ฅผ ์ฐธ์กฐํ•˜์„ธ์š”.

https://reactjs.org/blog/2022/03/29/react-v18.html
https://yceffort.kr/2022/04/react-18-changelog
https://ko.reactjs.org/docs/concurrent-mode-patterns.html

๐ŸŽˆ New Strict Mode Behaviorsโ€‹

์•ž์œผ๋กœ React๊ฐ€ ์ƒํƒœ๋ฅผ ์œ ์ง€ํ•˜๋ฉด์„œ UI์˜ ์„น์…˜์„ ์ถ”๊ฐ€ ๋ฐ ์ œ๊ฑฐํ•  ์ˆ˜ ์žˆ๋Š” ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•˜๊ณ  ์‹ถ์Šต๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, ์‚ฌ์šฉ์ž๊ฐ€ ํ™”๋ฉด์—์„œ ๋ฉ€์–ด์กŒ๋‹ค๊ฐ€ ๋’ค๋กœ ํƒญํ•˜๋ฉด React๋Š” ์ฆ‰์‹œ ์ด์ „ ํ™”๋ฉด์„ ํ‘œ์‹œํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฅผ ์œ„ํ•ด React๋Š” ์ด์ „๊ณผ ๋™์ผํ•œ component ์ƒํƒœ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํŠธ๋ฆฌ๋ฅผ ๋งˆ์šดํŠธ ํ•ด์ œํ•˜๊ณ  ๋‹ค์‹œ ๋งˆ์šดํŠธํ•ฉ๋‹ˆ๋‹ค.

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

์ด๋Ÿฌํ•œ ๋ฌธ์ œ๋ฅผ ํ‘œ์‹œํ•˜๋Š” ๋ฐ ๋„์›€์ด ๋˜๋„๋ก React 18์€ Strict Mode์— ๋Œ€ํ•œ ์ƒˆ๋กœ์šด ๊ฐœ๋ฐœ ์ „์šฉ ๊ฒ€์‚ฌ๋ฅผ ๋„์ž…ํ–ˆ์Šต๋‹ˆ๋‹ค. ์ด ์ƒˆ๋กœ์šด ๊ฒ€์‚ฌ๋Š” component๊ฐ€ ์ฒ˜์Œ์œผ๋กœ ๋งˆ์šดํŠธ๋  ๋•Œ๋งˆ๋‹ค ๋ชจ๋“  component๋ฅผ ์ž๋™์œผ๋กœ ๋งˆ์šดํŠธ ํ•ด์ œํ–ˆ๋‹ค๊ฐ€ ๋‹ค์‹œ ๋งˆ์šดํŠธํ•˜์—ฌ ๋‘ ๋ฒˆ์งธ ๋งˆ์šดํŠธ์—์„œ ์ด์ „ ์ƒํƒœ๋ฅผ ๋ณต์›ํ•ฉ๋‹ˆ๋‹ค.

https://reactjs.org/docs/strict-mode.html#ensuring-reusable-state

๐ŸŽˆ ์ผ๊ด€๋œ useEffect ํƒ€์ด๋ฐโ€‹

์œ„์—์„œ ์–ธ๊ธ‰ํ•œ Automatic Batching์—์„œ ์ด์–ด์ง€๋Š” ๋งฅ๋ฝ์ž…๋‹ˆ๋‹ค. click, keydown event์™€ ๊ฐ™์€ ๊ฐœ๋ณ„ ์‚ฌ์šฉ์ž ์ž…๋ ฅ ์—๋ฒคํŠธ ์ค‘์— ์—…๋ฐ์ดํŠธ๊ฐ€ ๋ฐœ์ƒํ•œ ๊ฒฝ์šฐ, ํ•ญ์ƒ ๋™๊ธฐ์‹์œผ๋กœ effect ํ•จ์ˆ˜๋ฅผ ํ”Œ๋Ÿฌ์‰ฌํ•ฉ๋‹ˆ๋‹ค. ์ด์ „์—๋Š” ์ด ๊ธฐ๋Šฅ์ด ์˜ˆ์ธก๊ฐ€๋Šฅํ•˜๊ฑฐ๋‚˜, ์ผ๊ด€์ ์ด์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค.

๐ŸŽˆ ์—„๊ฒฉํ•ด์ง„ hydration ์—๋Ÿฌโ€‹

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

๐ŸŽˆ Suspense ๊ฐ€ ์ด์ œ ํ•ญ์ƒ ์ผ๊ด€๋˜๊ฒŒ ์ ์šฉ๋จโ€‹

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

๐ŸŽˆ Suspense์™€ layout effectโ€‹

ํŠธ๋ฆฌ๊ฐ€ suspend๋˜์—ˆ๋‹ค๊ฐ€ fallback์œผ๋กœ ๋Œ์•„๊ฐ€๋ฉด, ๋ฆฌ์•กํŠธ ๋ ˆ์ด์•„์›ƒ effect๋ฅผ ์ •๋ฆฌํ•œ ๋‹ค์Œ, ๋ฐ”์šด๋”๋ฆฌ ๋‚ด๋ถ€์˜ ๋‚ด์šฉ์ด ๋‹ค์‹œ ํ‘œ์‹œ ๋  ๋•Œ๊นŒ์ง€ ๋งŒ๋“ญ๋‹ˆ๋‹ค. ์ด๋กœ ์ธํ•ด ์ปดํฌ๋„ŒํŠธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ suspense์™€ ํ•จ๊ป˜ ์‚ฌ์šฉ๋  ๋•Œ ๋ ˆ์ด์•„์›ƒ์„ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ธก์ •ํ•  ์ˆ˜ ์—†์—ˆ๋˜ ๋ฌธ์ œ๊ฐ€ ํ•ด๊ฒฐ๋ฉ๋‹ˆ๋‹ค.

๐ŸŽˆ ์ƒˆ๋กœ์šด js ํ™˜๊ฒฝ (polyfill ํ•„์š”)โ€‹

๋ฆฌ์•กํŠธ๋Š” ์ด์ œ ๋ชจ๋˜ ๋ธŒ๋ผ์šฐ์ € ๊ธฐ๋Šฅ์ธ Promise Symbol Object.assign์— ์˜์กดํ•ฉ๋‹ˆ๋‹ค. ์ตœ์‹  ๋ธŒ๋ผ์šฐ์ € ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•˜์ง€ ์•Š๊ฑฐ๋‚˜, ํ˜น์€ ํ˜ธํ™˜๋˜์ง€ ์•Š๋Š” ์ธํ„ฐ๋„ท ์ต์Šคํ”Œ๋กœ๋Ÿฌ ๋“ฑ ์˜ค๋ž˜๋œ ๋ธŒ๋ผ์šฐ์ €๋ฅผ ์ง€์›ํ•ด์•ผ ํ•˜๋Š” ๊ฒฝ์šฐ, ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์— ๊ธ€๋กœ๋ฒŒ ํ”Œ๋กœํ•„์„ ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒƒ์„ ๊ณ ๋ คํ•ด๋ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๋ˆˆ์— ๋„๋Š” ๋ณ€ํ™”โ€‹

๐ŸŽˆ undefined๋„ ๋ Œ๋”๋ง ๊ฐ€๋Šฅโ€‹

์ด์ œ ์ปดํฌ๋„ŒํŠธ๊ฐ€ undefined๋ฅผ ๋ฆฌํ„ดํ•ด๋„ ์—๋Ÿฌ๋ฅผ ๋ฆฌํ„ดํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

๐ŸŽˆ ํ…Œ์ŠคํŠธ ์‹œ์—, act ๊ฒฝ๊ณ ๊ฐ€ ์˜ตํŠธ์ธ ๋จโ€‹

e2e ํ…Œ์ŠคํŠธ ์‹œ act ๊ฒฝ๊ณ ๋Š” ๋ถˆํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. opt-in ๊ฐœ๋…์„ ๋„์ž…ํ•˜์—ฌ ์œ ๋‹›ํ…Œ์ŠคํŠธ ์‹œ์—๋งŒ ์ด๋Ÿฌํ•œ ๊ฒฝ๊ณ ๋ฌธ์„ ๋ฐ›์„ ์ˆ˜ ์žˆ๋„๋ก ๊ตฌ์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐ŸŽˆ No Suppression of console.logโ€‹

strict ๋ชจ๋“œ์—์„œ, ๊ฐ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋‘๋ฒˆ์”ฉ ๋ Œ๋”๋ง ํ•˜๋ฉด ์˜ˆ๋ผ์น˜์•Š์€ ์‚ฌ์ด๋“œ ์ดํŽ™ํŠธ๋ฅผ ๊ฒช์„ ์ˆ˜ ์žˆ๋‹ค. react 17์—์„œ๋Š” ์ด๋Ÿฌํ•œ ๋กœ๊ทธ๋ฅผ ์‰ฝ๊ฒŒ ์ฝ๊ฒŒ ํ•˜๊ธฐ ์œ„ํ•ด ๋‘ ๋ Œ๋”๋ง ์ค‘์— ํ•˜๋‚˜์˜ console.log๋ฅผ ์˜๋„์ ์œผ๋กœ ๋„์šฐ์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ์ด๋Ÿฌํ•œ ๋™์ž‘์ด ํ˜ผ๋ž€์Šค๋Ÿฝ๋‹ค๋Š” ์˜๊ฒฌ์ด ์žˆ์–ด ๋”์ด์ƒ ๊ฒฝ๊ณ ๋ฌธ์„ ์ œ๊ฑฐํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋Œ€์‹ , React DevTools๊ฐ€ ์„ค์น˜๋˜์–ด ์žˆ๋‹ค๋ฉด, ๋‘๋ฒˆ์งธ ๋กœ๊ทธ๊ฐ€ ํšŒ์ƒ‰์œผ๋กœ ํ‘œ์‹œ๋˜๊ณ , ์ด๋ฅผ ์™„์ „ํžˆ ์—†์•จ ์ˆ˜ ์žˆ๋Š” ์˜ต์…˜์ด ์กด์žฌํ•ฉ๋‹ˆ๋‹ค.

๐ŸŽˆ ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰ ์ตœ์ ํ™”โ€‹

๋ฆฌ์•กํŠธ๋Š” ๋งˆ์šดํŠธ ํ•ด์ œ์‹œ์— ๋” ๋งŽ์€ ๋‚ด๋ถ€ ํ•„๋“œ๋ฅผ ์ •๋ฆฌํ•˜์—ฌ, ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์— ์กด์žฌํ•  ์ˆ˜ ์žˆ๋Š” ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜๋กœ ์ธํ•œ ์˜ํ–ฅ์„ ์ค„์—ฌ์ฃผ์—ˆ์Šต๋‹ˆ๋‹ค.

๐ŸŽˆ React DOM Serverโ€‹

renderToStringโ€‹

์„œ๋ฒ„์—์„œ suspending์ด ์ผ์–ด๋‚  ๊ฒฝ์šฐ ๋” ์ด์ƒ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋Œ€์‹  ๊ฐ€์žฅ ๊ฐ€๊นŒ์šด <Suspense> ๋ฐ”์šด๋”๋ฆฌ์— fallback HTML์„ ๋‚ด๋ณด๋‚ธ ํ›„, ํด๋ผ์ด์–ธํŠธ ๋ ˆ๋ฒจ์—์„œ ๊ฐ™์€ ๋ Œ๋”๋ง์„ ์žฌ์‹œ๋„ ํ•ฉ๋‹ˆ๋‹ค. renderToString๋ณด๋‹ค๋Š” renderToPipableStream renderToReadableStream๊ณผ ๊ฐ™์€ ์ŠคํŠธ๋ฆฌ๋ฐ api๋กœ ์ „ํ™˜ํ•˜๋Š” ๊ฒƒ์„ ์ถ”์ฒœํ•ฉ๋‹ˆ๋‹ค.

renderToStaticMarkupโ€‹

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

https://github.com/facebook/react/blob/main/CHANGELOG.md#all-changes