문서 간 뷰 전환 (Cross-Document View Transitions): 아무도 언급하지 않는 주의사항
요약
멀티 페이지 애플리케이션(MPA)에서 네이티브 앱 같은 전환 효과를 제공하는 '문서 간 뷰 전환(Cross-Document View Transitions)' 구현 시 겪을 수 있는 흔한 오류와 주의사항을 다룹니다. 폐기된 메타 태그 방식과 단일 문서용 JavaScript API를 혼동하여 발생하는 문제점을 지적하며, 올바른 구현을 위한 개념적 차이를 설명합니다.
핵심 포인트
- 기존의 `<meta name="view-transition">` 태그 방식은 폐기(Deprecated)되었으므로 사용 시 주의가 필요함
- 단일 문서 내의 `document.startViewTransition()` API와 문서 간 전환(Cross-Document)은 서로 다른 메커니즘임
- 인터넷상의 많은 튜토리얼이 업데이트되지 않은 구식 스펙이나 잘못된 문맥의 정보를 제공하고 있음
- MPA에서 매끄러운 전환을 구현하려면 SPA 방식의 멘탈 모델이 아닌 문서 간 전환 전용 스펙을 이해해야 함
나는 이 문제로 토요일 하루를 통째로 날려버렸다.
그냥 게으른 토요일이 아니라, 드물게 시간을 따로 내어 “드디어 저걸 만들어보겠어”라고 다짐한 그런 토요일이었다. 나는 Jake Archibald의 데모를 보았고, Chrome Dev Summit의 강연도 시청했다. 문서 간 뷰 전환 (Cross-Document View Transitions)이 실제로 가능하다는 것, 즉 프레임워크를 단 하나도 사용하지 않고도 평범한 멀티 페이지 사이트에서 매끄럽고 네이티브 앱 같은 페이지 전환 효과를 얻을 수 있다는 사실을 알고 있었다. React도 필요 없다. Astro도 필요 없다. 멀티 페이지 애플리케이션 (MPA)이 싱글 페이지 애플리케이션 (SPA)인 척 속이는 클라이언트 사이드 라우터 (client-side router)도 필요 없다. 그저 HTML 페이지들이 다른 HTML 페이지로 링크를 걸고, 브라우저가 그 사이의 애니메이션을 처리하기만 하면 된다. 정말 최고다.
그래서 나는 구축을 시작했다. 그런데 아무것도 작동하지 않았다.
내가 처음 발견한 튜토리얼에서는 <head> 안에 <meta name="view-transition" content="same-origin">를 넣으라고 했다.
충분히 간단해 보였다. 두 페이지 모두에 이를 추가하고 링크를 클릭했지만... 아무 일도 일어나지 않았다. 전환도 없었고, 에러도 없었다. 그저 2004년처럼 평범하고 즉각적인 페이지 로딩만 일어날 뿐이었다. DevTools를 열어 구문을 재확인하고, 서버를 재시작하고, Chrome Canary를 시도해보고, 캐시를 삭제해 보았다. 아무것도 안 됐다. 나는 그 시점에서 자존심 있는 개발자라면 누구나 하는 행동을 했다. 블로그 포스트의 코드를 한 글자 한 글자 그대로 복사해서 붙여넣었다. 여전히 아무 일도 없었다.
나는 내가 바보라고 확신하며 두 시간을 보냈다.
알고 보니 그 <meta> 태그 구문은? 폐기(Deprecated)되었다. 사라진 것이다. Chrome이 이를 출시했다가 CSS 기반의 옵트인 (opt-in) 방식으로 교체했는데, 인터넷 튜토리얼의 절반은 여전히 옛날 방식을 보여주고 있었다. 그 오래된 블로그 포스트들은 여전히 검색 순위가 높다. 권위 있어 보인다. 그리고 지금은 그냥 틀렸다. 저자들이 실력이 없어서 틀린 것이 아니라, 스펙 (spec)이 모두의 발밑에서 바뀌어 버렸는데 아무도 포스트를 업데이트하러 돌아오지 않았기 때문에 틀린 것이다.
내가 찾은 나머지 절반의 튜토리얼은 동일 문서 뷰 전환 (same-document view transitions)에 관한 것이었다. SPA 관련 내용 말이다. document.startViewTransition() 같은 것들 말이다.
직접 DOM 콘텐츠를 교체할 때 JavaScript에서 호출되는 방식인데, 이는 멋지고 유용하지만 실제로 구현하려고 앉아보면 완전히 다른 기능입니다. API 표면(API surface)이 다릅니다. 멘탈 모델(Mental model)이 다릅니다. 주의사항(Gotchas) 또한 매우 다릅니다. 그런데도 Google에서 “view transitions tutorial”을 검색하면, 세 단락 정도 읽기 전까지는 자신이 어떤 종류의 설명을 읽고 있는지 파악하기가 매우 어렵습니다.
따라서 여러분이 이 글을 보고 있다면, 아마도 이런 과정을 한 번쯤 겪어보셨을 것이라 추측합니다. 메타 태그(meta tag)를 시도해 보았지만 작동하지 않았을 것입니다. 실제 멀티 페이지 사이트(multi-page site)에서 JavaScript API를 시도해 보았지만, 이것이 단일 문서(single document) 내에서만 실행된다는 사실을 깨달았을 것입니다. 데모에서는 절반 정도 작동하는 것을 보았을지도 모르지만, 실제 콘텐츠를 추가하는 순간 무너져 내렸을 것입니다. 이미지가 이상하게 늘어나거나, 이유도 모른 채 전환(transition)이 몇 초 동안 멈춰 있거나, 혹은 40개의 상품 카드가 격자로 배치되어 있어 CSS 파일이 200줄의 view-transition-name 선언문으로 변해버리는 식 말입니다. 여러분은 스스로를 탓했을 것입니다. 하지만 여러분의 잘못이 아닙니다. 이 기능에 관한 문서 생태계는 현재 엉망이며, 명세(spec) 또한 계속해서 변하고 있는 목표물(moving target)이기 때문입니다.
이 글은 2부작 시리즈 중 제1부이며, 제가 그 토요일에 간절히 찾았던 바로 그 글입니다. 우리는 먼저 (메타 태그나 JavaScript가 아닌) CSS에서 @view-transition을 통해 현재 실제로 옵트인(opt in)하는 방법을 다룰 것입니다. 그다음, 느린 페이지에서 전환을 소리 없이 종료시켜 버리는 4초 타임아웃(4-second timeout)과 이를 디버깅하는 방법을 파헤칠 것입니다. 이어서 모든 이미지 중심의 전환을 마치 유령의 집 거울처럼 보이게 만드는 종횡비 왜곡(aspect ratio warping) 문제를 해결하고, 마지막으로 전체 라이프사이클(lifecycle)에 대한 프로그래밍 방식의 제어권을 제공하는 pagereveal 및 pageswap 이벤트를 제대로 다룰 것입니다.
제2부에서는 확장성 문제(scaling problem)를 다룰 것입니다. 즉, 스타일시트가 재앙이 되지 않으면서 수십 또는 수백 개의 요소에 걸쳐 view-transition-name을 처리하는 방법, view-transition-name과 view-transition-class의 차이점, 적시 명명 패턴(just-in-time naming patterns), 그리고 prefers-reduced-motion을 올바르게 적용하는 방법을 살펴볼 것입니다.
커피를 준비하세요. 한 잔 더 리필할지도 모릅니다. 이번 내용은 내용이 방대하며 여러분의 시간을 낭비하지 않기 위해 핵심만 다루겠지만, 다뤄야 할 범위가 넓고 그 어떤 것도 당연하게 느껴지지 않을 것입니다.
구식 방식은 끝났습니다
<!-- 이것은 더 이상 권장되지 않습니다(DEPRECATED) - 오래된 튜토리얼에서 이를 복사하지 마세요 -->
<meta name="view-transition" content="same-origin">
/* 이것이 현재의 선택적 적용(opt-in) 방식입니다 - CSS에 작성합니다 */
@view-transition {
navigation: auto;
...
최소한의 설정은 다음과 같습니다. 두 개의 HTML 파일과 각 파일에 적용된 하나의 CSS 규칙입니다. 2026년 기준으로, 문서 간 뷰 전환 (Cross-document view transitions)은 Chromium 기반 브라우저와 Safari 18.2+ 버전에서 지원됩니다. 제가 이 글을 쓰는 시점에서 Firefox 지원은 진행 중입니다.
그게 전부입니다. 두 개의 HTML 파일, 각 파일에 하나의 CSS 규칙. 지원되는 브라우저(최신 Chromium 또는 Safari 18.2+ 등)에서 두 파일 사이의 링크를 클릭하면 부드러운 크로스 페이드 (cross-fade) 효과가 나타납니다. JavaScript도 필요 없고, 메타 태그 (meta tags)도 필요 없으며, 빌드 단계 (build step)도 필요 없습니다. 브라우저가 이전 페이지의 스냅샷을 찍고, 새 페이지의 스냅샷을 찍은 뒤, 그 사이를 자동으로 애니메이션화합니다.
그렇다면 왜 명세 (spec)가 메타 태그에서 CSS @rule로 변경되었을까요? 이는 임의적인 결정이 아니었습니다.
메타 태그는 투박한 도구였습니다. 페이지 전체에 대해 켜거나 끄는 방식이었죠. "데스크톱에서는 전환 효과를 활성화하되, 저사양 하드웨어에서 애니메이션이 매끄럽지 않게 느껴지는 모바일에서는 비활성화하라"고 말할 수 없었습니다. 사용자의 선호도에 따라 조건부로 선택적 적용 (opt-in)을 할 수도 없었습니다. 그저... 있거나 없거나 둘 중 하나였습니다.
CSS 방식은 이 모든 것을 가능하게 합니다:
/* 사용자가 동작 줄이기(reduced motion)를 요청하지 않은 경우에만 전환 효과를 활성화합니다 */
@media (prefers-reduced-motion: no-preference) {
@view-transition {
...
/* 애니메이션이 자연스럽게 느껴질 만큼 충분히 넓은 뷰포트(viewport)에서만 활성화합니다 */
@media (min-width: 768px) {
@view-transition {
...
이것은 진정한 업그레이드입니다. 여러분이 이미 다른 모든 CSS 기능에서 사용하고 있는 것과 동일한 조건부 제어 능력을 얻게 됩니다. 미디어 쿼리 (Media queries), @supports, 그리고 여러분이 원하는 어떤 범위 지정 로직 (scoping logic)이든 — 선택적 적용 (opt-in) 방식이 스타일이 정의되는 곳에 위치하기 때문에 모든 것이 자연스럽게 작동합니다.
또한 주의해야 할 미묘한 차이가 있습니다. 이전 페이지와 새 페이지의 CSS 규칙이 다를 수 있다는 점입니다. 전환 (transition)이 실행되려면 두 페이지 모두 선택적 적용 (opt-in)을 해야 합니다. 만약 페이지 A에는 @view-transition { navigation: auto; }가 있지만, 페이지 B에는 없다면 전환은 발생하지 않습니다. 이는 사실 유용한 기능입니다. 즉, JavaScript의 조정 없이도 404 페이지나 로그인 리다이렉트 (login redirect) 시 전환을 건너뛸 수 있음을 의미합니다.
여기서 언급할 가치가 있는 한 가지가 더 있습니다: navigation: auto는 사용자가 직접 시작한 동일 출처 (same-origin) 탐색에 대해서만 작동합니다. 사용자가 일반 링크를 클릭하거나 브라우저의 뒤로 가기 버튼을 누르면 전환이 발생합니다. 하지만 프로그래밍 방식으로 설정된 window.location.href = "/somewhere"나, 교차 출처 (cross-origin) 링크, 또는 POST 방식의 폼 제출 (form submission)은 어떨까요? 전환이 발생하지 않습니다. 브라우저는 전환이 실행되는 시점에 대해 의도적으로 보수적인 태도를 취하며, 솔직히 그것이 올바른 결정입니다. 결제를 생성하는 POST 요청에서 화려한 크로스 페이드 (cross-fade) 효과가 나타나기를 원하지는 않을 테니까요.
만약 여러분이 오래된 튜토리얼을 따르고 있는데 전환이 아무런 반응 없이 작동하지 않는다면, 거의 확실히 이 때문일 것입니다. 해당 메타 태그 (meta tag)는 Chrome 111에서 출시되어 몇 달간 실제로 사용되었으나, Chrome 팀은 Chrome 126 버전부터 CSS 어트 룰 (at-rule)을 대신 사용하도록 이를 지원 중단 (deprecated)했습니다. 콘솔 경고도 없었고, 에러도 없었습니다. 이전 구문은 이제 그저 조용히 아무 일도 하지 않을 뿐입니다. 솔직히 DevTools에 지원 중단 경고가 떴다면 저(그리고 아마 여러분도)의 고생을 훨씬 줄여주었겠지만, 상황은 이렇습니다.
메타 태그를 CSS 규칙으로 교체하세요. 그것이 첫 번째 단계입니다. 이 글의 나머지 모든 내용은 이를 바탕으로 구축됩니다.
전환이 무작위로 중단될 것이며, 그 이유는 다음과 같습니다
// 실제로 어떤 일이 일어나고 있는지 확인하려면 페이지에 이 코드를 넣으세요
window.addEventListener("pagereveal", (event) => {
if (!event.viewTransition) {
...
아무도 블로그 포스트에 쓰지 않는 사실이 하나 있습니다. 바로 문서 간 뷰 전환 (cross-document view transitions)에는 엄격한 4초 타임아웃 (timeout)이 존재한다는 점입니다. 내비게이션 (navigation)이 시작된 후 4초 이내에 새 페이지가 브라우저가 "렌더링 가능 (renderable)"하다고 판단하는 상태에 도달하지 못하면, 전환은 그냥... 죽어버립니다. 애니메이션도 없고, 크로스 페이드 (cross-fade)도 없습니다. 새 페이지는 마치 뷰 전환 (view transitions)이 존재하지 않는 것처럼 즉시 나타납니다. 그리고 만약 pagereveal 리스너 (listener)를 연결해두고 콘솔 (console)을 열어두지 않았다면, 무엇인가 잘못되었다는 어떠한 징후도 알 수 없을 것입니다.
4초라는 시간은 관대하게 들릴 수 있지만, 그렇지 않을 때가 있습니다.
실제 사이트에서 어떤 일이 일어나는지 생각해 보세요. 페이지가 로드됩니다. HTML이 도착합니다. 좋습니다, 이건 빠릅니다. 하지만 렌더링 차단 (render-blocking)을 일으키는 커다란 히어로 이미지 (hero image)가 있을 수도 있습니다. 혹은 서버가 응답을 보내기 전까지 기다려야 하는 느린 API 호출 (API call)이 있을 수도 있습니다. 응답하기 전에 실제로 작업을 수행하는 서버 사이드 렌더링 (server-side rendering)이 적용된 제품 페이지가 재고 서비스에 접근하거나, 대시보드가 분석 데이터를 기다리는 등의 상황 말입니다. 괜찮은 연결 상태에 있더라도, 페이지에 font-display: block 설정이 된 Google Fonts의 웹 폰트 (web fonts) 3개가 로드되고 있을 수도 있습니다. 이 중 어떤 것이든 4초라는 시간 제한을 넘길 수 있으며, 타임아웃은 당신이 왜 느린지는 상관하지 않습니다. 그저 전환을 끊어버릴 뿐입니다.
정말 미치게 만드는 부분은 무엇일까요? 바로 localhost에서는 완벽하게 작동한다는 점입니다. 개발 서버는 80ms 만에 응답합니다. 전환은 매우 부드럽습니다. 하지만 프로덕션 (production)에 배포하고 나면, 서버의 람다 (lambda)가 콜드 스타트 (cold-starting)되거나 CDN 캐시 미스 (cache miss)가 발생하여, 갑자기 사용자들이 첫 클릭에서 전환 효과를 전혀 보지 못하게 됩니다. 로컬에서는 재현할 수 없습니다. 당신은 모든 것을 의심하기 시작할 것입니다.
// `pageswap`을 사용하여 이전 (OLD) 페이지에서도 이를 포착할 수 있습니다.
// 어떤 내비게이션이 실패했는지 정리하거나 로깅 (logging)하는 데 유용합니다.
window.addEventListener("pageswap", (event) => {
...
그렇다면, 실제로 이에 대해 어떻게 대처해야 할까요?
옵션 1: 페이지를 더 빠르게 만드세요. 잘 알고 계시겠지만, 아주 획기적인 조언이죠. 하지만 진심입니다. 만약 문서 간 전환 (cross-document transition)이 실패하고 있다면, 그것은 페이지 로드 (page load)가 실제로 느리다는 신호입니다. 타임아웃 (timeout)은 성능의 카나리아 (performance canary) 역할을 하고 있는 것입니다. DevTools의 Performance 탭을 확인하고, Lighthouse 감사 (audit)를 실행하여 (완벽하지 않을 수는 있지만), 무엇이 첫 번째 렌더링 (first render)을 방해하고 있는지 파악하세요. 이것은 뷰 전환 (view-transition)에만 국한된 조언은 아니지만, 타임아웃이 여러분으로 하여금 이 문제에 관심을 갖게 만듭니다.
옵션 2는 더 흥미롭습니다. 그리고 제가 즉시 알았더라면 좋았을 내용이기도 합니다.
<!-- 참고: rel="expect"는 최신 기능이며 브라우저 지원이 순차적으로 도입되고 있습니다 -->
<link rel="expect" href="#hero" blocking="render">
이것:
<link rel="expect" href="#hero" blocking="render">
…은 브라우저에게 다음과 같이 지시합니다: "#hero와 일치하는 요소가 DOM에 들어올 때까지 이 페이지를 렌더링 가능한 상태로 간주하지 마세요."
이 방식은 상황을 더 느리게 만들 것처럼 들리고, 어떤 면에서는 실제로 그렇습니다. 첫 번째 페인트 (first paint)를 지연시키니까요. 하지만 뷰 전환 (view transitions)을 위해서는 바로 그것이 우리가 원하는 것입니다. 즉, 절반만 로드된 페이지의 스냅샷을 찍거나, 더 나쁘게는 푸터 (footer)의 어떤 이미지가 여전히 다운로드 중이라 무언가를 차단하고 있다는 이유로 타임아웃이 발생하는 대신, 중요한 콘텐츠가 실제로 나타날 때까지 브라우저에게 스냅샷 (snapshot)을 유지하라고 말하는 것입니다.
이것은 트레이드오프 (trade-off)입니다. 빠르지만 깨진 전환 대신, 약간 지연되더라도 매끄러운 전환을 선택하는 것입니다.
솔직히 말해서, 4초 제한은 브라우저의 관점에서는 아마도 올바른 결정일 것입니다. 사용자가 링크를 클릭했는데 브라우저가 멋진 애니메이션을 수행하기 위해 기다리는 동안 10초 동안 멈춘 페이지를 멍하니 바라보게 하고 싶지는 않을 테니까요. 어느 시점에서는 그냥 페이지를 보여주는 것이 예쁜 전환 효과보다 낫습니다. 하지만 저는 Chrome이 타임아웃을 더 눈에 띄게 드러내 주기를 바랍니다. DevTools 경고든, 성능 마커 (performance marker)든, 무엇이라도 말이죠. 현재로서는 조용히 실패 (fails silently)하고 있으며, 바로 그 점이 모든 문제입니다.
한 가지 더 알아둘 가치가 있는 사실은, 타임아웃 시계(timeout clock)가 새 페이지의 HTML이 도착하기 시작할 때가 아니라 내비게이션(navigation)이 시작될 때부터 작동한다는 점입니다. 네트워크 지연 시간(Network latency)이 계산에 포함됩니다. Core Web Vital의 첫 바이트 도착 시간(TTFB, Time to First Byte)도 포함됩니다. 만약 서버가 응답하는 데 2초가 걸리고, 그 후 페이지를 렌더링(render)하는 데 2.5초가 걸린다면, 각각의 단계가 개별적으로는 느리게 느껴지지 않더라도 이미 제한 시간을 초과한 것입니다.
제가 여러 번 도움을 받았던 디버깅 팁을 하나 드리자면, Chrome의 DevTools에는 애니메이션(Animations) 패널이 있습니다(보이지 않는다면 "More tools" 아래에 있습니다). 이 패널은 실제로 작동 중인 뷰 전환(view transitions)을 캡처할 수 있습니다. 속도를 10%로 늦추거나, 다시 재생하거나, 애니메이션 중간에 의사 요소(pseudo-element) 트리를 검사할 수 있습니다. 이것이 뷰 전환(view transitions)에 작동한다는 사실이 명확하게 드러나지는 않지만, 실제로 작동합니다. 이 기능과 위에서 언급한 pagereveal 리스너(listener)를 활용하면 대부분의 타임아웃 문제를 상당히 빠르게 진단할 수 있습니다.
pagereveal 리스너를 초기에 배치하세요. 테스트하는 동안 콘솔(console)을 주시하십시오. 나중에 스스로에게 고마워하게 될 것입니다.
이미지가 왜 엿가락(Taffy)처럼 보이는가
이 문제는 먼저 동일 문서(same-document) 데모로 보여주는 것이 더 쉽지만(단일 파일에서 실제로 실행할 수 있기 때문), 문제와 해결 방법은 문서 간 전환(cross-document transitions)에서도 동일합니다.
그 데모를 실행해 보세요. 이미지를 클릭해 보세요. 개가 실리 퍼티(silly putty)처럼 변하는 것을 지켜보세요.
이미지 자체는 양쪽 모두 object-fit: cover 속성을 가지고 있습니다. 썸네일(thumbnail)도 괜찮아 보이고, 히어로(hero) 이미지도 괜찮아 보입니다. 하지만 전환(transition) 도중에는 어떨까요? 브라우저는 사용자의 <img> 태그를 전환하지 않습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 CSS-Tricks의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기