--- id: frontend-view-transitions-deep title: View Transitions API — 페이지 / 요소 transition category: Coding status: draft source_trust_level: B verification_status: conceptual created_at: 2026-05-09 updated_at: 2026-05-09 tags: [frontend, view-transitions, animation, vibe-coding] tech_stack: { language: "TS / CSS", applicable_to: ["Frontend"] } applied_in: [] aliases: [View Transitions API, MPA transitions, SPA transitions, view-transition-name, cross-document] --- # View Transitions API > Page / view 변경의 native transition. **Same-document (SPA) + Cross-document (MPA)**. Chrome 111+ / Safari 18+ / Firefox 141+. CSS-only 가 가능. ## 📖 핵심 개념 - Transition: snapshot → 새 view → 자동 cross-fade. - view-transition-name: element 별 morph. - Same-document: SPA 안. - Cross-document: 페이지 navigation. ## 💻 코드 패턴 ### Same-document (SPA) ```ts async function navigate(url: string) { if (!document.startViewTransition) { return router.go(url); // fallback } const t = document.startViewTransition(async () => { await router.go(url); }); await t.finished; } ``` ```css /* Default cross-fade */ ::view-transition-old(root), ::view-transition-new(root) { animation-duration: 0.3s; } ``` ### CSS-only fade ```css @view-transition { navigation: auto; } ``` → 모든 navigation 자동 fade. Chrome 126+. ### Element morph (shared element) ```html ``` → 같은 name = 자동 morph (size, position). ```css ::view-transition-group(hero) { animation-duration: 0.5s; } ``` ### React Router 7 / Next 통합 ```tsx // React Router 7 About // Next.js (next/link) import Link from 'next/link'; About // + Chrome flag 또는 view-transition CSS ``` ### Custom transition ```css /* 슬라이드 in/out */ ::view-transition-old(root) { animation: 0.4s ease-out both slide-out-left; } ::view-transition-new(root) { animation: 0.4s ease-out both slide-in-right; } @keyframes slide-out-left { to { transform: translateX(-100%); opacity: 0; } } @keyframes slide-in-right { from { transform: translateX(100%); opacity: 0; } } ``` ### 다중 elements ```html

...

...

``` → 클릭 시 → 자동 morph. iOS-like UX. ### Direction-based (back / forward) ```ts const t = document.startViewTransition(async () => { await router.go(url); }); if (direction === 'back') { document.documentElement.classList.add('back'); } ``` ```css .back::view-transition-old(root) { animation: slide-out-right; } .back::view-transition-new(root) { animation: slide-in-left; } ``` ### Cross-document (MPA, Chrome 126+) ```css @view-transition { navigation: auto; } ``` ```html ``` → Page 간 이동 자동 morph. SPA framework 없이 OK. ### Fallback 처리 ```ts async function navigate(url: string) { if (!('startViewTransition' in document)) { return route(url); // 즉시 } document.startViewTransition(() => route(url)); } ``` ### Reduce motion ```css @media (prefers-reduced-motion: reduce) { ::view-transition-old(root), ::view-transition-new(root) { animation: none; } } ``` ### Promise / async ```ts const transition = document.startViewTransition(async () => { // DOM 변경 await fetchAndUpdate(); }); await transition.ready; // animation 시작 직전 await transition.finished; // 끝 ``` ### 사용 case ``` 1. SPA 페이지 전환 2. List → detail (shared element) 3. Image gallery (큰 image morph) 4. Tab switching (smooth) 5. Modal 열기 / 닫기 6. Theme switch (큰 cross-fade) ``` ### Theme switch 예 ```ts async function toggleTheme() { const t = document.startViewTransition(() => { document.documentElement.classList.toggle('dark'); }); await t.ready; document.documentElement.animate({ clipPath: ['circle(0% at 100% 0%)', 'circle(150% at 100% 0%)'], }, { duration: 500, pseudoElement: '::view-transition-new(root)', }); } ``` → Dark mode toggle 시 화면 wipe. ### Snapshot 비용 ``` Browser 가 each transition 시 snapshot. 큰 page = 약간 느림. → 측정: Chrome DevTools Performance → Animation. ``` ### 제어 (skip) ```ts const t = document.startViewTransition(() => update()); // 사용자가 다른 거 누름 — 즉시 끝 button.onclick = () => t.skipTransition(); ``` ### 단일 element transition ```ts // Modal 열기 async function openModal() { document.startViewTransition(() => { modalEl.style.viewTransitionName = 'modal'; modalEl.classList.add('open'); }); } ``` ```css ::view-transition-new(modal) { animation: 0.3s scale-in; } @keyframes scale-in { from { transform: scale(0.8); opacity: 0; } to { transform: scale(1); opacity: 1; } } ``` ### Multi-step ```ts const t1 = document.startViewTransition(() => updateA()); await t1.finished; const t2 = document.startViewTransition(() => updateB()); await t2.finished; ``` ### Astro / Next 통합 ```astro --- // Astro import { ViewTransitions } from 'astro:transitions'; --- ... ``` ```tsx // Next.js — built-in router 가 자동 support (가까운 미래) // 현재 = unstable_viewTransition flag ``` ### iOS-style page transition ```css @view-transition { navigation: auto; } ::view-transition-old(root) { animation: 0.3s ease both ios-out; } ::view-transition-new(root) { animation: 0.3s ease both ios-in; } @keyframes ios-out { to { transform: translateX(-30%); opacity: 0.5; } } @keyframes ios-in { from { transform: translateX(100%); } } ``` → iOS Safari 같은 swipe-back 느낌. ## 🤔 의사결정 기준 | 상황 | 추천 | |---|---| | SPA navigation | startViewTransition | | MPA (Astro / 일반) | @view-transition + CSS | | Shared element (list → detail) | view-transition-name | | Theme switch | startViewTransition + clipPath | | 옛 browser 지원 | Framer Motion / CSS animation fallback | ## ❌ 안티패턴 - **prefers-reduced-motion 무시**: a11y / 멀미. - **너무 길은 transition (> 500ms)**: 사용자 답답. - **모든 page 가 같은 transition**: 의미 없음. - **Snapshot 큰 page + 매번**: 느림. - **Fallback 없음 옛 browser**: blank. - **Animation 중 사용자 click 무시**: skipTransition. - **Shared element name conflict**: 한 page 안 unique. ## 🤖 LLM 활용 힌트 - `document.startViewTransition` + `view-transition-name` 2종. - @view-transition CSS 가 MPA 자동. - iOS-like UX 한 줄로. - prefers-reduced-motion 항상. ## 🔗 관련 문서 - [[Frontend_Animation_Motion]] - [[Frontend_Progressive_Enhancement]] - [[React_TanStack_Router_Patterns]]