Files
2nd/10_Wiki/Topics/Coding/Frontend_View_Transitions_Deep.md
T
2026-05-09 21:08:02 +09:00

7.2 KiB

id, title, category, status, source_trust_level, verification_status, created_at, updated_at, tags, tech_stack, applied_in, aliases
id title category status source_trust_level verification_status created_at updated_at tags tech_stack applied_in aliases
frontend-view-transitions-deep View Transitions API — 페이지 / 요소 transition Coding draft B conceptual 2026-05-09 2026-05-09
frontend
view-transitions
animation
vibe-coding
language applicable_to
TS / CSS
Frontend
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)

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;
}
/* Default cross-fade */
::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: 0.3s;
}

CSS-only fade

@view-transition {
  navigation: auto;
}

→ 모든 navigation 자동 fade. Chrome 126+.

Element morph (shared element)

<!-- Page A -->
<img src="thumbnail.jpg" style="view-transition-name: hero" />

<!-- Page B -->
<img src="full.jpg" style="view-transition-name: hero" />

→ 같은 name = 자동 morph (size, position).

::view-transition-group(hero) {
  animation-duration: 0.5s;
}

React Router 7 / Next 통합

// React Router 7
<Link to="/about" viewTransition>About</Link>

// Next.js (next/link)
import Link from 'next/link';
<Link href="/about" prefetch>About</Link>
// + Chrome flag 또는 view-transition CSS

Custom transition

/* 슬라이드 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

<!-- List → detail 화면 -->
<!-- List item -->
<article style={{ viewTransitionName: `card-${id}` }}>
  <img style={{ viewTransitionName: `image-${id}` }} />
  <h3 style={{ viewTransitionName: `title-${id}` }}>...</h3>
</article>

<!-- Detail page -->
<article style={{ viewTransitionName: `card-${id}` }}>
  <img style={{ viewTransitionName: `image-${id}` }} />
  <h1 style={{ viewTransitionName: `title-${id}` }}>...</h1>
</article>

→ 클릭 시 → 자동 morph. iOS-like UX.

Direction-based (back / forward)

const t = document.startViewTransition(async () => {
  await router.go(url);
});

if (direction === 'back') {
  document.documentElement.classList.add('back');
}
.back::view-transition-old(root) { animation: slide-out-right; }
.back::view-transition-new(root) { animation: slide-in-left; }

Cross-document (MPA, Chrome 126+)

@view-transition {
  navigation: auto;
}
<!-- 매 page 의 hero element 가 같은 name -->
<img class="hero" src="..." style="view-transition-name: hero" />

→ Page 간 이동 자동 morph. SPA framework 없이 OK.

Fallback 처리

async function navigate(url: string) {
  if (!('startViewTransition' in document)) {
    return route(url);  // 즉시
  }
  
  document.startViewTransition(() => route(url));
}

Reduce motion

@media (prefers-reduced-motion: reduce) {
  ::view-transition-old(root),
  ::view-transition-new(root) {
    animation: none;
  }
}

Promise / async

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 예

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)

const t = document.startViewTransition(() => update());

// 사용자가 다른 거 누름 — 즉시 끝
button.onclick = () => t.skipTransition();

단일 element transition

// Modal 열기
async function openModal() {
  document.startViewTransition(() => {
    modalEl.style.viewTransitionName = 'modal';
    modalEl.classList.add('open');
  });
}
::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

const t1 = document.startViewTransition(() => updateA());
await t1.finished;

const t2 = document.startViewTransition(() => updateB());
await t2.finished;

Astro / Next 통합

---
// Astro
import { ViewTransitions } from 'astro:transitions';
---
<html>
  <head>
    <ViewTransitions />
  </head>
  <body>...</body>
</html>
// Next.js — built-in router 가 자동 support (가까운 미래)
// 현재 = unstable_viewTransition flag

iOS-style page transition

@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 항상.

🔗 관련 문서