<ViewTransition> - This feature is available in the latest Canary version of React

Canary

The <ViewTransition /> API is currently only available in Reactโ€™s Canary and Experimental channels.

Learn more about Reactโ€™s release channels here.

<ViewTransition>์„ ์‚ฌ์šฉํ•˜๋ฉด Transition ๋‚ด๋ถ€์—์„œ ์—…๋ฐ์ดํŠธ๋˜๋Š” ์—˜๋ฆฌ๋จผํŠธ์— ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์ ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

import {ViewTransition} from 'react';

<ViewTransition>
<div>...</div>
</ViewTransition>

๋ ˆํผ๋Ÿฐ์Šค

<ViewTransition>

์—˜๋ฆฌ๋จผํŠธ๋ฅผ <ViewTransition>์œผ๋กœ ๊ฐ์‹ธ๋ฉด Transition ๋‚ด๋ถ€์—์„œ ์—…๋ฐ์ดํŠธํ•  ๋•Œ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์ ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. React๋Š” ๋‹ค์Œ ํœด๋ฆฌ์Šคํ‹ฑ์„ ์‚ฌ์šฉํ•˜์—ฌ View Transition์ด ์• ๋‹ˆ๋ฉ”์ด์…˜์— ํ™œ์„ฑํ™”๋˜๋Š”์ง€ ํŒ๋‹จํ•ฉ๋‹ˆ๋‹ค.

  • enter: ํ•ด๋‹น Transition์—์„œ ViewTransition ์ž์ฒด๊ฐ€ ์‚ฝ์ž…๋˜๋ฉด ํ™œ์„ฑํ™”๋ฉ๋‹ˆ๋‹ค.
  • exit: ํ•ด๋‹น Transition์—์„œ ViewTransition ์ž์ฒด๊ฐ€ ์‚ญ์ œ๋˜๋ฉด ํ™œ์„ฑํ™”๋ฉ๋‹ˆ๋‹ค.
  • update: ViewTransition ๋‚ด๋ถ€์—์„œ React๊ฐ€ ์ˆ˜ํ–‰ํ•˜๋Š” DOM ๋ณ€๊ฒฝ(์˜ˆ: ํ”„๋กœํผํ‹ฐ ๋ณ€๊ฒฝ)์ด ์žˆ๊ฑฐ๋‚˜ ์ธ์ ‘ํ•œ ํ˜•์ œ ์—˜๋ฆฌ๋จผํŠธ์˜ ์˜ํ–ฅ์œผ๋กœ ViewTransition ๊ฒฝ๊ณ„ ์ž์ฒด์˜ ํฌ๊ธฐ๋‚˜ ์œ„์น˜๊ฐ€ ๋ณ€๊ฒฝ๋˜๋Š” ๊ฒฝ์šฐ ํ™œ์„ฑํ™”๋ฉ๋‹ˆ๋‹ค. ์ค‘์ฒฉ๋œ ViewTransition์ด ์žˆ์œผ๋ฉด ๋ณ€๊ฒฝ์ด ๋ถ€๋ชจ๊ฐ€ ์•„๋‹Œ ํ•ด๋‹น ํ•ญ๋ชฉ์— ์ ์šฉ๋ฉ๋‹ˆ๋‹ค.
  • share: ์ด๋ฆ„์ด ์ง€์ •๋œ ViewTransition์ด ์‚ญ์ œ๋œ ์„œ๋ธŒํŠธ๋ฆฌ ๋‚ด๋ถ€์— ์žˆ๊ณ  ๊ฐ™์€ ์ด๋ฆ„์„ ๊ฐ€์ง„ ๋‹ค๋ฅธ ์ด๋ฆ„ ์žˆ๋Š” ViewTransition์ด ๊ฐ™์€ Transition์—์„œ ์‚ฝ์ž…๋œ ์„œ๋ธŒํŠธ๋ฆฌ์˜ ์ผ๋ถ€์ธ ๊ฒฝ์šฐ ๊ณต์œ  ์—˜๋ฆฌ๋จผํŠธ Transition์„ ํ˜•์„ฑํ•˜๋ฉฐ, ์‚ญ์ œ๋œ ๊ฒƒ์—์„œ ์‚ฝ์ž…๋œ ๊ฒƒ์œผ๋กœ ์• ๋‹ˆ๋ฉ”์ด์…˜๋ฉ๋‹ˆ๋‹ค.

๊ธฐ๋ณธ์ ์œผ๋กœ <ViewTransition>์€ ๋ถ€๋“œ๋Ÿฌ์šด ํฌ๋กœ์Šค ํŽ˜์ด๋“œ(๋ธŒ๋ผ์šฐ์ € ๊ธฐ๋ณธ View Transition)๋กœ ์• ๋‹ˆ๋ฉ”์ด์…˜๋ฉ๋‹ˆ๋‹ค. <ViewTransition> ์ปดํฌ๋„ŒํŠธ์— View Transition ํด๋ž˜์Šค๋ฅผ ์ œ๊ณตํ•˜์—ฌ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์ปค์Šคํ„ฐ๋งˆ์ด์ง•ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ฐ ํŠธ๋ฆฌ๊ฑฐ ์œ ํ˜•์— ๋Œ€ํ•ด ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์ปค์Šคํ„ฐ๋งˆ์ด์ง•ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค(View Transition ์Šคํƒ€์ผ๋ง ์ฐธ๊ณ ).

์ž์„ธํžˆ ์‚ดํŽด๋ณด๊ธฐ

<ViewTransition>์€ ์–ด๋–ป๊ฒŒ ์ž‘๋™ํ•˜๋‚˜์š”?

๋‚ด๋ถ€์ ์œผ๋กœ React๋Š” <ViewTransition> ์ปดํฌ๋„ŒํŠธ ๋‚ด๋ถ€์— ์ค‘์ฒฉ๋œ ๊ฐ€์žฅ ๊ฐ€๊นŒ์šด DOM ๋…ธ๋“œ์˜ ์ธ๋ผ์ธ ์Šคํƒ€์ผ์— view-transition-name์„ ์ ์šฉํ•ฉ๋‹ˆ๋‹ค. <ViewTransition><div /><div /></ViewTransition>์ฒ˜๋Ÿผ ์—ฌ๋Ÿฌ ํ˜•์ œ DOM ๋…ธ๋“œ๊ฐ€ ์žˆ์„ ๊ฒฝ์šฐ, React๋Š” ๊ฐ ๋…ธ๋“œ์˜ ์ด๋ฆ„์ด ๊ณ ์œ ํ•˜๋„๋ก ์ ‘๋ฏธ์‚ฌ๋ฅผ ์ถ”๊ฐ€ํ•˜์ง€๋งŒ, ๊ฐœ๋…์ ์œผ๋กœ๋Š” ๋™์ผํ•œ ์ „ํ™˜์— ์†ํ•˜๋Š” ๊ฒƒ์œผ๋กœ ๊ฐ„์ฃผํ•ฉ๋‹ˆ๋‹ค.

React๋Š” ๋‚ด๋ถ€์ ์œผ๋กœ startViewTransition์„ ์ž์ฒด์ ์œผ๋กœ ํ˜ธ์ถœํ•˜๋ฏ€๋กœ ์ง์ ‘ ํ˜ธ์ถœํ•ด์„œ๋Š” ์•ˆ๋ฉ๋‹ˆ๋‹ค. ์‹ค์ œ๋กœ ํŽ˜์ด์ง€์—์„œ ๋‹ค๋ฅธ ์Šคํฌ๋ฆฝํŠธ๋‚˜ ์ฝ”๋“œ๊ฐ€ ViewTransition์„ ์‹คํ–‰ํ•˜๊ณ  ์žˆ๋‹ค๋ฉด React๊ฐ€ ์ด๋ฅผ ์ค‘๋‹จํ•ฉ๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ React ์ž์ฒด๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ด๋ฅผ ์กฐ์ •ํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค. ๊ณผ๊ฑฐ์— ViewTransition์„ ํŠธ๋ฆฌ๊ฑฐํ•˜๋Š” ๋‹ค๋ฅธ ๋ฐฉ๋ฒ•์ด ์žˆ์—ˆ๋‹ค๋ฉด ๋‚ด์žฅ ๋ฐฉ๋ฒ•์œผ๋กœ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

