[G1-Sync] Manual knowledge update
This commit is contained in:
@@ -0,0 +1,330 @@
|
||||
---
|
||||
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
|
||||
<!-- 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).
|
||||
|
||||
```css
|
||||
::view-transition-group(hero) {
|
||||
animation-duration: 0.5s;
|
||||
}
|
||||
```
|
||||
|
||||
### React Router 7 / Next 통합
|
||||
```tsx
|
||||
// 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
|
||||
```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
|
||||
<!-- 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)
|
||||
```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 의 hero element 가 같은 name -->
|
||||
<img class="hero" src="..." style="view-transition-name: hero" />
|
||||
```
|
||||
|
||||
→ 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';
|
||||
---
|
||||
<html>
|
||||
<head>
|
||||
<ViewTransitions />
|
||||
</head>
|
||||
<body>...</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
```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]]
|
||||
Reference in New Issue
Block a user