05 — View Transitions API (DOM 두 상태의 자동 보간)
한 줄 답: View Transitions API는 DOM의 변경 전후를 스냅샷으로 캡처하고, 그 두 이미지 사이를 브라우저가 자동 보간한다. 개발자는 바뀐 결과 DOM만 만들면 되고, 어떤 요소가 어떤 요소로 변화·이동·페이드 인/아웃할지를
view-transition-name한 줄로 선언한다. 2024년 Chrome은 Cross-Document 모드를 안착시켜, 풀 페이지 로드(MPA)에서도 SPA 같은 부드러운 전환이 가능해졌다.
Why — 왜 새 API가 필요했나
기존 SPA에서 목록 → 상세 페이지 전환을 부드럽게 하려면 다음을 직접 해야 했다.
- 현재 카드의 위치·크기 측정 (
getBoundingClientRect) - 라우터 변경 후 DOM이 그려진 직후 상세 페이지의 같은 영역 위치 측정
- 그 사이의 차이를
transform으로 흉내 내며transition(FLIP 패턴) - 다른 영역들(헤더·바디 텍스트 등)은 별도로 페이드·슬라이드
→ 한 화면당 100~200줄의 수작업 FLIP 코드. 라이브러리(Framer Motion layoutId, FLIP Toolkit, Shared Element Transitions)가 발달했으나, 프레임워크 종속·DOM 트리 깊이에 약함.
View Transitions는 이걸 브라우저 네이티브 + 선언적으로 바꾼다.
How — 어떻게 동작하는가
1) 4단계 메커니즘
핵심 발상은 FLIP의 브라우저화: F를 자동 캡처, L도 자동 캡처, I/P는 ::view-transition pseudo로 합성.
2) Same-Document (SPA) 모드
JavaScript 한 줄로 시작.
document.startViewTransition(() => {
/* DOM을 바꾸는 코드. 동기 또는 async. */
updateDOM();
});startViewTransition은:
- 현재 화면 스냅샷 캡처.
- 콜백 실행 (DOM이 바뀜).
- 새 화면 캡처.
- 두 스냅샷을 ::view-transition pseudo-element 트리로 합성해 애니메이션.
기본 동작: 전체 화면 크로스페이드 500ms.
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 500ms;
}3) view-transition-name — 특정 요소를 morph
기본은 전체 크로스페이드. 특정 요소를 연결하려면 양쪽 DOM에 같은 이름을 부여:
.card.featured {
view-transition-name: featured-card;
}
.detail .hero {
view-transition-name: featured-card; /* 같은 이름 */
}→ 브라우저는 *변환 전 .card.featured*와 *변환 후 .detail .hero*를 하나의 시각적 요소로 인식하고, 위치·크기·내용을 보간한다. 이게 모바일 OS의 Shared Element Transition과 동등.
제약:
- 한 시점에 같은 이름은 오직 하나의 요소에만 — 둘 이상이면 트랜지션 자체가 취소된다 (콘솔에 에러).
- 각 named 요소는 별도의 GPU 레이어로 분리된다 (composite-only).
4) Pseudo-element 트리
::view-transition
└── ::view-transition-group(name)
└── ::view-transition-image-pair(name)
├── ::view-transition-old(name)
└── ::view-transition-new(name)각 named 요소는 4겹 pseudo를 만든다. 그 위에서 CSS animation을 자유롭게 정의 가능.
::view-transition-old(featured-card) {
animation: fade-out 300ms forwards;
}
::view-transition-new(featured-card) {
animation: fade-in 300ms forwards;
}
::view-transition-group(featured-card) {
animation-duration: 400ms;
animation-timing-function: cubic-bezier(.2,.8,.2,1);
}5) Cross-Document (MPA, 2024년 Chrome 안착)
기존 한계: startViewTransition은 같은 문서 내에서만. 페이지 전체 로드에는 못 씀.
새 모드 — CSS만으로 활성화:
@view-transition {
navigation: auto; /* 또는 same-origin */
}이 한 줄로 같은 origin의 페이지 전환에 자동으로 View Transition이 적용된다. JavaScript 불필요.
/* product-list.html */
.product-card[data-id="42"] {
view-transition-name: product-42;
}
/* product-detail.html */
.product-hero {
view-transition-name: product-42;
}→ 사용자가 카드 클릭 → 새 페이지가 로드되는데, 동일 이름의 카드와 hero가 자연스럽게 morph. SSR/MPA가 SPA 같은 UX를 JS 없이 얻음.
Baseline: Chrome 126+ (2024-06). Safari 18+ 부분 지원 (Same-Doc). Firefox 작업 중.
6) @view-transition (CSS at-rule)
navigation 값:
auto— 모든 same-origin 네비게이션에 적용none— 비활성- (제안)
same-origin등 세부 제어
7) view-transition-class (2024 후반)
여러 요소에 공통 트랜지션 스타일을 부여하려면 이름이 아니라 클래스가 필요. view-transition-class가 그 역할.
.card { view-transition-class: card-morph; }
::view-transition-group(.card-morph) {
animation-duration: 300ms;
}What — 구체 활용 패턴
패턴 1: 모달 등장
function openModal() {
if (!document.startViewTransition) {
/* fallback */ modal.showModal();
return;
}
document.startViewTransition(() => {
modal.showModal();
});
}::view-transition-new(root) {
animation: fade-up 200ms cubic-bezier(.2,.8,.2,1);
}
@keyframes fade-up {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}패턴 2: 리스트 → 상세 (Hero shared element)
{/* list.html */}
<a href="/product/42">
<img class="cover" src="..." style="view-transition-name: cover-42">
<h3>제목</h3>
</a>
{/* detail.html */}
<img class="hero" src="..." style="view-transition-name: cover-42">이미지가 목록 카드 → 상세 hero로 부드럽게 확대·이동. 모바일 앱의 공유 요소 전환과 동등한 UX.
패턴 3: Theme 전환 (light → dark)
function toggleTheme() {
document.startViewTransition(() => {
document.documentElement.dataset.theme =
document.documentElement.dataset.theme === 'dark' ? 'light' : 'dark';
});
}→ 전체 화면이 부드러운 크로스페이드로 다크모드 전환. 한 줄로.
패턴 4: List reorder (drag-to-sort 후)
async function reorder(newOrder) {
await document.startViewTransition(() => {
list.replaceChildren(...newOrder); // 카드 순서 변경
}).finished;
}각 카드에 *고유 view-transition-name*을 주면 브라우저가 자동 FLIP. 라이브러리 없이.
패턴 5: Skipping during back/forward (MPA)
브라우저 뒤로가기 시 역방향 전환이 자동 생성된다. navigation.transition 객체로 방향 감지:
window.addEventListener('pageswap', e => {
if (e.viewTransition && navigation.activation.from) {
/* 어디서 왔는지에 따라 다른 애니메이션 */
}
});이벤트:
pageswap— 떠나기 직전pagereveal— 도착 직후
What-if — 잘못 쓰면
1) view-transition-name을 여러 요소에 동일하게
.card { view-transition-name: card; } /* ❌ 모든 카드가 같은 이름 */→ 콘솔에 “Duplicate view-transition-name” 에러, 트랜지션 전체 취소. 항상 고유한 ID가 포함된 이름:
.card[data-id="42"] { view-transition-name: card-42; }2) 트랜지션 중 사용자 인터랙션 무시
기본적으로 트랜지션 중에는 입력이 일시 정지. 길게 잡으면 사용자가 앱이 멈춘 줄 안다. 권장: 200~300ms.
3) view-transition-name을 transform된 요소에
요소가 이미 3D transform 컨텍스트 안에 있으면, View Transition은 2D 스냅샷을 캡처하므로 깊이 정보가 사라진다. 표 형식 결과는 flat. 의도와 다른 결과.
4) 너무 많은 named 요소
각 named 요소는 별도 GPU 레이어. 100개 카드 모두에 이름 → 모바일 GPU 메모리 초과. 권장: 변할 가능성이 높은 핵심 요소 1~5개만.
5) Cross-doc에서 두 페이지의 toolchain 차이
view-transition-name이 같아도 대상 요소가 다른 컴포넌트로 렌더링되면(예: list는 <img>, detail은 <picture>) → 시각적으로 어색한 morph. 두 페이지의 해당 요소를 의도적으로 동일 박스 모델로 맞춰야 한다.
6) prefers-reduced-motion 미고려
View Transitions도 모션이다. 흔들림 거부 사용자에게는 즉시 전환으로:
@media (prefers-reduced-motion: reduce) {
::view-transition-group(*),
::view-transition-old(*),
::view-transition-new(*) {
animation: none !important;
}
}자세한 건 07-accessibility.
Insight — 한 단락 이야기
“View Transitions는 iOS Shared Element Transition을 웹으로 옮긴 9년의 결과다.”
2014년 iOS 7의 카드 전환이 도입됐을 때, 웹 개발자들은 *“왜 우리는 이걸 못 만들지?”*를 물었다. 답은 FLIP이었지만 한계가 분명했다 — DOM 트리가 깊으면 깨졌고, 라우터 전환 시 언마운트와 마운트 사이의 빈 프레임이 발생했다. 2018년 Google의 Jake Archibald와 Shu-yu Guo가 *“브라우저가 두 DOM 상태를 직접 캡처하면 어떨까”*를 제안했다 (당시 이름 Same-Document Transitions). 2022년 Chrome 111에서 Same-Doc 안착, 2024년 Chrome 126에서 드디어 Cross-Doc(MPA). 그 사이 9년. 핵심 통찰은 단순하다 — FLIP이 어려운 이유는 First/Last를 동기화하는 데 있고, 그건 브라우저만이 100% 정확히 안다. CSS의
view-transition-name은 어떤 요소가 같은 요소인가에 대한 선언이고, 나머지는 브라우저 몫이다. 선언적 UI의 정신을 모션에 적용한 첫 큰 사례다.
요약 + Mermaid
- View Transitions = DOM 두 상태의 자동 보간.
- Same-Doc (SPA):
document.startViewTransition(cb)로 SPA 전환을 FLIP 없이 자동화. - Cross-Doc (MPA, 2024 Chrome):
@view-transition { navigation: auto }한 줄로 풀 페이지 로드에도 적용. view-transition-name: 같은 이름의 두 요소는 하나로 morph. 고유성 필수.- Pseudo-element 트리:
::view-transition-{old,new,group,image-pair}(name)으로 각 단계 커스텀. - Composite-only로 합성됨 → 60fps 친화적.
- 200~300ms 권장. 더 길면 사용자가 멈춘 줄 안다.
prefers-reduced-motion의무 처리.
다음: 06-scroll-driven — 시간조차 스크롤로 대체하는 새로운 트리거.