๋‹ค๋ฅธ React ViewTransition์ด ์ด๋ฏธ ์‹คํ–‰ ์ค‘์ด๋ผ๋ฉด, React๋Š” ๊ทธ๊ฒƒ๋“ค์„ ์™„๋ฃŒํ•  ๋•Œ๊นŒ์ง€ ๋‹ค์Œ ์ „ํ™˜์„ ์‹œ์ž‘ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ์ค‘์š”ํ•œ ์ ์€ ์ฒซ ๋ฒˆ์งธ ์ „ํ™˜์ด ์ง„ํ–‰๋˜๋Š” ๋™์•ˆ ์—ฌ๋Ÿฌ ์—…๋ฐ์ดํŠธ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด, ๊ทธ ์—…๋ฐ์ดํŠธ๋“ค์€ ๋ชจ๋‘ ํ•˜๋‚˜๋กœ ๋ฌถ์—ฌ ์ฒ˜๋ฆฌ๋œ๋‹ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด A์—์„œ B๋กœ ์ด๋™ํ•˜๋Š” ์ „ํ™˜์„ ์‹œ์ž‘ํ–ˆ๋‹ค๊ณ  ๊ฐ€์ •ํ•ฉ์‹œ๋‹ค. ๊ทธ ์‚ฌ์ด์— C๋กœ ๊ฐ€๋Š” ์—…๋ฐ์ดํŠธ๊ฐ€ ๋ฐœ์ƒํ•˜๊ณ  ๋‹ค์‹œ D๋กœ ๊ฐ€๋Š” ์—…๋ฐ์ดํŠธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค๋ฉด, ์ฒซ ๋ฒˆ์งธ A->B ์• ๋‹ˆ๋ฉ”์ด์…˜์ด ๋๋‚œ ํ›„ ๋‹ค์Œ ์• ๋‹ˆ๋ฉ”์ด์…˜์€ B์—์„œ D๋กœ ์ „ํ™˜๋ฉ๋‹ˆ๋‹ค.

getSnapshotBeforeUpdate ์ƒ๋ช…์ฃผ๊ธฐ๋Š” startViewTransition ์ „์— ํ˜ธ์ถœ๋˜๊ณ  ์ผ๋ถ€ view-transition-name์€ ๋™์‹œ์— ์—…๋ฐ์ดํŠธ๋ฉ๋‹ˆ๋‹ค.

๊ทธ๋Ÿฐ ๋‹ค์Œ React๋Š” startViewTransition์„ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค. updateCallback ๋‚ด๋ถ€์—์„œ React๋Š” ๋‹ค์Œ์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.

  • DOM์— ๋ณ€๊ฒฝ์„ ์ ์šฉํ•˜๊ณ  useInsertionEffect๋ฅผ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค.
  • ํฐํŠธ๊ฐ€ ๋กœ๋“œ๋  ๋•Œ๊นŒ์ง€ ๊ธฐ๋‹ค๋ฆฝ๋‹ˆ๋‹ค.
  • componentDidMount, componentDidUpdate, useLayoutEffect, refs๋ฅผ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค.
  • ๋Œ€๊ธฐ ์ค‘์ธ ํƒ์ƒ‰์ด ์™„๋ฃŒ๋  ๋•Œ๊นŒ์ง€ ๊ธฐ๋‹ค๋ฆฝ๋‹ˆ๋‹ค.
  • ๊ทธ๋Ÿฐ ๋‹ค์Œ React๋Š” ๋ ˆ์ด์•„์›ƒ์˜ ๋ณ€๊ฒฝ ์‚ฌํ•ญ์„ ์ธก์ •ํ•˜์—ฌ ์–ด๋–ค ๊ฒฝ๊ณ„๊ฐ€ ์• ๋‹ˆ๋ฉ”์ด์…˜๋˜์–ด์•ผ ํ•˜๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.

startViewTransition์˜ ready Promise๊ฐ€ ํ•ด๊ฒฐ๋œ ์ดํ›„, React๋Š” view-transition-name์„ ๋˜๋Œ๋ฆฝ๋‹ˆ๋‹ค. ๊ทธ ๋‹ค์Œ React๋Š” onEnter, onExit, onUpdate, onShare ์ฝœ๋ฐฑ๋“ค์„ ํ˜ธ์ถœํ•˜์—ฌ ์• ๋‹ˆ๋ฉ”์ด์…˜์— ๋Œ€ํ•ด ์ˆ˜๋™์œผ๋กœ ํ”„๋กœ๊ทธ๋ž˜๋ฐ ๋ฐฉ์‹์˜ ์ œ์–ด๋ฅผ ํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•ฉ๋‹ˆ๋‹ค. ์ด ํ˜ธ์ถœ์€ ๋‚ด์žฅ๋œ ๊ธฐ๋ณธ ์• ๋‹ˆ๋ฉ”์ด์…˜์ด ์ด๋ฏธ ๊ณ„์‚ฐ๋œ ์ดํ›„์— ์ด๋ฃจ์–ด์ง‘๋‹ˆ๋‹ค.

์ด ์‹œํ€€์Šค ์ค‘๊ฐ„์— flushSync๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด ๋™๊ธฐ์ ์œผ๋กœ ์™„๋ฃŒ๋˜์–ด์•ผ ํ•˜๋Š” ํŠน์„ฑ ๋•Œ๋ฌธ์— React๋Š” ํ•ด๋‹น Transition์„ ๊ฑด๋„ˆ๋œ๋‹ˆ๋‹ค.

startViewTransition์˜ finished Promise๊ฐ€ ํ•ด๊ฒฐ๋œ ์ดํ›„์— React๋Š” useEffect๋ฅผ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค. ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด useEffect๊ฐ€ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์„ฑ๋Šฅ์— ์˜ํ–ฅ์„ ์ฃผ์ง€ ์•Š๋„๋ก ๋ฐฉ์ง€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ์ด๊ฒƒ์ด ๋ฐ˜๋“œ์‹œ ๋ณด์žฅ๋˜๋Š” ๊ฒƒ์€ ์•„๋‹™๋‹ˆ๋‹ค. ๋งŒ์•ฝ ์• ๋‹ˆ๋ฉ”์ด์…˜์ด ์‹คํ–‰๋˜๋Š” ๋„์ค‘์— ๋‹ค๋ฅธ setState๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด, ์ˆœ์ฐจ์  ๋™์ž‘ ๋ณด์žฅ์„ ์œ ์ง€ํ•˜๊ธฐ ์œ„ํ•ด useEffect๋ฅผ ๋” ์ผ์ฐ ํ˜ธ์ถœํ•ด์•ผ ํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.

Props

๊ธฐ๋ณธ์ ์œผ๋กœ <ViewTransition>์€ ๋ถ€๋“œ๋Ÿฌ์šด ํฌ๋กœ์Šค ํŽ˜์ด๋“œ๋กœ ์• ๋‹ˆ๋ฉ”์ด์…˜๋ฉ๋‹ˆ๋‹ค. ์ด๋Ÿฌํ•œ ํ”„๋กœํผํ‹ฐ๋กœ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์ปค์Šคํ„ฐ๋งˆ์ด์ฆˆํ•˜๊ฑฐ๋‚˜ ๊ณต์œ  ์—˜๋ฆฌ๋จผํŠธ Transition์„ ์ง€์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

  • optional enter: ๋ฌธ์ž์—ด ๋˜๋Š” ๊ฐ์ฒด. โ€œenterโ€๊ฐ€ ํ™œ์„ฑํ™”๋  ๋•Œ ์ ์šฉํ•  View Transition ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค.
  • optional exit: ๋ฌธ์ž์—ด ๋˜๋Š” ๊ฐ์ฒด. โ€œexitโ€์ด ํ™œ์„ฑํ™”๋  ๋•Œ ์ ์šฉํ•  View Transition ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค.
  • optional update: ๋ฌธ์ž์—ด ๋˜๋Š” ๊ฐ์ฒด. โ€œupdateโ€๊ฐ€ ํ™œ์„ฑํ™”๋  ๋•Œ ์ ์šฉํ•  View Transition ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค.
  • optional share: ๋ฌธ์ž์—ด ๋˜๋Š” ๊ฐ์ฒด. ๊ณต์œ  ์—˜๋ฆฌ๋จผํŠธ๊ฐ€ ํ™œ์„ฑํ™”๋  ๋•Œ ์ ์šฉํ•  View Transition ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค.
  • optional default: ๋ฌธ์ž์—ด ๋˜๋Š” ๊ฐ์ฒด. ๋‹ค๋ฅธ ์ผ์น˜ํ•˜๋Š” ํ™œ์„ฑํ™” ํ”„๋กœํผํ‹ฐ๊ฐ€ ์—†์„ ๋•Œ ์‚ฌ์šฉ๋˜๋Š” View Transition ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค.
  • optional name: ๋ฌธ์ž์—ด ๋˜๋Š” ๊ฐ์ฒด. ๊ณต์œ  ์—˜๋ฆฌ๋จผํŠธ transition์— ์‚ฌ์šฉ๋˜๋Š” View Transition์˜ ์ด๋ฆ„์ž…๋‹ˆ๋‹ค. ์ œ๊ณต๋˜์ง€ ์•Š์œผ๋ฉด React๋Š” ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ๊ฐ View Transition์— ๋Œ€ํ•ด ๊ณ ์œ ํ•œ ์ด๋ฆ„์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

