다중 페이지 애플리케이션의 문서 간 보기 전환

두 개의 서로 다른 문서 간에 뷰 전환이 발생하면 이를 교차 문서 뷰 전환이라고 합니다. 일반적으로 다중 페이지 애플리케이션 (MPA)에서 이러한 상황이 발생합니다. 문서 간 뷰 전환은 Chrome 126부터 Chrome에서 지원됩니다.

브라우저 지원

  • Chrome: 126
  • Edge: 126.
  • Firefox: 지원되지 않음
  • Safari Technology Preview: 지원됨

문서 간 뷰 전환은 동일한 문서 뷰 전환과 매우 동일한 빌딩 블록과 원칙에 의존합니다. 이는 매우 의도적인 전환입니다.

  1. 브라우저는 이전 페이지와 새 페이지 모두에 고유한 view-transition-name가 있는 요소의 스냅샷을 찍습니다.
  2. 렌더링이 억제되는 동안 DOM이 업데이트됩니다.
  3. 마지막으로 전환은 CSS 애니메이션을 기반으로 합니다.

동일 문서 뷰 전환과 비교할 때 다른 점은 교차 문서 뷰 전환에서는 document.startViewTransition를 호출하여 뷰 전환을 시작할 필요가 없다는 것입니다. 대신 교차 문서 뷰 전환의 트리거는 한 페이지에서 다른 페이지로의 동일 출처 탐색으로, 이는 일반적으로 웹사이트 사용자가 링크를 클릭할 때 실행하는 작업입니다.

즉, 두 문서 간의 뷰 전환을 시작하기 위해 호출할 API가 없습니다. 하지만 다음 두 가지 조건을 충족해야 합니다.

  • 두 문서가 동일한 출처에 있어야 합니다.
  • 뷰 전환을 허용하려면 두 페이지 모두에서 선택해야 합니다.

이 두 가지 조건은 이 문서의 뒷부분에서 설명합니다.


교차 문서 뷰 전환은 동일 출처 탐색으로 제한됩니다.

문서 간 뷰 전환은 동일 출처 탐색으로만 제한됩니다. 참여하는 두 페이지의 출처가 동일하면 탐색이 동일 출처로 간주됩니다.

페이지의 출처는 web.dev에 자세히 설명된 대로 사용된 스키마, 호스트 이름, 포트의 조합입니다.

스키마, 호스트 이름, 포트가 강조 표시된 URL 예시 이 두 값을 결합하면 출처가 됩니다.
스키마, 호스트 이름, 포트가 강조표시된 URL의 예시 이 두 가지를 결합하면 출처가 됩니다.

예를 들어 developer.chrome.com에서 developer.chrome.com/blog로 이동할 때 교차 문서 뷰 전환을 사용할 수 있습니다. 동일한 출처이기 때문입니다. developer.chrome.com에서 www.chrome.com로 이동할 때는 교차 출처 및 동일 사이트이므로 이러한 전환을 사용할 수 없습니다.


문서 간 뷰 전환 선택

두 문서 간에 문서 간 보기 전환이 이루어지도록 하려면 두 참여 페이지 모두 이 전환을 허용하도록 선택해야 합니다. 이 작업은 CSS의 @view-transition at-rule을 통해 실행됩니다.

@view-transition at-rule에서 navigation 설명자를 auto로 설정하여 교차 문서 동일 출처 탐색의 뷰 전환을 사용 설정합니다.

@view-transition {
  navigation: auto;
}

navigation 설명어를 auto로 설정하면 다음 NavigationType에서 뷰 전환이 발생하도록 허용하게 됩니다.

  • traverse
  • push 또는 replace: 사용자가 브라우저 UI 메커니즘을 통해 활성화를 시작하지 않은 경우

auto에서 제외되는 탐색은 URL 주소 표시줄을 사용하거나 북마크를 클릭하는 탐색, 그리고 모든 형태의 사용자 또는 스크립트가 시작한 새로고침입니다.

