깜빡임 없는 페이지 전환
페이지 이동할 때마다 화면이 뚝 끊기죠. 브라우저 API 하나면 매끄럽게 이어줄 수 있어요.
SPA에서 라우트를 바꿀 때마다 화면이 뚝 끊기는 거, 다들 한 번쯤 겪어봤을 거예요. DOM을 통째로 갈아치우니까 이전 화면이 사라지고 다음 화면이 뜨는 사이에 빈 프레임이 끼어들죠. 이걸 매끄럽게 만들려고 old 요소를 페이드아웃 시키고, new 요소를 페이드인 시키고, 그 사이에 사용자 클릭도 막아야 하고... 직접 해보면 생각보다 손이 많이 갑니다.
View Transitions API는 이 과정을 브라우저에 맡기자는 아이디어에서 시작됐어요.
화면이 "뚝" 끊기는 순간
SPA 프레임워크는 라우트 전환 시 DOM을 교체합니다. React라면 컴포넌트 트리가 통째로 언마운트되고 새 트리가 마운트되죠. 이 교체가 한 프레임 안에 끝나면 좋겠지만, 실제로는 이전 DOM이 사라진 뒤 다음 DOM이 그려지기까지 깜빡이는 순간이 생겨요.
직접 전환 애니메이션을 만들려면 신경 쓸 게 많습니다. old 콘텐츠와 new 콘텐츠의 위치를 동시에 관리해야 하고, 전환 도중 포커스와 스크린 리더 접근성을 처리해야 하고, 사용자 인터랙션도 적절히 차단해야 해요. 이런 문제를 API 레벨에서 풀어주는 게 View Transitions API입니다.
한 줄로 시작하는 전환
가장 단순한 사용법은 document.startViewTransition()을 호출하는 것뿐이에요.
콜백 안에서 DOM을 업데이트하면, 브라우저가 알아서 이전 화면과 다음 화면 사이에 cross-fade 애니메이션을 넣어줍니다. 기본 전환 시간은 250ms예요. document.startViewTransition이 지원되지 않는 브라우저에서는 그냥 콜백만 실행하면 되니까, feature detection도 간단하죠.
브라우저가 전환을 만드는 순서
겉으로는 한 줄이지만, 브라우저 안에서는 꽤 정교한 과정이 돌아가고 있어요.
Old state 캡처
startViewTransition()이 호출되면, 브라우저가 현재 화면의 스냅샷을 찍습니다. 스크린샷처럼 정적 이미지로 저장해두는 거예요.
DOM 업데이트
콜백 함수가 실행되면서 DOM이 바뀝니다. 이 시점에 렌더링은 억제되어 있어서 사용자 눈에는 아직 이전 화면이 보여요.
New state 캡처
DOM 업데이트가 끝나면 브라우저가 새 화면의 스냅샷도 찍습니다.
의사 요소 트리 생성
::view-transition-old(root)에 이전 스냅샷을, ::view-transition-new(root)에 새 스냅샷을 넣은 의사 요소 트리가 문서 위에 올라갑니다.
애니메이션 실행
old 스냅샷은 opacity 1에서 0으로, new 스냅샷은 0에서 1로. 250ms 동안 cross-fade가 진행됩니다.
여기서 눈여겨볼 건, 전환 중에 실제 DOM이 아니라 스냅샷 이미지 위에서 애니메이션이 돈다는 점이에요. 렌더링 파이프라인을 다룬 글에서 합성(composite) 단계가 빠른 이유를 봤죠. View Transitions도 같은 원리입니다. 스냅샷을 GPU 레이어로 올려서 합성만으로 전환을 처리하니까, 메인 스레드를 거의 건드리지 않아요.
그리고 핵심 설계 원칙이 하나 있어요.
"A failure to create a visual transition never prevents the state change." - CSS View Transitions Level 1
전환 애니메이션이 어떤 이유로 실패해도, DOM 업데이트 자체는 반드시 실행돼요. 전환은 장식이지 기능이 아니라는 원칙입니다.
이 덕분에 "지원 안 되는 브라우저에서 깨지면 어쩌지?"라는 걱정을 할 필요가 없습니다. 전환이 안 되면 그냥 이전처럼 즉시 교체될 뿐이거든요.
요소별로 다르게 움직이기
기본 cross-fade만으로도 꽤 매끄럽지만, 특정 요소를 따로 움직이고 싶을 때가 있어요. 카드 목록에서 카드를 클릭하면 그 카드의 이미지가 상세 페이지 상단으로 슬라이드되는 전환 같은 거요.
이때 쓰는 게 view-transition-name이에요.
.card-image {
view-transition-name: hero-image;
}
.detail-header-image {
view-transition-name: hero-image;
}같은 이름을 old 화면의 요소와 new 화면의 요소에 부여하면, 브라우저가 둘을 짝지어서 위치와 크기를 부드럽게 보간해줍니다. 전체 페이지는 cross-fade로 전환되면서, hero-image만 슬라이드로 이동하는 효과가 만들어지죠.
카드 목록에서 카드를 클릭하면 썸네일이 상세 영역으로 이동하는 전환을 직접 확인해보세요.
각 카드에 card-1, card-2, card-3처럼 고유한 view-transition-name을 부여했어요. 상세 화면에서도 선택된 카드와 같은 이름을 쓰니까, 브라우저가 둘을 짝지어 위치와 크기를 보간합니다.
의사 요소 트리를 CSS로 커스터마이징할 수도 있습니다.
::view-transition-old(hero-image) {
animation: none;
}
::view-transition-new(hero-image) {
animation: slide-in 300ms ease-out;
}
@keyframes slide-in {
from {
opacity: 0;
transform: translateY(20px);
}
}::view-transition-old()와 ::view-transition-new() 의사 요소에 직접 애니메이션을 걸 수 있어요. 기본 cross-fade를 끄고 커스텀 키프레임을 적용하면 됩니다.
셀렉트 박스에서 효과를 바꿔가며 같은 전환에 다른 애니메이션을 적용해보세요.
document.documentElement.dataset.transition에 효과 이름을 넣고, CSS에서 [data-transition="slide-left"]::view-transition-old(root) 같은 선택자로 분기하는 거예요. 하나의 API로 전환 효과를 자유롭게 바꿀 수 있죠.
안전하게 도입하기
View Transitions API는 progressive enhancement에 최적화되어 있어요. 지원하지 않는 브라우저에서는 document.startViewTransition이 undefined니까, 한 줄 체크만 추가하면 돼요.
function navigate(to) {
if (!document.startViewTransition) {
updateDOM(to);
return;
}
document.startViewTransition(() => updateDOM(to));
}SPA(same-document) 전환은 현재 Chrome 111+, Edge 111+, Firefox 144+, Safari 18+에서 지원됩니다. MPA(cross-document) 전환도 @view-transition { navigation: auto; } CSS at-rule로 활성화할 수 있는데, 아직 Firefox에서 미지원이라 SPA 전환부터 도입하는 게 안전해요.
ViewTransition 객체의 ready promise를 쓰면 의사 요소가 생성된 시점을 잡을 수 있어요. Web Animations API와 조합하면 JavaScript에서 전환 애니메이션을 세밀하게 제어할 수 있습니다. finished promise는 전환이 완전히 끝난 후 정리 작업에 유용하고요.
skipTransition()은 애니메이션만 즉시 종료하고 DOM 변경은 유지해요. 사용자가 빠르게 연속 네비게이션할 때, 이전 전환을 건너뛰고 마지막 전환만 보여주는 데 쓸 수 있습니다.
버튼을 빠르게 연타해보세요. 이전 전환이 skip되고 마지막 상태로 바로 넘어가는 걸 로그에서 확인할 수 있어요.
천천히 클릭하면 숫자가 scale 애니메이션과 함께 전환되고, 빠르게 연타하면 skipTransition()이 이전 전환을 즉시 끝내서 끊김 없이 다음 전환으로 넘어갑니다.
현재 스펙은 Candidate Recommendation Draft 단계라서 큰 틀은 확정됐어요. 브라우저 합성 레이어가 전환 성능의 핵심인 만큼, will-change 속성이 레이어에 미치는 영향도 함께 이해해두면 도움이 됩니다.