์ฝœ๋ฐฑ

์ด ์ฝœ๋ฐฑ์„ ์‚ฌ์šฉํ•˜๋ฉด animate API๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ๋ช…๋ น์ ์œผ๋กœ ์กฐ์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

  • optional onEnter: ํ•จ์ˆ˜. React๋Š” โ€œenterโ€ ์• ๋‹ˆ๋ฉ”์ด์…˜ ํ›„์— onEnter๋ฅผ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค.
  • optional onExit: ํ•จ์ˆ˜. React๋Š” โ€œexitโ€ ์• ๋‹ˆ๋ฉ”์ด์…˜ ํ›„์— onExit๋ฅผ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค.
  • optional onShare: ํ•จ์ˆ˜. React๋Š” โ€œshareโ€ ์• ๋‹ˆ๋ฉ”์ด์…˜ ํ›„์— onShare๋ฅผ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค.
  • optional onUpdate: ํ•จ์ˆ˜. React๋Š” โ€œupdateโ€ ์• ๋‹ˆ๋ฉ”์ด์…˜ ํ›„์— onUpdate๋ฅผ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค.

๊ฐ ์ฝœ๋ฐฑ์€ ๋‹ค์Œ์„ ์ธ์ˆ˜๋กœ ๋ฐ›์Šต๋‹ˆ๋‹ค.

  • element: ์• ๋‹ˆ๋ฉ”์ด์…˜๋œ DOM ์—˜๋ฆฌ๋จผํŠธ์ž…๋‹ˆ๋‹ค.
  • types: ์• ๋‹ˆ๋ฉ”์ด์…˜์— ํฌํ•จ๋œ Transition ํƒ€์ž…์ž…๋‹ˆ๋‹ค.

View Transition ํด๋ž˜์Šค

View Transition ํด๋ž˜์Šค๋Š” ViewTransition์ด ํ™œ์„ฑํ™”๋  ๋•Œ Transition ์ค‘์— React๊ฐ€ ์ ์šฉํ•˜๋Š” CSS ํด๋ž˜์Šค ์ด๋ฆ„์ž…๋‹ˆ๋‹ค. ๋ฌธ์ž์—ด ๋˜๋Š” ๊ฐ์ฒด์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

  • string: ํ™œ์„ฑํ™”๋  ๋•Œ ์ž์‹ ์—˜๋ฆฌ๋จผํŠธ์— ์ถ”๊ฐ€๋˜๋Š” class์ž…๋‹ˆ๋‹ค. 'none'์ด ์ œ๊ณต๋˜๋ฉด ํด๋ž˜์Šค๊ฐ€ ์ถ”๊ฐ€๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
  • object: ์ž์‹ ์—˜๋ฆฌ๋จผํŠธ์— ์ถ”๊ฐ€๋˜๋Š” ํด๋ž˜์Šค๋Š” addTransitionType์œผ๋กœ ์ถ”๊ฐ€๋œ View Transition ํƒ€์ž…๊ณผ ์ผ์น˜ํ•˜๋Š” ํ‚ค์ž…๋‹ˆ๋‹ค. ๊ฐ์ฒด๋Š” ์ผ์น˜ํ•˜๋Š” ํƒ€์ž…์ด ์—†์„ ๋•Œ ์‚ฌ์šฉํ•  default๋„ ์ง€์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ฐ’ 'none'์€ ํŠน์ • ํŠธ๋ฆฌ๊ฑฐ์— ๋Œ€ํ•ด View Transition์ด ํ™œ์„ฑํ™”๋˜์ง€ ์•Š๋„๋ก ํ•˜๋Š” ๋ฐ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

View Transition ์Šคํƒ€์ผ๋ง

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

์›น์—์„œ View Transition์˜ ๋งŽ์€ ์ดˆ๊ธฐ ์˜ˆ์‹œ์—์„œ view-transition-name์„ ์‚ฌ์šฉํ•œ ๋‹ค์Œ ::view-transition-...(my-name) ์„ ํƒ์ž๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์Šคํƒ€์ผ์„ ์ง€์ •ํ•˜๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ์ด๋Ÿฌํ•œ ๋ฐฉ์‹์œผ๋กœ ์Šคํƒ€์ผ๋งํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋Œ€์‹ , ์ผ๋ฐ˜์ ์œผ๋กœ View Transition ํด๋ž˜์Šค๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

<ViewTransition>์˜ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์ปค์Šคํ„ฐ๋งˆ์ด์ฆˆํ•˜๋ ค๋ฉด ํ™œ์„ฑํ™” ํ”„๋กœํผํ‹ฐ ์ค‘ ํ•˜๋‚˜์— View Transition ํด๋ž˜์Šค๋ฅผ ์ œ๊ณตํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. View Transition ํด๋ž˜์Šค๋Š” ViewTransition์ด ํ™œ์„ฑํ™”๋  ๋•Œ React๊ฐ€ ์ž์‹ ์—˜๋ฆฌ๋จผํŠธ์— ์ ์šฉํ•˜๋Š” CSS ํด๋ž˜์Šค ์ด๋ฆ„์ž…๋‹ˆ๋‹ค.

์˜ˆ๋ฅผ ๋“ค์–ด โ€œenterโ€ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์ปค์Šคํ„ฐ๋งˆ์ด์ฆˆํ•˜๋ ค๋ฉด enter ํ”„๋กœํผํ‹ฐ์— ํด๋ž˜์Šค ์ด๋ฆ„์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

<ViewTransition enter="slide-in">

<ViewTransition>์ด โ€œenterโ€ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ํ™œ์„ฑํ™”ํ•˜๋ฉด React๋Š” ํด๋ž˜์Šค ์ด๋ฆ„ slide-in์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋Ÿฐ ๋‹ค์Œ View Transition ๊ฐ€์ƒ ์„ ํƒ์ž๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ด ํด๋ž˜์Šค๋ฅผ ์ฐธ์กฐํ•˜์—ฌ ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ๊ตฌ์ถ•ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

::view-transition-group(.slide-in) {

}
::view-transition-old(.slide-in) {

}
::view-transition-new(.slide-in) {

}