탐색이 너무 오래 걸리는 경우(Chrome의 경우 4초 이상) TimeoutError DOMException로 뷰 전환을 건너뜁니다.

교차 문서 뷰 전환 데모

뷰 전환을 사용하여 스택 네비게이터 데모를 만드는 다음 데모를 확인하세요. 여기서는 document.startViewTransition()를 호출하지 않습니다. 뷰 전환은 한 페이지에서 다른 페이지로 이동하여 트리거됩니다.

스택 탐색기 데모 녹화본 Chrome 126 이상이 필요합니다.

교차 문서 뷰 전환 맞춤설정

교차 문서 뷰 전환을 맞춤설정하려면 사용할 수 있는 몇 가지 웹 플랫폼 기능이 있습니다.

이러한 기능은 View Transition API 사양 자체의 일부가 아니지만 View Transition API 사양과 함께 사용하도록 설계되었습니다.

pageswappagereveal 이벤트

브라우저 지원

  • Chrome: 124.
  • Edge: 124.
  • Firefox: 지원되지 않음
  • Safari: 지원되지 않음

소스

교차 문서 뷰 전환을 맞춤설정할 수 있도록 HTML 사양에는 사용할 수 있는 두 가지 새 이벤트인 pageswappagereveal가 포함되어 있습니다.

이 두 이벤트는 뷰 전환이 발생할지 여부와 관계없이 동일한 출처의 교차 문서 탐색마다 실행됩니다. 두 페이지 간에 뷰 전환이 발생하려는 경우 이러한 이벤트의 viewTransition 속성을 사용하여 ViewTransition 객체에 액세스할 수 있습니다.

  • pageswap 이벤트는 페이지의 마지막 프레임이 렌더링되기 전에 발생합니다. 이 기능을 사용하면 이전 스냅샷이 찍히기 직전에 발신 페이지를 마지막으로 변경할 수 있습니다.
  • pagereveal 이벤트는 페이지가 초기화되거나 재활성화된 후 첫 번째 렌더링 기회 전에 페이지에서 발생합니다. 이를 통해 새 스냅샷을 찍기 전에 새 페이지를 맞춤설정할 수 있습니다.

예를 들어 이러한 이벤트를 사용하여 일부 view-transition-name 값을 빠르게 설정하거나 변경하거나 sessionStorage에서 데이터를 쓰고 읽음으로써 한 문서에서 다른 문서로 데이터를 전달하여 뷰 전환이 실제로 실행되기 전에 이를 맞춤설정할 수 있습니다.

let lastClickX, lastClickY;
document.addEventListener('click', (event) => {
  if (event.target.tagName.toLowerCase() === 'a') return;
  lastClickX = event.clientX;
  lastClickY = event.clientY;
});

// Write position to storage on old page
window.addEventListener('pageswap', (event) => {
  if (event.viewTransition && lastClick) {
    sessionStorage.setItem('lastClickX', lastClickX);
    sessionStorage.setItem('lastClickY', lastClickY);
  }
});

// Read position from storage on new page
window.addEventListener('pagereveal', (event) => {
  if (event.viewTransition) {
    lastClickX = sessionStorage.getItem('lastClickX');
    lastClickY = sessionStorage.getItem('lastClickY');
  }
});

원하는 경우 두 이벤트 모두에서 전환을 건너뛸 수 있습니다.

