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

서로 다른 두 문서 간에 보기 전환이 발생하는 경우 이를 문서 간 보기 전환이라고 합니다. 일반적으로 다중 페이지 애플리케이션 (MPA)이 이러한 경우에 해당합니다. Chrome 126부터 Chrome에서 문서 간 보기 전환이 지원됩니다.

브라우저 지원

  • 126
  • 126
  • x
  • x

소스

문서 간 보기 전환은 동일한 문서 보기 전환과 매우 동일한 구성 요소와 원칙을 사용하며 이는 매우 의도적입니다.

  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에서 navigation 설명자를 auto로 설정하여 문서 간 동일 출처 탐색의 뷰 전환을 사용 설정합니다.

@view-transition {
  navigation: auto;
}

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

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

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

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

문서 간 뷰 전환 데모

뷰 전환을 사용하여 Stack Navigator 데모를 만드는 다음 데모를 확인해 보세요. 여기에는 document.startViewTransition() 호출이 없습니다. 뷰 전환은 한 페이지에서 다른 페이지로 이동하여 트리거됩니다.

Stack Navigator 데모의 녹화본입니다. Chrome 126 이상이 필요합니다.

문서 간 뷰 전환 맞춤설정

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

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

pageswappagereveal 이벤트

브라우저 지원

  • 124
  • 124
  • x
  • x

소스

문서 간 보기 전환을 맞춤설정할 수 있도록 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();
    }
  }
}

pageswappagerevealViewTransition 객체는 서로 다른 두 개의 객체입니다. 또한 다양한 프로미스를 다르게 처리합니다.

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

브라우저 지원

  • 123
  • 123
  • x
  • x

소스

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

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

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

이렇게 하려면 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);
    }
  }
});

렌더링 차단과 함께 콘텐츠가 로드될 때까지 대기

브라우저 지원

  • 124
  • 124
  • x
  • x

소스

경우에 따라 특정 요소가 새 DOM에 나타날 때까지 페이지의 첫 번째 렌더링을 보류하고 싶을 수 있습니다. 이렇게 하면 플래싱이 방지되고 애니메이션 적용 중인 상태가 안정적인지 확인할 수 있습니다.

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

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

이 메타 태그는 콘텐츠가 로드되어야 한다는 것이 아니라 DOM에 요소가 있어야 함을 의미합니다. 예를 들어 이미지의 경우 DOM 트리에 지정된 id가 있는 <img> 태그만 있어도 조건이 참으로 평가될 수 있습니다. 이미지 자체가 아직 로드 중일 수 있습니다.

렌더링 차단을 본격적으로 시작하기 전에 증분 렌더링은 웹의 기본 요소라는 점을 인지해야 합니다. 따라서 렌더링 차단을 선택할 때는 신중을 기해야 합니다. 렌더링 차단의 영향은 사례별로 평가해야 합니다. 코어 웹 바이탈에 미치는 영향을 측정하여 사용자에게 미치는 영향을 적극적으로 측정하고 측정할 수 있는 경우가 아니라면 기본적으로 blocking=render 사용을 피하세요.


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

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

예를 들어, 페이지 매김에서 다음 페이지로 이동하거나 이전 페이지로 이동할 때 순서에서 상위 페이지로 이동하는지 또는 하위 페이지로 이동하는지에 따라 다른 애니메이션을 사용할 수 있습니다.

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

@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;
  }
}

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

데모

다음 페이지로 나누기 데모에서 페이지 콘텐츠는 탐색하는 페이지 번호에 따라 앞뒤로 슬라이드됩니다.

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

사용할 전환 유형은 도착 및 이전 URL을 확인하여 pagerevealpageswap 이벤트에서 결정됩니다.

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 버그를 신고하세요.