ํ–ฅํ›„ CSS ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์—์„œ View Transition ํด๋ž˜์Šค๋ฅผ ์‚ฌ์šฉํ•œ ๋‚ด์žฅ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์ถ”๊ฐ€ํ•˜์—ฌ ์‚ฌ์šฉํ•˜๊ธฐ ์‰ฝ๊ฒŒ ๋งŒ๋“ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

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

  • ๊ธฐ๋ณธ์ ์œผ๋กœ setState๋Š” ์ฆ‰์‹œ ์—…๋ฐ์ดํŠธ๋˜๋ฉฐ <ViewTransition>์„ ํ™œ์„ฑํ™”ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. Transition์œผ๋กœ ๊ฐ์‹ผ ์—…๋ฐ์ดํŠธ๋งŒ ํ™œ์„ฑํ™”๋ฉ๋‹ˆ๋‹ค. <Suspense>](/reference/react/Suspense)๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ Transition์„ ์„ ํƒ์ ์œผ๋กœ ์ ์šฉํ•˜๊ณ  ์ฝ˜ํ…์ธ ๋ฅผ ํ•œ ๋ฒˆ์— ํ‘œ์‹œํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.
  • <ViewTransition>์€ ์ด๋™, ํฌ๊ธฐ ์กฐ์ • ๋ฐ ํฌ๋กœ์Šค-ํŽ˜์ด๋“œ๊ฐ€ ๊ฐ€๋Šฅํ•œ ์ด๋ฏธ์ง€๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. React Native๋‚˜ Motion์—์„œ ๋ณผ ์ˆ˜ ์žˆ๋Š” ๋ ˆ์ด์•„์›ƒ ์• ๋‹ˆ๋ฉ”์ด์…˜๊ณผ ๋‹ฌ๋ฆฌ, ๋‚ด๋ถ€์˜ ๋ชจ๋“  ๊ฐœ๋ณ„ ์š”์†Œ๊ฐ€ ์œ„์น˜๋ฅผ ์• ๋‹ˆ๋ฉ”์ด์…˜ํ•˜๋Š” ๊ฒƒ์ด ์•„๋‹™๋‹ˆ๋‹ค. ์ด๋Š” ๋ชจ๋“  ๊ฐœ๋ณ„ ์š”์†Œ๋ฅผ ์• ๋‹ˆ๋ฉ”์ด์…˜ํ•˜๋Š” ๊ฒƒ์— ๋น„ํ•ด ๋” ๋‚˜์€ ์„ฑ๋Šฅ๊ณผ ๋” ์—ฐ์†์ ์ด๊ณ  ๋ถ€๋“œ๋Ÿฌ์šด ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์ œ๊ณตํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ๋…๋ฆฝ์ ์œผ๋กœ ์›€์ง์—ฌ์•ผ ํ•˜๋Š” ์š”์†Œ๋“ค์˜ ์—ฐ์†์„ฑ์„ ์žƒ์„ ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ์ˆ˜๋™์œผ๋กœ ๋” ๋งŽ์€ <ViewTransition> ๊ฒฝ๊ณ„๋ฅผ ์ถ”๊ฐ€ํ•ด์•ผ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ๋งŽ์€ ์‚ฌ์šฉ์ž๊ฐ€ ํŽ˜์ด์ง€์˜ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์„ ํ˜ธํ•˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. React๋Š” ์ด ๊ฒฝ์šฐ์— ๋Œ€ํ•ด ์ž๋™์œผ๋กœ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ๋น„ํ™œ์„ฑํ™”ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž ์„ ํ˜ธ๋„์— ๋”ฐ๋ผ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ๋น„ํ™œ์„ฑํ™”ํ•˜๊ฑฐ๋‚˜ ์ค„์ด๊ธฐ ์œ„ํ•ด @media (prefers-reduced-motion) ๋ฏธ๋””์–ด ์ฟผ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•  ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค. ํ–ฅํ›„ CSS ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์—์„œ ํ”„๋ฆฌ์…‹์— ์ด ๊ธฐ๋Šฅ์ด ๋‚ด์žฅ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ํ˜„์žฌ <ViewTransition>์€ DOM์—์„œ๋งŒ ์ž‘๋™ํ•ฉ๋‹ˆ๋‹ค. React Native ๋ฐ ๊ธฐํƒ€ ํ”Œ๋žซํผ์— ๋Œ€ํ•œ ์ง€์›์„ ์ถ”๊ฐ€ํ•˜๊ธฐ ์œ„ํ•ด ์ž‘์—… ์ค‘์ž…๋‹ˆ๋‹ค.

์‚ฌ์šฉ๋ฒ•

enter/exit์—์„œ ์—˜๋ฆฌ๋จผํŠธ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ ์šฉํ•˜๊ธฐ

Enter/Exit Transition์€ <ViewTransition>์ด Transition์—์„œ ์ปดํฌ๋„ŒํŠธ์— ์˜ํ•ด ์ถ”๊ฐ€๋˜๊ฑฐ๋‚˜ ์ œ๊ฑฐ๋  ๋•Œ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค.

function Child() {
return (
<ViewTransition>
<div>Hi</div>
</ViewTransition>
);
}

function Parent() {
const [show, setShow] = useState();
if (show) {
return <Child />;
}
return null;
}

setShow๊ฐ€ ํ˜ธ์ถœ๋˜๋ฉด show๊ฐ€ true๋กœ ๋ฐ”๋€Œ๊ณ  Child ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋ Œ๋”๋ง๋ฉ๋‹ˆ๋‹ค. setShow๊ฐ€ startTransition ๋‚ด๋ถ€์—์„œ ํ˜ธ์ถœ๋˜๊ณ  Child๊ฐ€ ๋‹ค๋ฅธ DOM ๋…ธ๋“œ๋ณด๋‹ค ๋จผ์ € ViewTransition์„ ๋ Œ๋”๋งํ•˜๋ฉด enter ์• ๋‹ˆ๋ฉ”์ด์…˜์ด ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค.

show๊ฐ€ ๋‹ค์‹œ false๋กœ ๋ฐ”๋€Œ๋ฉด exit ์• ๋‹ˆ๋ฉ”์ด์…˜์ด ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค.

import {
  ViewTransition,
  useState,
  startTransition
} from 'react';
import {Video} from "./Video";
import videos from "./data"

function Item() {
  return (
    <ViewTransition>
      <Video video={videos[0]}/>
    </ViewTransition>
  );
}

export default function Component() {
  const [showItem, setShowItem] = useState(false);
  return (
    <>
      <button
        onClick={() => {
          startTransition(() => {
            setShowItem((prev) => !prev);
          });
        }}
      >{showItem ? 'โž–' : 'โž•'}</button>

      {showItem ? <Item /> : null}
    </>
  );
}

์ฃผ์˜ํ•˜์„ธ์š”!

<ViewTransition>์€ DOM ๋…ธ๋“œ๋ณด๋‹ค ์•ž์— ๋ฐฐ์น˜๋˜์–ด์•ผ๋งŒ ํ™œ์„ฑํ™”๋ฉ๋‹ˆ๋‹ค. Child๊ฐ€ ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค๋ฉด ์• ๋‹ˆ๋ฉ”์ด์…˜์ด ๋ฐœ์ƒํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

function Component() {
return <ViewTransition>Hi</ViewTransition>;
}

๊ณต์œ  ์—˜๋ฆฌ๋จผํŠธ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ ์šฉํ•˜๊ธฐ

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

<ViewTransition name={UNIQUE_NAME}>
<Child />
</ViewTransition>

ํ•˜๋‚˜์˜ ํŠธ๋ฆฌ๊ฐ€ ๋งˆ์šดํŠธ ํ•ด์ œ๋˜๊ณ  ๋‹ค๋ฅธ ํŠธ๋ฆฌ๊ฐ€ ๋งˆ์šดํŠธ๋  ๋•Œ ๋งˆ์šดํŠธ ํ•ด์ œ๋˜๋Š” ํŠธ๋ฆฌ์™€ ๋งˆ์šดํŠธ๋˜๋Š” ํŠธ๋ฆฌ์—์„œ ๋™์ผํ•œ ์ด๋ฆ„์ด ์กด์žฌํ•˜๋Š” ์Œ์ด ์žˆ์œผ๋ฉด ๋‘˜ ๋‹ค์—์„œ โ€œshareโ€ ์• ๋‹ˆ๋ฉ”์ด์…˜์ด ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. ๋งˆ์šดํŠธ ํ•ด์ œ๋˜๋Š” ์ชฝ์—์„œ ๋งˆ์šดํŠธ๋˜๋Š” ์ชฝ์œผ๋กœ ์• ๋‹ˆ๋ฉ”์ด์…˜์ด ์ ์šฉ๋ฉ๋‹ˆ๋‹ค.

exit/enter ์• ๋‹ˆ๋ฉ”์ด์…˜๊ณผ ๋‹ฌ๋ฆฌ ์‚ญ์ œ๋˜๊ฑฐ๋‚˜ ์ƒˆ๋กœ ๋งˆ์šดํŠธ๋œ ํŠธ๋ฆฌ์˜ ๊นŠ์ˆ™ํ•œ ๊ณณ์—์„œ๋„ ์ ์šฉ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. <ViewTransition>์ด exit/enter์—๋„ ํ•ด๋‹นํ•œ๋‹ค๋ฉด โ€œshareโ€ ์• ๋‹ˆ๋ฉ”์ด์…˜์ด ์šฐ์„ ์ˆœ์œ„๋ฅผ ๊ฐ–์Šต๋‹ˆ๋‹ค.

