[G1-Sync] Manual knowledge update
This commit is contained in:
@@ -0,0 +1,362 @@
|
||||
---
|
||||
id: web-view-transitions-cross-doc
|
||||
title: View Transitions API — same-doc / cross-doc
|
||||
category: Coding
|
||||
status: draft
|
||||
source_trust_level: B
|
||||
verification_status: conceptual
|
||||
created_at: 2026-05-09
|
||||
updated_at: 2026-05-09
|
||||
tags: [web, animation, vibe-coding]
|
||||
tech_stack: { language: "TS / CSS", applicable_to: ["Frontend"] }
|
||||
applied_in: []
|
||||
aliases: [View Transitions, startViewTransition, view-transition-name, cross-doc, MPA transition, navigation API]
|
||||
---
|
||||
|
||||
# View Transitions API
|
||||
|
||||
> SPA 의 fancy transition 가 표준 web. **Same-doc (SPA) 과 cross-doc (MPA) 둘 다**. Browser 가 magic.
|
||||
|
||||
## 📖 핵심 개념
|
||||
- 두 state snapshot.
|
||||
- 매칭된 element 가 morph.
|
||||
- CSS animation 가 매 transition pair.
|
||||
- Native browser support.
|
||||
|
||||
## 💻 코드 패턴
|
||||
|
||||
### Same-doc (SPA)
|
||||
```ts
|
||||
async function navigate(newContent) {
|
||||
if (!document.startViewTransition) {
|
||||
// Fallback
|
||||
document.body.innerHTML = newContent;
|
||||
return;
|
||||
}
|
||||
|
||||
const transition = document.startViewTransition(() => {
|
||||
document.body.innerHTML = newContent;
|
||||
});
|
||||
|
||||
await transition.finished;
|
||||
}
|
||||
```
|
||||
|
||||
→ Browser 가 자동:
|
||||
1. 현재 snapshot.
|
||||
2. Callback 실행.
|
||||
3. 새 snapshot.
|
||||
4. Crossfade (default).
|
||||
|
||||
### CSS (default)
|
||||
```css
|
||||
::view-transition-old(root) {
|
||||
animation: 0.3s fade-out;
|
||||
}
|
||||
::view-transition-new(root) {
|
||||
animation: 0.3s fade-in;
|
||||
}
|
||||
```
|
||||
|
||||
→ `root` = 페이지 전체.
|
||||
|
||||
### 매칭된 element (morph)
|
||||
```css
|
||||
.thumbnail {
|
||||
view-transition-name: hero-img;
|
||||
}
|
||||
|
||||
/* Detail page */
|
||||
.full-image {
|
||||
view-transition-name: hero-img;
|
||||
}
|
||||
```
|
||||
|
||||
→ Same name = morph (Hero animation).
|
||||
|
||||
### Custom CSS animation
|
||||
```css
|
||||
@keyframes slide-in {
|
||||
from { transform: translateX(100%); }
|
||||
}
|
||||
@keyframes slide-out {
|
||||
to { transform: translateX(-100%); }
|
||||
}
|
||||
|
||||
::view-transition-old(root) {
|
||||
animation: 0.3s slide-out;
|
||||
}
|
||||
::view-transition-new(root) {
|
||||
animation: 0.3s slide-in;
|
||||
}
|
||||
```
|
||||
|
||||
### Cross-doc (MPA)
|
||||
```html
|
||||
<!-- old.html, new.html — 둘 다 -->
|
||||
<meta name="view-transition" content="same-origin">
|
||||
```
|
||||
|
||||
```css
|
||||
@view-transition {
|
||||
navigation: auto;
|
||||
}
|
||||
```
|
||||
|
||||
→ Anchor click → 새 page 로 transition (MPA 도).
|
||||
|
||||
→ 2024 Chrome 126+ 지원.
|
||||
|
||||
### Astro / Next 통합
|
||||
```astro
|
||||
---
|
||||
// Astro view transitions
|
||||
import { ViewTransitions } from 'astro:transitions';
|
||||
---
|
||||
<head>
|
||||
<ViewTransitions />
|
||||
</head>
|
||||
```
|
||||
|
||||
```tsx
|
||||
// Next.js (App Router)
|
||||
// Layout 에 추가.
|
||||
<head>
|
||||
<meta name="view-transition" content="same-origin" />
|
||||
</head>
|
||||
```
|
||||
|
||||
→ MPA 가 SPA 같은 UX.
|
||||
|
||||
### TanStack Router / React Router
|
||||
```ts
|
||||
// Manually trigger
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleClick = async () => {
|
||||
if (document.startViewTransition) {
|
||||
document.startViewTransition(() => {
|
||||
flushSync(() => navigate('/profile'));
|
||||
});
|
||||
} else {
|
||||
navigate('/profile');
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### React 19 + view transitions
|
||||
```tsx
|
||||
import { useViewTransitionState } from 'react-router';
|
||||
|
||||
const isTransitioning = useViewTransitionState('/profile');
|
||||
// → CSS class 추가 가능
|
||||
```
|
||||
|
||||
### Skip transition (specific case)
|
||||
```ts
|
||||
const transition = document.startViewTransition(() => render(newState));
|
||||
|
||||
if (!important) {
|
||||
transition.skipTransition(); // 즉시 끝남
|
||||
}
|
||||
```
|
||||
|
||||
### State-based animation
|
||||
```ts
|
||||
// List item delete + re-render
|
||||
async function deleteItem(id) {
|
||||
document.startViewTransition(() => {
|
||||
items = items.filter(i => i.id !== id);
|
||||
rerender();
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
→ List item 가 자연 animate.
|
||||
|
||||
### Dynamic name
|
||||
```ts
|
||||
// 매 element 가 unique name
|
||||
listItems.forEach((item, i) => {
|
||||
item.style.viewTransitionName = `item-${item.id}`;
|
||||
});
|
||||
```
|
||||
|
||||
### 함정: 같은 name 두 element
|
||||
```css
|
||||
.a, .b { view-transition-name: same; }
|
||||
/* → conflict, transition 깨짐 */
|
||||
```
|
||||
|
||||
### Old / new pseudo-element
|
||||
```css
|
||||
::view-transition /* root */
|
||||
::view-transition-image-pair(name) /* container */
|
||||
::view-transition-old(name)
|
||||
::view-transition-new(name)
|
||||
```
|
||||
|
||||
→ Animatable.
|
||||
|
||||
### Group, image-pair, old, new
|
||||
```
|
||||
::view-transition-group(name) — wrapper, position
|
||||
::view-transition-image-pair(name) — old + new layered
|
||||
::view-transition-old(name) — outgoing
|
||||
::view-transition-new(name) — incoming
|
||||
```
|
||||
|
||||
### Nested transitions
|
||||
```ts
|
||||
document.startViewTransition(async () => {
|
||||
await updateA();
|
||||
await updateB();
|
||||
});
|
||||
```
|
||||
|
||||
→ Promise resolve = transition 끝.
|
||||
|
||||
### Async render
|
||||
```ts
|
||||
document.startViewTransition(async () => {
|
||||
const data = await fetch('/data').then(r => r.json());
|
||||
render(data);
|
||||
});
|
||||
```
|
||||
|
||||
→ Network 동안 사용자 가 "frozen" 안 보고 (snapshot 후 wait).
|
||||
|
||||
→ 너무 느림 = bad UX.
|
||||
|
||||
### Reduce motion (a11y)
|
||||
```css
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
::view-transition-old(*),
|
||||
::view-transition-new(*) {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Browser support
|
||||
```
|
||||
- Chrome 111+ (same-doc)
|
||||
- Chrome 126+ (cross-doc)
|
||||
- Edge 동일
|
||||
- Firefox: 안 (no plan)
|
||||
- Safari: 진행 중
|
||||
|
||||
→ Progressive enhancement. Fallback graceful.
|
||||
```
|
||||
|
||||
### Use case
|
||||
```
|
||||
- Hero animation (list → detail)
|
||||
- Tab switch (smooth)
|
||||
- Modal open/close
|
||||
- Navigation (page change)
|
||||
- Theme toggle (color smooth)
|
||||
- Filter / sort animation
|
||||
```
|
||||
|
||||
### Theme toggle
|
||||
```ts
|
||||
async function toggleTheme() {
|
||||
if (!document.startViewTransition) return setTheme();
|
||||
|
||||
const transition = document.startViewTransition(setTheme);
|
||||
await transition.ready;
|
||||
|
||||
document.documentElement.animate(
|
||||
{ clipPath: ['circle(0% at 0 0)', 'circle(150% at 0 0)'] },
|
||||
{ duration: 500, pseudoElement: '::view-transition-new(root)' }
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
→ Circle reveal — Twitter style.
|
||||
|
||||
### Performance
|
||||
```
|
||||
- 매 transition = 2 snapshot (hardware accelerated)
|
||||
- 큰 page 가 느림 (snapshot cost)
|
||||
- 매우 많은 element name = 폭발
|
||||
|
||||
→ Hero element 만 name. 나머지 = root crossfade.
|
||||
```
|
||||
|
||||
### vs Framer Motion / GSAP
|
||||
```
|
||||
Library:
|
||||
- Cross-browser
|
||||
- 정밀 control
|
||||
- 큰 bundle
|
||||
|
||||
Native View Transitions:
|
||||
- 0 bundle
|
||||
- Browser-native
|
||||
- 모든 element 가 native
|
||||
|
||||
→ Native 가 default. Library 가 specific 정밀.
|
||||
```
|
||||
|
||||
### Animation API combine
|
||||
```css
|
||||
.box {
|
||||
view-transition-name: my-box;
|
||||
}
|
||||
|
||||
::view-transition-old(my-box) {
|
||||
animation: scale-down 0.3s ease, fade-out 0.3s;
|
||||
}
|
||||
```
|
||||
|
||||
→ 다중 animation property.
|
||||
|
||||
### Debugging
|
||||
```
|
||||
Chrome DevTools:
|
||||
- Animations panel.
|
||||
- 매 transition 가 timeline.
|
||||
- Pause / step.
|
||||
```
|
||||
|
||||
### Production tips
|
||||
```
|
||||
1. Hero element 만 name (모든 element X).
|
||||
2. Reduce motion 항상 존중.
|
||||
3. Async render < 500ms (frozen 시간 길면 bad).
|
||||
4. Fallback 가 있어야 (Firefox).
|
||||
5. Cross-doc 가 Chrome 126+ 만.
|
||||
```
|
||||
|
||||
## 🤔 의사결정 기준
|
||||
| 작업 | 추천 |
|
||||
|---|---|
|
||||
| List → detail morph | view-transition-name |
|
||||
| Page navigation MPA | Cross-doc transition |
|
||||
| Page navigation SPA | startViewTransition |
|
||||
| Theme toggle | Custom + clip-path |
|
||||
| 작은 element animate | Native CSS animation |
|
||||
| Cross-browser | Framer Motion |
|
||||
| Modal | View Transitions |
|
||||
| 정밀 timeline | GSAP |
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **같은 name 여러 element**: conflict.
|
||||
- **Async 가 길음 (>1s)**: frozen UX.
|
||||
- **모든 element 가 name**: 성능.
|
||||
- **Fallback 없음**: Firefox 깨짐.
|
||||
- **Reduce motion 무시**: a11y.
|
||||
- **Cross-doc + 다른 origin**: 안 됨.
|
||||
|
||||
## 🤖 LLM 활용 힌트
|
||||
- View Transitions = native + CSS-driven.
|
||||
- Same-doc (Chrome 111+) / Cross-doc (Chrome 126+).
|
||||
- view-transition-name 가 morph.
|
||||
- Astro / Next 가 자체 wrapper.
|
||||
|
||||
## 🔗 관련 문서
|
||||
- [[Frontend_View_Transitions_Deep]]
|
||||
- [[Frontend_Animation_Motion]]
|
||||
- [[Web_History_API_Routing]]
|
||||
Reference in New Issue
Block a user