window.addEventListener("pagereveal", async (e) => {
  if (e.viewTransition) {
    if (goodReasonToSkipTheViewTransition()) {
      e.viewTransition.skipTransition();
    }
  }
}

pageswapViewTransition 객체와 pagerevealViewTransition 객체는 서로 다른 객체입니다. 또한 다양한 약속을 다르게 처리합니다.

  • pageswap: 문서가 숨겨지면 이전 ViewTransition 객체가 건너뜁니다. 이 경우 viewTransition.ready가 거부되고 viewTransition.finished가 확인됩니다.
  • pagereveal: 이 시점에서 updateCallBack 프로미스가 이미 해결되었습니다. viewTransition.readyviewTransition.finished 약속을 사용할 수 있습니다.

브라우저 지원

  • Chrome: 123
  • Edge: 123.
  • Firefox: 지원되지 않음
  • Safari: 지원되지 않음

소스

pageswappagereveal 이벤트 모두에서 이전 페이지와 새 페이지의 URL을 기반으로 조치를 취할 수도 있습니다.

예를 들어 MPA 스택 탐색기에서 사용할 애니메이션 유형은 탐색 경로에 따라 다릅니다.

  • 개요 페이지에서 세부정보 페이지로 이동할 때 새 콘텐츠가 오른쪽에서 왼쪽으로 슬라이드되어야 합니다.
  • 세부정보 페이지에서 개요 페이지로 이동할 때 이전 콘텐츠가 왼쪽에서 오른쪽으로 슬라이드 아웃되어야 합니다.

이렇게 하려면 pageswap의 경우 곧 실행될 탐색에 관한 정보 또는 pagereveal의 경우 방금 실행된 탐색에 관한 정보가 필요합니다.

이를 위해 이제 브라우저는 동일 출처 탐색에 관한 정보를 보유하는 NavigationActivation 객체를 노출할 수 있습니다. 이 객체는 사용된 탐색 유형, 현재, 최종 대상 기록 항목을 노출하며, 이는 Navigation API의 navigation.entries()에 있는 항목과 같습니다.

활성화된 페이지에서 navigation.activation를 통해 이 객체에 액세스할 수 있습니다. pageswap 이벤트에서는 e.activation를 통해 액세스할 수 있습니다.

pageswappagereveal 이벤트의 NavigationActivation 정보를 사용하여 뷰 전환에 참여해야 하는 요소의 view-transition-name 값을 설정하는 이 프로필 데모를 확인하세요.

이렇게 하면 view-transition-name를 사용하여 목록의 모든 항목을 데코레이션할 필요가 없습니다. 대신 JavaScript를 사용하여 필요한 요소에서만 적시에 실행됩니다.

프로필 데모 녹화본 Chrome 126 이상이 필요합니다.

코드는 다음과 같습니다.

// OLD PAGE LOGIC
window.addEventListener('pageswap', async (e) => {
  if (e.viewTransition) {
    const targetUrl = new URL(e.activation.entry.url);

    // Navigating to a profile page
    if (isProfilePage(targetUrl)) {
      const profile = extractProfileNameFromUrl(targetUrl);

      // Set view-transition-name values on the clicked row
      document.querySelector(`#${profile} span`).style.viewTransitionName = 'name';
      document.querySelector(`#${profile} img`).style.viewTransitionName = 'avatar';

      // Remove view-transition-names after snapshots have been taken
      // (this to deal with BFCache)
      await e.viewTransition.finished;
      document.querySelector(`#${profile} span`).style.viewTransitionName = 'none';
      document.querySelector(`#${profile} img`).style.viewTransitionName = 'none';
    }
  }
});

// NEW PAGE LOGIC
window.addEventListener('pagereveal', async (e) => {
  if (e.viewTransition) {
    const fromURL = new URL(navigation.activation.from.url);
    const currentURL = new URL(navigation.activation.entry.url);

    // Navigating from a profile page back to the homepage
    if (isProfilePage(fromURL) && isHomePage(currentURL)) {
      const profile = extractProfileNameFromUrl(currentURL);

      // Set view-transition-name values on the elements in the list
      document.querySelector(`#${profile} span`).style.viewTransitionName = 'name';
      document.querySelector(`#${profile} img`).style.viewTransitionName = 'avatar';

      // Remove names after snapshots have been taken
      // so that we're ready for the next navigation
      await e.viewTransition.ready;
      document.querySelector(`#${profile} span`).style.viewTransitionName = 'none';
      document.querySelector(`#${profile} img`).style.viewTransitionName = 'none';
    }
  }
});

또한 이 코드는 뷰 전환이 실행된 후 view-transition-name 값을 삭제하여 자체적으로 정리합니다. 이렇게 하면 페이지가 연속적인 탐색에 대비할 수 있고 기록 순회도 처리할 수 있습니다.

이 작업을 지원하려면 view-transition-name를 일시적으로 설정하는 이 유틸리티 함수를 사용하세요.

const setTemporaryViewTransitionNames = async (entries, vtPromise) => {
  for (const [$el, name] of entries) {
    $el.style.viewTransitionName = name;
  }

  await vtPromise;

  for (const [$el, name] of entries) {
    $el.style.viewTransitionName = '';
  }
}

이제 이전 코드를 다음과 같이 간소화할 수 있습니다.

// OLD PAGE LOGIC
window.addEventListener('pageswap', async (e) => {
  if (e.viewTransition) {
    const targetUrl = new URL(e.activation.entry.url);

    // Navigating to a profile page
    if (isProfilePage(targetUrl)) {
      const profile = extractProfileNameFromUrl(targetUrl);

      // Set view-transition-name values on the clicked row
      // Clean up after the page got replaced
      setTemporaryViewTransitionNames([
        [document.querySelector(`#${profile} span`), 'name'],
        [document.querySelector(`#${profile} img`), 'avatar'],
      ], e.viewTransition.finished);
    }
  }
});

// NEW PAGE LOGIC
window.addEventListener('pagereveal', async (e) => {
  if (e.viewTransition) {
    const fromURL = new URL(navigation.activation.from.url);
    const currentURL = new URL(navigation.activation.entry.url);

    // Navigating from a profile page back to the homepage
    if (isProfilePage(fromURL) && isHomePage(currentURL)) {
      const profile = extractProfileNameFromUrl(currentURL);

      // Set view-transition-name values on the elements in the list
      // Clean up after the snapshots have been taken
      setTemporaryViewTransitionNames([
        [document.querySelector(`#${profile} span`), 'name'],
        [document.querySelector(`#${profile} img`), 'avatar'],
      ], e.viewTransition.ready);
    }
  }
});

렌더링 차단으로 콘텐츠가 로드될 때까지 기다리기

브라우저 지원

  • Chrome: 124
  • Edge: 124.
  • Firefox: 지원되지 않음
  • Safari: 지원되지 않음

경우에 따라 특정 요소가 새 DOM에 표시될 때까지 페이지의 첫 번째 렌더링을 보류할 수 있습니다. 이렇게 하면 플래시 현상이 방지되고 애니메이션을 적용할 상태가 안정적입니다.

<head>에서 다음 메타 태그를 사용하여 페이지가 첫 번째 렌더링되기 전에 있어야 하는 요소 ID를 하나 이상 정의합니다.

<link rel="expect" blocking="render" href="#section1">

이 메타 태그는 콘텐츠를 로드해야 한다는 의미가 아니라 요소가 DOM에 있어야 한다는 의미입니다. 예를 들어 이미지의 경우 DOM 트리에 지정된 id가 있는 <img> 태그가 있으면 조건이 true로 평가되기에 충분합니다. 이미지 자체는 여전히 로드 중일 수 있습니다.

렌더링 차단에 올인하기 전에 증분 렌더링이 웹의 기본적인 측면이므로 렌더링 차단을 선택할 때는 주의하세요. 렌더링 차단의 영향은 사례별로 평가해야 합니다. 기본적으로 blocking=renderCore Web Vitals에 미치는 영향을 측정하여 사용자에게 미치는 영향을 적극적으로 측정하고 평가할 수 없는 한 사용하지 않는 것이 좋습니다.


문서 간 뷰 전환의 전환 유형 보기

문서 간 뷰 전환은 보기 전환 유형도 지원하여 애니메이션과 캡처되는 요소를 맞춤설정합니다.

예를 들어 페이지로 나누기에서 다음 또는 이전 페이지로 이동할 때 시퀀스에서 상위 페이지로 이동하는지 또는 하위 페이지로 이동하는지에 따라 다른 애니메이션을 사용할 수 있습니다.

이러한 유형을 미리 설정하려면 @view-transition at-규칙에 유형을 추가하세요.

@view-transition {
  navigation: auto;
  types: slide, forwards;
}

유형을 즉시 설정하려면 pageswappagereveal 이벤트를 사용하여 e.viewTransition.types의 값을 조작합니다.

window.addEventListener("pagereveal", async (e) => {
  if (e.viewTransition) {
    const transitionType = determineTransitionType(navigation.activation.from, navigation.activation.entry);
    e.viewTransition.types.add(transitionType);
  }
});

유형은 이전 페이지의 ViewTransition 객체에서 새 페이지의 ViewTransition 객체로 자동으로 이전되지 않습니다. 애니메이션이 예상대로 실행되려면 적어도 새 페이지에서 사용할 유형을 결정해야 합니다.

이러한 유형에 응답하려면 동일한 문서 뷰 전환과 동일한 방식으로 :active-view-transition-type() 의사 클래스 선택기를 사용하세요.

/* Determine what gets captured when the type is forwards or backwards */
html:active-view-transition-type(forwards, backwards) {
  :root {
    view-transition-name: none;
  }
  article {
    view-transition-name: content;
  }
  .pagination {
    view-transition-name: pagination;
  }
}

/* Animation styles for forwards type only */
html:active-view-transition-type(forwards) {
  &::view-transition-old(content) {
    animation-name: slide-out-to-left;
  }
  &::view-transition-new(content) {
    animation-name: slide-in-from-right;
  }
}

/* Animation styles for backwards type only */
html:active-view-transition-type(backwards) {
  &::view-transition-old(content) {
    animation-name: slide-out-to-right;
  }
  &::view-transition-new(content) {
    animation-name: slide-in-from-left;
  }
}

/* Animation styles for reload type only */
html:active-view-transition-type(reload) {
  &::view-transition-old(root) {
    animation-name: fade-out, scale-down;
  }
  &::view-transition-new(root) {
    animation-delay: 0.25s;
    animation-name: fade-in, scale-up;
  }
}

유형은 활성 뷰 전환에만 적용되므로 뷰 전환이 완료되면 유형이 자동으로 정리됩니다. 따라서 유형은 BFCache와 같은 기능과 잘 작동합니다.

데모

다음 페이지 분류 데모에서는 이동하는 페이지 번호에 따라 페이지 콘텐츠가 앞뒤로 슬라이드합니다.

페이지로 나누기 데모 (MPA) 녹화 이동하려는 페이지에 따라 다른 전환을 사용합니다.

사용할 전환 유형은 pagerevealpageswap 이벤트에서 출발 URL과 도착 URL을 보고 결정됩니다.

const determineTransitionType = (fromNavigationEntry, toNavigationEntry) => {
  const currentURL = new URL(fromNavigationEntry.url);
  const destinationURL = new URL(toNavigationEntry.url);

  const currentPathname = currentURL.pathname;
  const destinationPathname = destinationURL.pathname;

  if (currentPathname === destinationPathname) {
    return "reload";
  } else {
    const currentPageIndex = extractPageIndexFromPath(currentPathname);
    const destinationPageIndex = extractPageIndexFromPath(destinationPathname);

    if (currentPageIndex > destinationPageIndex) {
      return 'backwards';
    }
    if (currentPageIndex < destinationPageIndex) {
      return 'forwards';
    }

    return 'unknown';
  }
};

의견

개발자 의견은 언제나 환영합니다. 공유하려면 제안사항과 질문을 포함하여 GitHub에서 CSS 작업 그룹에 문제를 제출하세요. 문제 앞에 [css-view-transitions]를 붙입니다. 버그가 발생하면 대신 Chromium 버그를 신고하세요.