Transition์ด ๋จผ์ € ํ•œ์ชฝ์„ ๋งˆ์šดํŠธ ํ•ด์ œํ•˜๊ณ  ์ƒˆ๋กœ์šด ์ด๋ฆ„์ด ๋งˆ์šดํŠธ๋˜๊ธฐ ์ „์— <Suspense> ํด๋ฐฑ์ด ํ‘œ์‹œ๋˜๋Š” ๊ฒฝ์šฐ ๊ณต์œ  ์—˜๋ฆฌ๋จผํŠธ Transition์€ ๋ฐœ์ƒํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

import {
  ViewTransition,
  useState,
  startTransition
} from "react";
import {Video, Thumbnail, FullscreenVideo} from "./Video";
import videos from "./data";

export default function Component() {
  const [fullscreen, setFullscreen] = useState(false);
  if (fullscreen) {
    return <FullscreenVideo
      video={videos[0]}
      onExit={() => startTransition(() => setFullscreen(false))}
    />
  }
  return <Video
    video={videos[0]}
    onClick={() => startTransition(() => setFullscreen(true))}
  />
}

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

ํ•œ ์Œ์˜ ๋งˆ์šดํŠธ๋œ ์ชฝ์ด๋‚˜ ๋งˆ์šดํŠธ ํ•ด์ œ๋œ ์ชฝ ์ค‘ ํ•˜๋‚˜๊ฐ€ ๋ทฐํฌํŠธ ๋ฐ–์— ์žˆ์œผ๋ฉด ์Œ์ด ํ˜•์„ฑ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ด๋Š” ๋ฌด์–ธ๊ฐ€๊ฐ€ ์Šคํฌ๋กค๋  ๋•Œ ๋ทฐํฌํŠธ ์•ˆํŒŽ์œผ๋กœ ๋‚ ์•„๊ฐ€๋Š” ๊ฒƒ์„ ๋ฐฉ์ง€ํ•ฉ๋‹ˆ๋‹ค. ๋Œ€์‹  ์ผ๋ฐ˜์ ์ธ enter/exit๋กœ ์ž์ฒด์ ์œผ๋กœ ์ฒ˜๋ฆฌ๋ฉ๋‹ˆ๋‹ค.

๋™์ผํ•œ ์ปดํฌ๋„ŒํŠธ ์ธ์Šคํ„ด์Šค๊ฐ€ ์œ„์น˜๋ฅผ ๋ณ€๊ฒฝํ•˜๋Š” ๊ฒฝ์šฐ์—๋Š” ์ด๋Ÿฐ ์ผ์ด ๋ฐœ์ƒํ•˜์ง€ ์•Š์œผ๋ฉฐ โ€œupdateโ€๊ฐ€ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. ํ•œ ์œ„์น˜๊ฐ€ ๋ทฐํฌํŠธ ๋ฐ–์— ์žˆ์–ด๋„ ์• ๋‹ˆ๋ฉ”์ด์…˜์ด ์ ์šฉ๋ฉ๋‹ˆ๋‹ค.

ํ˜„์žฌ ํ•œ ๊ฐ€์ง€ ํŠน์ดํ•œ ์ ์ด ์žˆ๋Š”๋ฐ, ๊นŠ๊ฒŒ ์ค‘์ฒฉ๋œ ๋งˆ์šดํŠธ ํ•ด์ œ๋œ <ViewTransition>์ด ๋ทฐํฌํŠธ ์•ˆ์— ์žˆ๊ณ , ๋งˆ์šดํŠธ๋˜๋Š” ์ชฝ์ด ๋ทฐํฌํŠธ ๋ฐ–์— ์žˆ๋Š” ๊ฒฝ์šฐ, ํ•ด๋‹น ๋งˆ์šดํŠธ ํ•ด์ œ๋œ ์š”์†Œ๋Š” ๋ถ€๋ชจ ์• ๋‹ˆ๋ฉ”์ด์…˜์˜ ์ผ๋ถ€๋กœ ๋™์ž‘ํ•˜๋Š” ๋Œ€์‹ , ๊นŠ๊ฒŒ ์ค‘์ฒฉ๋˜์–ด ์žˆ๋”๋ผ๋„ ์ž์ฒด์ ์ธ โ€œexitโ€ ์• ๋‹ˆ๋ฉ”์ด์…˜์œผ๋กœ ๋™์ž‘ํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

์ฃผ์˜ํ•˜์„ธ์š”!

์ „์ฒด ์•ฑ์—์„œ ๋™์‹œ์— ๋™์ผํ•œ name์œผ๋กœ ๋งˆ์šดํŠธ๋œ ๊ฒƒ์ด ํ•˜๋‚˜๋งŒ ์žˆ์–ด์•ผ ํ•œ๋‹ค๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ์ถฉ๋Œ์„ ํ”ผํ•˜๊ธฐ ์œ„ํ•ด name์— ๊ณ ์œ ํ•œ ๋„ค์ž„์ŠคํŽ˜์ด์Šค๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฅผ ํ™•์‹คํžˆ ํ•˜๊ธฐ ์œ„ํ•ด ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ๋Š” ๋ณ„๋„ ๋ชจ๋“ˆ์— ์ƒ์ˆ˜๋ฅผ ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

export const MY_NAME = "my-globally-unique-name";
import {MY_NAME} from './shared-name';
...
<ViewTransition name={MY_NAME}>

๋ชฉ๋ก์—์„œ ํ•ญ๋ชฉ ์ˆœ์„œ ๋ณ€๊ฒฝ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ ์šฉํ•˜๊ธฐ

items.map(item => <Component key={item.id} item={item} />)

์ฝ˜ํ…์ธ ๋ฅผ ์—…๋ฐ์ดํŠธํ•˜์ง€ ์•Š๊ณ  ๋ชฉ๋ก ์ˆœ์„œ๋ฅผ ๋ณ€๊ฒฝํ•  ๋•Œ DOM ๋…ธ๋“œ ๋ฐ–์— ์žˆ์œผ๋ฉด ๋ชฉ๋ก์˜ ๊ฐ <ViewTransition>์—์„œ โ€œupdateโ€ ์• ๋‹ˆ๋ฉ”์ด์…˜์ด ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. enter/exit ์• ๋‹ˆ๋ฉ”์ด์…˜๊ณผ ์œ ์‚ฌํ•ฉ๋‹ˆ๋‹ค.

์ด๋Š” ์ด <ViewTransition>์—์„œ ์• ๋‹ˆ๋ฉ”์ด์…˜์ด ๋ฐœ์ƒํ•œ๋‹ค๋Š” ์˜๋ฏธ์ž…๋‹ˆ๋‹ค.

function Component() {
return <ViewTransition><div>...</div></ViewTransition>;
}
import {
  ViewTransition,
  useState,
  startTransition
} from "react";
import {Video} from "./Video";
import videos from "./data";

export default function Component() {
  const [orderedVideos, setOrderedVideos] = useState(videos);
  const reorder = () => {
    startTransition(() => {
      setOrderedVideos((prev) => {
        return [...prev.sort(() => Math.random() - 0.5)];
      });
    });
  };
  return (
    <>
      <button onClick={reorder}>๐ŸŽฒ</button>
      <div className="listContainer">
        {orderedVideos.map((video, i) => {
          return (
            <ViewTransition key={video.title}>
              <Video video={video} />
            </ViewTransition>
          );
        })}
      </div>
    </>
  );
}

ํ•˜์ง€๋งŒ ๋‹ค์Œ์€ ๊ฐ ๊ฐœ๋ณ„ ํ•ญ๋ชฉ์— ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์ ์šฉํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

function Component() {
return <div><ViewTransition>...</ViewTransition></div>;
}

๋Œ€์‹  ๋ถ€๋ชจ <ViewTransition>์ด ํฌ๋กœ์Šค ํŽ˜์ด๋“œ๋ฉ๋‹ˆ๋‹ค. ๋ถ€๋ชจ <ViewTransition>์ด ์—†์œผ๋ฉด ๋ณ„๋„์˜ ์• ๋‹ˆ๋ฉ”์ด์…˜์ด ์ ์šฉ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

import {
  ViewTransition,
  useState,
  startTransition
} from "react";
import {Video} from "./Video";
import videos from "./data";

export default function Component() {
  const [orderedVideos, setOrderedVideos] = useState(videos);
  const reorder = () => {
    startTransition(() => {
      setOrderedVideos((prev) => {
        return [...prev.sort(() => Math.random() - 0.5)];
      });
    });
  };
  return (
    <>
      <button onClick={reorder}>๐ŸŽฒ</button>
      <ViewTransition>
        <div className="listContainer">
          {orderedVideos.map((video, i) => {
            return <Video video={video} key={video.title} />;
          })}
        </div>
      </ViewTransition>
    </>
  );
}

์ด๋Š” ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์ž์ฒด์ ์œผ๋กœ ์ˆœ์„œ ๋ณ€๊ฒฝ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์ œ์–ดํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•˜๊ณ  ์‹ถ์„ ๋•Œ๋Š” ๋ฆฌ์ŠคํŠธ ์•ˆ์— ๋ž˜ํผ ์š”์†Œ๋ฅผ ๋‘์ง€ ์•Š๋Š” ๊ฒƒ์ด ์ข‹๋‹ค๋Š” ๋œป์ž…๋‹ˆ๋‹ค.

items.map(item => <div><Component key={item.id} item={item} /></div>)

์œ„ ๊ทœ์น™์€ ํ•ญ๋ชฉ ์ค‘ ํ•˜๋‚˜๊ฐ€ ํฌ๊ธฐ ์กฐ์ •์„ ์œ„ํ•ด ์—…๋ฐ์ดํŠธ๋˜์–ด ํ˜•์ œ ํ•ญ๋ชฉ๋“ค์ด ํฌ๊ธฐ ์กฐ์ •๋˜๋Š” ๊ฒฝ์šฐ์—๋„ ์ ์šฉ๋˜๋ฉฐ, ์ด๋Š” ํ˜•์ œ <ViewTransition>๋„ ์• ๋‹ˆ๋ฉ”์ด์…˜์‹œํ‚ค์ง€๋งŒ ์ง์ ‘์ ์ธ ํ˜•์ œ์ธ ๊ฒฝ์šฐ์—๋งŒ ํ•ด๋‹นํ•ฉ๋‹ˆ๋‹ค.

์ด๊ฒƒ์€ ์—…๋ฐ์ดํŠธ๊ฐ€ ๋ฐœ์ƒํ•˜์—ฌ ๋ ˆ์ด์•„์›ƒ์ด ํฌ๊ฒŒ ๋ณ€๊ฒฝ๋  ๋•Œ, ํŽ˜์ด์ง€์— ์žˆ๋Š” ๋ชจ๋“  <ViewTransition>์„ ๊ฐ๊ฐ ๊ฐœ๋ณ„์ ์œผ๋กœ ์• ๋‹ˆ๋ฉ”์ด์…˜ํ•˜์ง€ ์•Š๋Š”๋‹ค๋Š” ๋œป์ž…๋‹ˆ๋‹ค. ๊ทธ๋ ‡๊ฒŒ ํ•˜๋ฉด ์‹ค์ œ ๋ณ€ํ™”์™€ ๊ด€๊ณ„์—†๋Š” ๋งŽ์€ ์‚ฐ๋งŒํ•œ ์• ๋‹ˆ๋ฉ”์ด์…˜์ด ๋ฐœ์ƒํ•ด ์ฃผ์˜๋ฅผ ํํŠธ๋Ÿฌ๋œจ๋ฆฌ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ React๋Š” ๊ฐœ๋ณ„ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์–ธ์ œ ํŠธ๋ฆฌ๊ฑฐํ• ์ง€์— ๋Œ€ํ•ด ๋ณด๋‹ค ๋ณด์ˆ˜์ ์œผ๋กœ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค.

์ฃผ์˜ํ•˜์„ธ์š”!

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


Suspense ์ฝ˜ํ…์ธ ์—์„œ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ ์šฉํ•˜๊ธฐ

๋‹ค๋ฅธ Transition๊ณผ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ React๋Š” ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์‹คํ–‰ํ•˜๊ธฐ ์ „์— ๋ฐ์ดํ„ฐ์™€ ์ƒˆ๋กœ์šด CSS(<link rel="stylesheet" precedence="...">)๋ฅผ ๊ธฐ๋‹ค๋ฆฝ๋‹ˆ๋‹ค. ์ด์— ๋”ํ•ด ViewTransition์€ ์ƒˆ๋กœ์šด ํฐํŠธ๊ฐ€ ๋‚˜์ค‘์— ๊นœ๋นก์ด๋Š” ๊ฒƒ์„ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์‹œ์ž‘ํ•˜๊ธฐ ์ „์— ์ƒˆ๋กœ์šด ํฐํŠธ๊ฐ€ ๋กœ๋“œ๋  ๋•Œ๊นŒ์ง€ ์ตœ๋Œ€ 500ms๊นŒ์ง€ ๊ธฐ๋‹ค๋ฆฝ๋‹ˆ๋‹ค. ๊ฐ™์€ ์ด์œ ๋กœ ViewTransition์œผ๋กœ ๋ž˜ํ•‘๋œ ์ด๋ฏธ์ง€๋Š” ์ด๋ฏธ์ง€๊ฐ€ ๋กœ๋“œ๋  ๋•Œ๊นŒ์ง€ ๊ธฐ๋‹ค๋ฆฝ๋‹ˆ๋‹ค.

์ƒˆ๋กœ์šด Suspense ๊ฒฝ๊ณ„ ์ธ์Šคํ„ด์Šค ๋‚ด๋ถ€์— ์žˆ์œผ๋ฉด ํด๋ฐฑ์ด ๋จผ์ € ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค. Suspense ๊ฒฝ๊ณ„๊ฐ€ ์™„์ „ํžˆ ๋กœ๋“œ๋œ ํ›„ <ViewTransition>์ด ์ฝ˜ํ…์ธ ๋กœ ์ „ํ™˜๋˜๋Š” ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค.

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

<ViewTransition>์„ ๋ฐฐ์น˜ํ•˜๋Š” ์œ„์น˜์— ๋”ฐ๋ผ Suspense ๊ฒฝ๊ณ„๋ฅผ ์• ๋‹ˆ๋ฉ”์ด์…˜ํ•˜๋Š” ๋‘ ๊ฐ€์ง€ ๋ฐฉ๋ฒ•์ด ์žˆ์Šต๋‹ˆ๋‹ค.

Update:

<ViewTransition>
<Suspense fallback={<A />}>
<B />
</Suspense>
</ViewTransition>

์ด ์‹œ๋‚˜๋ฆฌ์˜ค์—์„œ ์ฝ˜ํ…์ธ ๊ฐ€ A์—์„œ B๋กœ ๋ฐ”๋€” ๋•Œ โ€œupdateโ€๋กœ ์ฒ˜๋ฆฌ๋˜๋ฉฐ ์ ์ ˆํ•œ ๊ฒฝ์šฐ ํ•ด๋‹น ํด๋ž˜์Šค๋ฅผ ์ ์šฉํ•ฉ๋‹ˆ๋‹ค. A์™€ B ๋ชจ๋‘ ๋™์ผํ•œ view-transition-name์„ ๊ฐ–๊ฒŒ ๋˜๋ฏ€๋กœ ๊ธฐ๋ณธ์ ์œผ๋กœ ํฌ๋กœ์Šค ํŽ˜์ด๋“œ๋กœ ์ž‘๋™ํ•ฉ๋‹ˆ๋‹ค.

import {
  ViewTransition,
  useState,
  startTransition,
  Suspense
} from 'react';
import {Video, VideoPlaceholder} from "./Video";
import {useLazyVideoData} from "./data"

function LazyVideo() {
  const video = useLazyVideoData();
  return (
    <Video video={video}/>
  );
}

export default function Component() {
  const [showItem, setShowItem] = useState(false);
  return (
    <>
      <button
        onClick={() => {
          startTransition(() => {
            setShowItem((prev) => !prev);
          });
        }}
      >{showItem ? 'โž–' : 'โž•'}</button>
      {showItem ? (
        <ViewTransition>
          <Suspense fallback={<VideoPlaceholder />}>
            <LazyVideo />
          </Suspense>
        </ViewTransition>
      ) : null}
    </>
  );
}

Enter/Exit:

<Suspense fallback={<ViewTransition><A /></ViewTransition>}>
<ViewTransition><B /></ViewTransition>
</Suspense>

์ด ์‹œ๋‚˜๋ฆฌ์˜ค์—์„œ๋Š” ๊ฐ๊ฐ ๊ณ ์œ ํ•œ view-transition-name์„ ๊ฐ–๋Š” ๋‘ ๊ฐœ์˜ ๋ณ„๋„ ViewTransition ์ธ์Šคํ„ด์Šค์ž…๋‹ˆ๋‹ค. ์ด๋Š” <A>์˜ โ€œexitโ€์™€ <B>์˜ โ€œenterโ€๋กœ ์ฒ˜๋ฆฌ๋ฉ๋‹ˆ๋‹ค.

<ViewTransition> ๊ฒฝ๊ณ„๋ฅผ ๋ฐฐ์น˜ํ•˜๋Š” ์œ„์น˜์— ๋”ฐ๋ผ ๋‹ค๋ฅธ ํšจ๊ณผ๋ฅผ ์–ป์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


์• ๋‹ˆ๋ฉ”์ด์…˜ ์ œ์™ธํ•˜๊ธฐ

๋•Œ๋กœ๋Š” ์ „์ฒด ํŽ˜์ด์ง€์™€ ๊ฐ™์€ ํฐ ๊ธฐ์กด ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ž˜ํ•‘ํ•˜๊ณ  ํ…Œ๋งˆ ๋ณ€๊ฒฝ๊ณผ ๊ฐ™์€ ์ผ๋ถ€ ์—…๋ฐ์ดํŠธ๋ฅผ ์• ๋‹ˆ๋ฉ”์ด์…˜ํ•˜๊ณ  ์‹ถ์ง€๋งŒ ์ „์ฒด ํŽ˜์ด์ง€ ๋‚ด๋ถ€์˜ ๋ชจ๋“  ์—…๋ฐ์ดํŠธ๊ฐ€ ์—…๋ฐ์ดํŠธ๋  ๋•Œ ํฌ๋กœ์Šค ํŽ˜์ด๋“œ์— ํฌํ•จ๋˜๋Š” ๊ฒƒ์„ ์›ํ•˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํŠนํžˆ ์ ์ง„์ ์œผ๋กœ ๋” ๋งŽ์€ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒฝ์šฐ์— ๊ทธ๋ ‡์Šต๋‹ˆ๋‹ค.

ํด๋ž˜์Šค โ€œnoneโ€์„ ์‚ฌ์šฉํ•˜์—ฌ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์ œ์™ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ž์‹์„ โ€œnoneโ€์œผ๋กœ ๋ž˜ํ•‘ํ•˜๋ฉด ๋ถ€๋ชจ๊ฐ€ ์—ฌ์ „ํžˆ ๋ฐœ์ƒํ•˜๋Š” ๋™์•ˆ ์ž์‹์— ๋Œ€ํ•œ ์—…๋ฐ์ดํŠธ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ๋น„ํ™œ์„ฑํ™”ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

<ViewTransition>
<div className={theme}>
<ViewTransition update="none">
{children}
</ViewTransition>
</div>
</ViewTransition>

์ด๋Š” ํ…Œ๋งˆ๊ฐ€ ๋ณ€๊ฒฝ๋  ๋•Œ๋งŒ ์• ๋‹ˆ๋ฉ”์ด์…˜๋˜๋ฉฐ ์ž์‹๋งŒ ์—…๋ฐ์ดํŠธ๋  ๋•Œ๋Š” ์• ๋‹ˆ๋ฉ”์ด์…˜๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ž์‹์€ ์—ฌ์ „ํžˆ ์ž์ฒด <ViewTransition>์œผ๋กœ ๋‹ค์‹œ ์ฐธ์—ฌํ•  ์ˆ˜ ์žˆ์ง€๋งŒ ์ตœ์†Œํ•œ ๋‹ค์‹œ ์ˆ˜๋™์œผ๋กœ ์ œ์–ดํ•˜๋Š” ๋ฐฉ์‹์ด ๋ฉ๋‹ˆ๋‹ค.


์• ๋‹ˆ๋ฉ”์ด์…˜ ์ปค์Šคํ„ฐ๋งˆ์ด์ง•

๊ธฐ๋ณธ์ ์œผ๋กœ <ViewTransition>์€ ๋ธŒ๋ผ์šฐ์ €์˜ ๊ธฐ๋ณธ ํฌ๋กœ์Šค ํŽ˜์ด๋“œ๋ฅผ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค.

์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์ปค์Šคํ„ฐ๋งˆ์ด์ง•ํ•˜๋ ค๋ฉด <ViewTransition> ์ปดํฌ๋„ŒํŠธ์— props๋ฅผ ์ œ๊ณตํ•˜์—ฌ <ViewTransition>์ด ํ™œ์„ฑํ™”๋˜๋Š” ๋ฐฉ์‹์— ๋”ฐ๋ผ ์‚ฌ์šฉํ•  ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์ง€์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์˜ˆ๋ฅผ ๋“ค์–ด ๊ธฐ๋ณธ ํฌ๋กœ์Šค ํŽ˜์ด๋“œ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ๋А๋ฆฌ๊ฒŒ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

<ViewTransition default="slow-fade">
<Video />
</ViewTransition>

๊ทธ๋ฆฌ๊ณ  View Transition ํด๋ž˜์Šค๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ CSS์—์„œ slow-fade๋ฅผ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.

::view-transition-old(.slow-fade) {
animation-duration: 500ms;
}

::view-transition-new(.slow-fade) {
animation-duration: 500ms;
}
import {
  ViewTransition,
  useState,
  startTransition
} from 'react';
import {Video} from "./Video";
import videos from "./data"

function Item() {
  return (
    <ViewTransition default="slow-fade">
      <Video video={videos[0]}/>
    </ViewTransition>
  );
}

export default function Component() {
  const [showItem, setShowItem] = useState(false);
  return (
    <>
      <button
        onClick={() => {
          startTransition(() => {
            setShowItem((prev) => !prev);
          });
        }}
      >{showItem ? 'โž–' : 'โž•'}</button>

      {showItem ? <Item /> : null}
    </>
  );
}

default ์„ค์ • ์™ธ์—๋„ enter, exit, update, share ์• ๋‹ˆ๋ฉ”์ด์…˜์— ๋Œ€ํ•œ ๊ตฌ์„ฑ์„ ์ œ๊ณตํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

import {
  ViewTransition,
  useState,
  startTransition
} from 'react';
import {Video} from "./Video";
import videos from "./data"

function Item() {
  return (
    <ViewTransition enter="slide-in" exit="slide-out">
      <Video video={videos[0]}/>
    </ViewTransition>
  );
}

export default function Component() {
  const [showItem, setShowItem] = useState(false);
  return (
    <>
      <button
        onClick={() => {
          startTransition(() => {
            setShowItem((prev) => !prev);
          });
        }}
      >{showItem ? 'โž–' : 'โž•'}</button>

      {showItem ? <Item /> : null}
    </>
  );
}

ํƒ€์ž…์œผ๋กœ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ปค์Šคํ„ฐ๋งˆ์ด์ง•ํ•˜๊ธฐ

ํŠน์ • ํ™œ์„ฑํ™” ํŠธ๋ฆฌ๊ฑฐ์— ๋Œ€ํ•ด ํŠน์ • Transition ํƒ€์ž…์ด ํ™œ์„ฑํ™”๋  ๋•Œ ์ž์‹ ์—˜๋ฆฌ๋จผํŠธ์— ํด๋ž˜์Šค ์ด๋ฆ„์„ ์ถ”๊ฐ€ํ•˜๊ธฐ ์œ„ํ•ด addTransitionType API๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด ๊ฐ Transition ํƒ€์ž…์— ๋Œ€ํ•œ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์ปค์Šคํ„ฐ๋งˆ์ด์ง•ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์˜ˆ๋ฅผ ๋“ค์–ด ๋ชจ๋“  ์•ž์œผ๋กœ ๋ฐ ๋’ค๋กœ ๋„ค๋น„๊ฒŒ์ด์…˜์— ๋Œ€ํ•œ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์ปค์Šคํ„ฐ๋งˆ์ด์ง•ํ•˜๋ ค๋ฉด,

<ViewTransition default={{
'navigation-back': 'slide-right',
'navigation-forward': 'slide-left',
}}>
<div>...</div>
</ViewTransition>

// ๋ผ์šฐํ„ฐ์—์„œ:
startTransition(() => {
addTransitionType('navigation-' + navigationType);
});

ViewTransition์ด โ€œnavigation-backโ€ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ํ™œ์„ฑํ™”ํ•˜๋ฉด React๋Š” โ€œslide-rightโ€ ํด๋ž˜์Šค ์ด๋ฆ„์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. ViewTransition์ด โ€œnavigation-forwardโ€ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ํ™œ์„ฑํ™”ํ•˜๋ฉด React๋Š” โ€œslide-leftโ€ ํด๋ž˜์Šค ์ด๋ฆ„์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.

ํ–ฅํ›„ ๋ผ์šฐํ„ฐ์™€ ๋‹ค๋ฅธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋“ค์ด ํ‘œ์ค€ view-transition ํƒ€์ž…๊ณผ ์Šคํƒ€์ผ์— ๋Œ€ํ•œ ์ง€์›์„ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

import {
  ViewTransition,
  addTransitionType,
  useState,
  startTransition,
} from "react";
import {Video} from "./Video";
import videos from "./data"

function Item() {
  return (
    <ViewTransition enter={
        {
          "add-video-back": "slide-in-back",
          "add-video-forward": "slide-in-forward"
        }
      }
      exit={
        {
          "remove-video-back": "slide-in-forward",
          "remove-video-forward": "slide-in-back"
        }
      }>
      <Video video={videos[0]}/>
    </ViewTransition>
  );
}

export default function Component() {
  const [showItem, setShowItem] = useState(false);
  return (
    <>
      <div className="button-container">
        <button
          onClick={() => {
            startTransition(() => {
              if (showItem) {
                addTransitionType("remove-video-back")
              } else {
                addTransitionType("add-video-back")
              }
              setShowItem((prev) => !prev);
            });
          }}
        >โฌ…๏ธ</button>
        <button
          onClick={() => {
            startTransition(() => {
              if (showItem) {
                addTransitionType("remove-video-forward")
              } else {
                addTransitionType("add-video-forward")
              }
              setShowItem((prev) => !prev);
            });
          }}
        >โžก๏ธ</button>
      </div>
      {showItem ? <Item /> : null}
    </>
  );
}

View Transition ์ง€์› ๋ผ์šฐํ„ฐ ๊ตฌ์ถ•ํ•˜๊ธฐ

์Šคํฌ๋กค ๋ณต์›์ด ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ค‘์— ์ •์ƒ์ ์œผ๋กœ ๋™์ž‘ํ•˜๋„๋ก, React๋Š” ๋Œ€๊ธฐ ์ค‘์ธ ๋‚ด๋น„๊ฒŒ์ด์…˜์ด ์™„๋ฃŒ๋  ๋•Œ๊นŒ์ง€ ๊ธฐ๋‹ค๋ฆฝ๋‹ˆ๋‹ค. ๋„ค๋น„๊ฒŒ์ด์…˜์ด React์—์„œ ์ฐจ๋‹จ๋˜๋Š” ๊ฒฝ์šฐ useEffect๋Š” ๊ต์ฐฉ ์ƒํƒœ๋กœ ์ด์–ด์งˆ ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ ๋ผ์šฐํ„ฐ๋Š” useLayoutEffect์—์„œ ์ฐจ๋‹จ์„ ํ•ด์ œํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

โ€๋’ค๋กœโ€ ๋„ค๋น„๊ฒŒ์ด์…˜ ์ค‘์ฒ˜๋Ÿผ ๋ ˆ๊ฑฐ์‹œ popstate ์ด๋ฒคํŠธ์—์„œ startTransition์ด ์‹œ์ž‘๋˜๋ฉด ์Šคํฌ๋กค๊ณผ ํผ ๋ณต์›์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ž‘๋™ํ•˜๋„๋ก ๋™๊ธฐ์ ์œผ๋กœ ์™„๋ฃŒ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ด๋Š” View Transition ์• ๋‹ˆ๋ฉ”์ด์…˜ ์‹คํ–‰๊ณผ ์ถฉ๋Œํ•ฉ๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ React๋Š” popstate์—์„œ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ๊ฑด๋„ˆ๋œ๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ๋’ค๋กœ ๋ฒ„ํŠผ์— ๋Œ€ํ•ด์„œ๋Š” ์• ๋‹ˆ๋ฉ”์ด์…˜์ด ์‹คํ–‰๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. Navigation API๋ฅผ ์‚ฌ์šฉํ•˜๋„๋ก ๋ผ์šฐํ„ฐ๋ฅผ ์—…๊ทธ๋ ˆ์ด๋“œํ•˜์—ฌ ์ด๋ฅผ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


๋ฌธ์ œ ํ•ด๊ฒฐ

<ViewTransition>์ด ํ™œ์„ฑํ™”๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค

<ViewTransition> only activates if it is placed before any DOM node:

function Component() {
return (
<div>
<ViewTransition>Hi</ViewTransition>
</div>
);
}

ํ•ด๊ฒฐํ•˜๋ ค๋ฉด <ViewTransition>์ด ๋‹ค๋ฅธ DOM ๋…ธ๋“œ๋ณด๋‹ค ์•ž์— ์˜ค๋„๋ก ํ•˜์„ธ์š”.

function Component() {
return (
<ViewTransition>
<div>Hi</div>
</ViewTransition>
);
}

โ€๋™์ผํ•œ ์ด๋ฆ„์œผ๋กœ ๋งˆ์šดํŠธ๋œ <ViewTransition name=%s> ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋‘ ๊ฐœ ์žˆ์Šต๋‹ˆ๋‹ค.โ€๋ผ๋Š” ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค

์ด ์˜ค๋ฅ˜๋Š” ๋™์ผํ•œ name์„ ๊ฐ€์ง„ ๋‘ ๊ฐœ์˜ <ViewTransition> ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋™์‹œ์— ๋งˆ์šดํŠธ๋  ๋•Œ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค.

function Item() {
// ๐Ÿšฉ ๋ชจ๋“  ํ•ญ๋ชฉ์ด ๋™์ผํ•œ "name"์„ ๊ฐ–๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.
return <ViewTransition name="item">...</ViewTransition>;
}

function ItemList({items}) {
return (
<>
{item.map(item => <Item key={item.id} />)}
</>
);
}

์ด๋Š” View Transition์—์„œ ์˜ค๋ฅ˜๋ฅผ ๋ฐœ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค. ๊ฐœ๋ฐœ ์ค‘์— React๋Š” ์ด ๋ฌธ์ œ๋ฅผ ๊ฐ์ง€ํ•˜์—ฌ ํ‘œ๋ฉดํ™”ํ•˜๊ณ  ๋‘ ๊ฐœ์˜ ์˜ค๋ฅ˜๋ฅผ ๊ธฐ๋กํ•ฉ๋‹ˆ๋‹ค.

Console
There are two <ViewTransition name=%s> components with the same name mounted at the same time. This is not supported and will cause View Transitions to error. Try to use a more unique name e.g. by using a namespace prefix and adding the id of an item to the name. at Item at ItemList
The existing <ViewTransition name=%s> duplicate has this stack trace. at Item at ItemList

ํ•ด๊ฒฐํ•˜๋ ค๋ฉด name์ด ๊ณ ์œ ํ•˜๋„๋ก ํ•˜๊ฑฐ๋‚˜ ์ด๋ฆ„์— id๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ์ „์ฒด ์•ฑ์—์„œ ๋™์ผํ•œ ์ด๋ฆ„์„ ๊ฐ€์ง„ <ViewTransition>์ด ํ•œ ๋ฒˆ์— ํ•˜๋‚˜๋งŒ ๋งˆ์šดํŠธ๋˜๋„๋ก ํ•˜์„ธ์š”.

function Item({id}) {
// โœ… ๋ชจ๋“  ํ•ญ๋ชฉ์ด ๊ณ ์œ ํ•œ "name"์„ ๊ฐ–๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.
return <ViewTransition name={`item-${id}`}>...</ViewTransition>;
}

function ItemList({items}) {
return (
<>
{item.map(item => <Item key={item.id} item={item} />)}
</>
);
}