단일 페이지 애플리케이션의 동일 문서 보기 전환

게시: 2021년 8월 17일, 최종 업데이트: 2024년 9월 25일

단일 문서에서 뷰 전환이 실행되는 경우 이를 동일 문서 뷰 전환이라고 합니다. 일반적으로 JavaScript를 사용하여 DOM을 업데이트하는 단일 페이지 애플리케이션 (SPA)에서 이러한 상황이 발생합니다. 동일한 문서 보기 전환은 Chrome 111부터 Chrome에서 지원됩니다.

동일한 문서 뷰 전환을 트리거하려면 다음과 같이 document.startViewTransition를 호출합니다.

function handleClick(e) {
  // Fallback for browsers that don't support this API:
  if (!document.startViewTransition) {
    updateTheDOMSomehow();
    return;
  }

  // With a View Transition:
  document.startViewTransition(() => updateTheDOMSomehow());
}

호출하면 브라우저가 view-transition-name CSS 속성이 선언된 모든 요소의 스냅샷을 자동으로 캡처합니다.

그런 다음 DOM을 업데이트하는 전달된 콜백을 실행한 다음 새 상태의 스냅샷을 찍습니다.

그런 다음 이러한 스냅샷은 의사 요소 트리에 정렬되며 CSS 애니메이션의 기능을 사용하여 애니메이션 처리됩니다. 이전 상태와 새 상태의 스냅샷 쌍이 이전 위치와 크기에서 새 위치로 원활하게 전환되는 동안 콘텐츠가 크로스페이드됩니다. 원하는 경우 CSS를 사용하여 애니메이션을 맞춤설정할 수 있습니다.


기본 전환: 크로스페이드

기본 보기 전환은 크로스 페이드이므로 API를 소개하는 역할을 합니다.

function spaNavigate(data) {
  // Fallback for browsers that don't support this API:
  if (!document.startViewTransition) {
    updateTheDOMSomehow(data);
    return;
  }

  // With a transition:
  document.startViewTransition(() => updateTheDOMSomehow(data));
}

여기서 updateTheDOMSomehow은 DOM을 새 상태로 변경합니다. 원하는 대로 할 수 있습니다. 예를 들어 요소를 추가 또는 삭제하거나, 클래스 이름을 변경하거나, 스타일을 변경할 수 있습니다.

이렇게 페이지가 크로스 페이드됩니다.

기본 크로스페이드입니다. 최소 데모 출처.

크로스페이드는 그다지 인상적이지 않습니다. 다행히 전환은 맞춤설정할 수 있지만 먼저 이 기본 크로스페이드가 작동하는 방식을 이해해야 합니다.


전환 작동 방식

이전 코드 샘플을 업데이트해 보겠습니다.

document.startViewTransition(() => updateTheDOMSomehow(data));

.startViewTransition()가 호출되면 API가 페이지의 현재 상태를 캡처합니다. 여기에는 스냅샷을 찍는 것도 포함됩니다.

완료되면 .startViewTransition()에 전달된 콜백이 호출됩니다. 여기서 DOM이 변경됩니다. 그런 다음 API는 페이지의 새 상태를 캡처합니다.

새 상태가 캡처되면 API는 다음과 같이 가상 요소 트리를 구성합니다.

::view-transition
└─ ::view-transition-group(root)
   └─ ::view-transition-image-pair(root)
      ├─ ::view-transition-old(root)
      └─ ::view-transition-new(root)

::view-transition는 페이지의 다른 모든 항목 위에 오버레이로 표시됩니다. 이는 전환의 배경 색상을 설정하려는 경우에 유용합니다.

::view-transition-old(root)는 이전 뷰의 스크린샷이고 ::view-transition-new(root)는 새 뷰의 실시간 표현입니다. 둘 다 CSS '대체 콘텐츠'(예: <img>)로 렌더링됩니다.

기존 뷰는 opacity: 1에서 opacity: 0로 애니메이션되고 새 뷰는 opacity: 0에서 opacity: 1로 애니메이션 처리되어 크로스 페이드가 생성됩니다.

모든 애니메이션은 CSS 애니메이션을 사용하여 실행되므로 CSS로 맞춤설정할 수 있습니다.

전환 맞춤설정

모든 뷰 전환 가상 요소는 CSS로 타겟팅할 수 있으며 애니메이션은 CSS를 사용하여 정의되므로 기존 CSS 애니메이션 속성을 사용하여 수정할 수 있습니다. 예를 들면 다음과 같습니다.

::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: 5s;
}

이 한 가지 변경사항으로 이제 페이드가 매우 느려집니다.

긴 크로스페이드입니다. 최소 데모 출처.

그래도 여전히 만족스럽지 않습니다. 대신 다음 코드는 Material Design의 공유 축 전환을 구현합니다.

@keyframes fade-in {
  from { opacity: 0; }
}

@keyframes fade-out {
  to { opacity: 0; }
}

@keyframes slide-from-right {
  from { transform: translateX(30px); }
}

@keyframes slide-to-left {
  to { transform: translateX(-30px); }
}

::view-transition-old(root) {
  animation: 90ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
    300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left;
}

::view-transition-new(root) {
  animation: 210ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in,
    300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
}

결과는 다음과 같습니다.

공유 축 전환 최소 데모 출처.
를 통해 개인정보처리방침을 정의할 수 있습니다.

여러 요소 전환

이전 데모에서는 전체 페이지가 공유 축 전환에 관여합니다. 이 방법은 대부분의 페이지에서 작동하지만, 다시 밀어 넣기 위해 슬라이드아웃되므로 제목에는 적합하지 않을 수 있습니다.

이 문제를 방지하려면 페이지의 나머지 부분에서 헤더를 추출하여 별도로 애니메이션을 적용할 수 있습니다. 요소에 view-transition-name를 할당하면 됩니다.

.main-header {
  view-transition-name: main-header;
}

view-transition-name의 값은 원하는 대로 지정할 수 있습니다(전환 이름이 없음을 나타내는 none 제외). 전환에서 요소를 고유하게 식별하는 데 사용됩니다.

그 결과:

<ph type="x-smartling-placeholder">
</ph>
고정 헤더가 있는 공유 축 전환. 최소 데모 출처.

이제 헤더가 고정되고 크로스 페이드됩니다.

이 CSS 선언으로 인해 가상 요소 트리가 변경되었습니다.

::view-transition
├─ ::view-transition-group(root)
│  └─ ::view-transition-image-pair(root)
│     ├─ ::view-transition-old(root)
│     └─ ::view-transition-new(root)
└─ ::view-transition-group(main-header)
   └─ ::view-transition-image-pair(main-header)
      ├─ ::view-transition-old(main-header)
      └─ ::view-transition-new(main-header)

이제 전환 그룹이 두 개 있습니다. 하나는 헤더용, 다른 하나는 나머지용입니다. CSS로 독립적으로 타겟팅하고 서로 다른 전환을 적용할 수 있습니다. 하지만 이 경우에는 main-header에 기본 전환인 크로스 페이드가 그대로 남아 있습니다.

기본 전환은 크로스 페이드뿐만 아니라 ::view-transition-group도 전환합니다.

  • 위치 및 변환 (transform 사용)
  • 너비
  • 높이

지금까지는 헤더가 DOM 변경의 양쪽에서 동일한 크기와 위치를 가지므로 문제가 되지 않았습니다. 헤더에서 텍스트를 추출할 수도 있습니다.

.main-header-text {
  view-transition-name: main-header-text;
  width: fit-content;
}

요소가 나머지 너비로 늘어나지 않고 텍스트 크기가 되도록 fit-content가 사용됩니다. 이 속성이 없으면 뒤로 화살표를 사용하면 헤더 텍스트 요소의 크기가 두 페이지에서 같은 크기로 줄어듭니다.

이제 세 가지 부분을 다루겠습니다.

::view-transition
├─ ::view-transition-group(root)
│  └─ …
├─ ::view-transition-group(main-header)
│  └─ …
└─ ::view-transition-group(main-header-text)
   └─ …

하지만 다시 기본값을 사용합니다.

슬라이딩 헤더 텍스트 최소 데모 출처.

이제 제목 텍스트가 뒤로 버튼을 위한 공간을 만들기 위해 약간 슬라이드합니다.


view-transition-class를 사용하여 여러 가상 요소에 동일한 방식으로 애니메이션 적용

브라우저 지원

  • Chrome: 125. <ph type="x-smartling-placeholder">
  • Edge: 125. <ph type="x-smartling-placeholder">
  • Firefox: 지원되지 않음
  • Safari Technology Preview: 지원됨

여러 장의 카드가 있고 페이지에 제목도 있는 뷰 전환이 있다고 가정해 보겠습니다. 제목을 제외한 모든 카드에 애니메이션을 적용하려면 모든 개별 카드를 타겟팅하는 선택기를 작성해야 합니다.

h1 {
    view-transition-name: title;
}
::view-transition-group(title) {
    animation-timing-function: ease-in-out;
}

#card1 { view-transition-name: card1; }
#card2 { view-transition-name: card2; }
#card3 { view-transition-name: card3; }
#card4 { view-transition-name: card4; }

#card20 { view-transition-name: card20; }

::view-transition-group(card1),
::view-transition-group(card2),
::view-transition-group(card3),
::view-transition-group(card4),

::view-transition-group(card20) {
    animation-timing-function: var(--bounce);
}

원소가 20개나 됐어? 20개의 선택기를 작성해야 합니다. 새 요소를 추가하시나요? 그런 다음 애니메이션 스타일을 적용하는 선택기도 확장해야 합니다. 정확히 확장 가능하지 않습니다.

뷰 전환 의사 요소에서 view-transition-class를 사용하여 동일한 스타일 규칙을 적용할 수 있습니다.

#card1 { view-transition-name: card1; }
#card2 { view-transition-name: card2; }
#card3 { view-transition-name: card3; }
#card4 { view-transition-name: card4; }
#card5 { view-transition-name: card5; }

#card20 { view-transition-name: card20; }

#cards-wrapper > div {
  view-transition-class: card;
}
html::view-transition-group(.card) {
  animation-timing-function: var(--bounce);
}

다음 카드의 예는 이전 CSS 스니펫을 활용합니다. 새로 추가된 카드를 포함한 모든 카드에 하나의 선택기(html::view-transition-group(.card))로 동일한 타이밍이 적용됩니다.

카드 데모 녹화본 view-transition-class를 사용하면 추가되거나 삭제된 카드를 제외한 모든 카드에 동일한 animation-timing-function가 적용됩니다.

전환 디버그

뷰 전환은 CSS 애니메이션을 기반으로 빌드되므로 Chrome DevTools의 애니메이션 패널은 전환을 디버깅하는 데 유용합니다.

애니메이션 패널을 사용하여 다음 애니메이션을 일시중지한 다음 애니메이션을 앞뒤로 스크러빙할 수 있습니다. 이때 전환 가상 요소는 요소 패널에서 확인할 수 있습니다.

Chrome DevTools로 뷰 전환 디버깅

전환 요소가 동일한 DOM 요소일 필요는 없습니다.

지금까지는 view-transition-name를 사용하여 헤더와 헤더의 텍스트를 위한 별도의 전환 요소를 만들었습니다. 이는 DOM 변경 전후에 개념적으로 동일한 요소이지만 그렇지 않은 경우 전환을 만들 수 있습니다.

예를 들어 기본 동영상 삽입에는 view-transition-name이 주어질 수 있습니다.

.full-embed {
  view-transition-name: full-embed;
}

그런 다음 썸네일을 클릭하면 전환 시간 동안만 동일한 view-transition-name가 부여될 수 있습니다.

thumbnail.onclick = async () => {
  thumbnail.style.viewTransitionName = 'full-embed';

  document.startViewTransition(() => {
    thumbnail.style.viewTransitionName = '';
    updateTheDOMSomehow();
  });
};

결과는 다음과 같습니다.

<ph type="x-smartling-placeholder">
</ph>
한 요소가 다른 요소로 전환됩니다. 최소 데모 출처.

썸네일이 기본 이미지로 전환됩니다. 두 요소는 개념적으로(실제로) 서로 다른 요소이지만 동일한 view-transition-name를 공유하므로 전환 API는 이를 동일한 것으로 취급합니다.

이 전환의 실제 코드는 썸네일 페이지로 다시 전환하는 것도 처리하므로 이전 예시보다 약간 더 복잡합니다. 전체 구현은 소스를 참고하세요.


맞춤 들어가기 및 나가기 전환

다음 예를 살펴보세요.

<ph type="x-smartling-placeholder">
</ph>
사이드바에 들어가고 나갑니다. 최소 데모 출처.

사이드바는 전환의 일부입니다.

.sidebar {
  view-transition-name: sidebar;
}

그러나 앞선 예의 헤더와 달리 사이드바는 일부 페이지에만 표시됩니다. 두 상태 모두 사이드바가 있는 경우 전환 가상 요소는 다음과 같이 표시됩니다.

::view-transition
├─ …other transition groups…
└─ ::view-transition-group(sidebar)
   └─ ::view-transition-image-pair(sidebar)
      ├─ ::view-transition-old(sidebar)
      └─ ::view-transition-new(sidebar)

하지만 사이드바가 새 페이지에만 있는 경우 ::view-transition-old(sidebar) 의사 요소는 표시되지 않습니다. '이전'이라는 사이드바용 이미지인 경우 이미지 쌍에는 ::view-transition-new(sidebar)만 있습니다. 마찬가지로 사이드바가 이전 페이지에만 있는 경우 이미지 쌍에는 ::view-transition-old(sidebar)만 있습니다.

이전 데모에서 사이드바는 두 상태 모두에서 진입, 이탈 또는 표시 여부에 따라 다르게 전환됩니다. 오른쪽에서 슬라이드하여 페이드 인하여 표시되고, 오른쪽으로 슬라이드하여 페이드 아웃하여 종료되며, 두 상태 모두에 표시되면 제자리에 유지됩니다.

특정 시작 및 종료 전환을 만들려면 :only-child 의사 클래스를 사용하여 이전 또는 새 유사 요소가 이미지 쌍의 유일한 하위 요소인 경우 이를 타겟팅하면 됩니다.

/* Entry transition */
::view-transition-new(sidebar):only-child {
  animation: 300ms cubic-bezier(0, 0, 0.2, 1) both fade-in,
    300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
}

/* Exit transition */
::view-transition-old(sidebar):only-child {
  animation: 150ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
    300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-right;
}

이 경우 기본값이 완벽하므로 사이드바가 두 상태 모두에 있는 경우의 특정 전환은 없습니다.

비동기 DOM 업데이트 및 콘텐츠 대기

.startViewTransition()에 전달된 콜백은 비동기 DOM 업데이트를 허용하고 중요한 콘텐츠가 준비되기를 기다리는 프로미스를 반환할 수 있습니다.

document.startViewTransition(async () => {
  await something;
  await updateTheDOMSomehow();
  await somethingElse;
});

프로미스가 처리될 때까지 전환이 시작되지 않습니다. 이때 페이지가 정지되므로 지연을 최소화해야 합니다. 특히 네트워크 가져오기는 .startViewTransition() 콜백의 일부로 실행하는 대신 .startViewTransition()를 호출하기 전에 페이지가 완전히 상호작용할 수 있는 상태에서 실행해야 합니다.

이미지나 글꼴이 준비될 때까지 기다리기로 한 경우 공격적인 제한 시간을 사용해야 합니다.

const wait = ms => new Promise(r => setTimeout(r, ms));

document.startViewTransition(async () => {
  updateTheDOMSomehow();

  // Pause for up to 100ms for fonts to be ready:
  await Promise.race([document.fonts.ready, wait(100)]);
});

하지만 경우에 따라서는 지연을 완전히 피하고 이미 보유하고 있는 콘텐츠를 사용하는 것이 더 나을 수도 있습니다.


이미 보유한 콘텐츠를 최대한 활용하세요

썸네일이 더 큰 이미지로 전환되는 경우:

썸네일이 더 큰 이미지로 전환됩니다. 데모 사이트 사용해 보기

기본 전환은 크로스페이드입니다. 즉, 썸네일이 아직 로드되지 않은 전체 이미지와 크로스페이드될 수 있습니다.

이를 처리하는 한 가지 방법은 전환을 시작하기 전에 전체 이미지가 로드될 때까지 기다리는 것입니다. 이상적으로는 .startViewTransition()를 호출하기 전에 이 작업을 실행하여 페이지의 상호작용 상태를 유지하고 스피너를 표시하여 사용자에게 항목이 로드되고 있음을 나타낼 수 있습니다. 하지만 이 경우에는 더 좋은 방법이 있습니다.

::view-transition-old(full-embed),
::view-transition-new(full-embed) {
  /* Prevent the default animation,
  so both views remain opacity:1 throughout the transition */
  animation: none;
  /* Use normal blending,
  so the new view sits on top and obscures the old view */
  mix-blend-mode: normal;
}

이제 썸네일이 사라지지 않고 전체 이미지 아래에 표시됩니다. 즉, 새 뷰가 로드되지 않은 경우 전환하는 동안 썸네일이 계속 표시됩니다. 즉, 전환을 즉시 시작할 수 있으며 전체 이미지를 원하는 시간에 로드할 수 있습니다.

새 보기에서 투명도를 표시했다면 작동하지 않지만 이 경우에는 그렇지 않다는 것을 알고 있으므로 이 최적화를 만들 수 있습니다.

가로세로 비율 변경 처리

편리하게도, 지금까지의 모든 전환은 동일한 가로세로 비율의 요소로 이루어졌지만 항상 그런 것은 아닙니다. 썸네일이 1:1이고 기본 이미지가 16:9인 경우 어떻게 하나요?

가로세로 비율이 변경되면서 한 요소가 다른 요소로 전환됩니다. 최소 데모 출처.

기본 전환에서 그룹은 전 크기에서 이후 크기로 애니메이션됩니다. 이전 뷰와 새 뷰는 그룹의 너비가 100%이고 높이는 자동입니다. 즉, 그룹 크기와 관계없이 가로세로 비율이 유지됩니다.

좋은 기본값이지만 이 경우에는 적합하지 않습니다. 따라서 다음과 같은 전략을 구사하세요.

::view-transition-old(full-embed),
::view-transition-new(full-embed) {
  /* Prevent the default animation,
  so both views remain opacity:1 throughout the transition */
  animation: none;
  /* Use normal blending,
  so the new view sits on top and obscures the old view */
  mix-blend-mode: normal;
  /* Make the height the same as the group,
  meaning the view size might not match its aspect-ratio. */
  height: 100%;
  /* Clip any overflow of the view */
  overflow: clip;
}

/* The old view is the thumbnail */
::view-transition-old(full-embed) {
  /* Maintain the aspect ratio of the view,
  by shrinking it to fit within the bounds of the element */
  object-fit: contain;
}

/* The new view is the full image */
::view-transition-new(full-embed) {
  /* Maintain the aspect ratio of the view,
  by growing it to cover the bounds of the element */
  object-fit: cover;
}

즉, 너비가 넓어지면 썸네일이 요소의 중앙에 유지되지만 전체 이미지는 1:1에서 16:9로 전환되면 '자르기가 해제'됩니다.

자세한 내용은 전환 보기: 가로세로 비율 변경 처리를 참고하세요.


미디어 쿼리를 사용하여 다양한 기기 상태의 전환 변경

모바일과 데스크톱에서 서로 다른 전환을 사용할 수 있습니다. 예를 들어 모바일에서는 측면에 전체 슬라이드를 실행하고 데스크톱에서는 좀 더 섬세하게 슬라이드합니다.

하나의 요소가 다른 요소로 전환됩니다. 최소 데모 출처.

이렇게 하려면 일반 미디어 쿼리를 사용하면 됩니다.

/* Transitions for mobile */
::view-transition-old(root) {
  animation: 300ms ease-out both full-slide-to-left;
}

::view-transition-new(root) {
  animation: 300ms ease-out both full-slide-from-right;
}

@media (min-width: 500px) {
  /* Overrides for larger displays.
  This is the shared axis transition from earlier in the article. */
  ::view-transition-old(root) {
    animation: 90ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
      300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left;
  }

  ::view-transition-new(root) {
    animation: 210ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in,
      300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
  }
}

일치하는 미디어 쿼리에 따라 view-transition-name를 할당하는 요소를 변경할 수도 있습니다.


'모션 감소' 환경설정에 반응

사용자는 운영체제를 통해 모션 감소를 선호하고 이러한 선호도가 CSS에 노출되도록 표시할 수 있습니다.

이러한 사용자의 전환이 발생하지 않도록 다음과 같이 선택할 수 있습니다.

@media (prefers-reduced-motion) {
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) {
    animation: none !important;
  }
}

하지만 '모션 감소' 환경설정을 선택했다고 해서 사용자가 모션이 없기를 원하는 것은 아닙니다. 위 스니펫 대신 요소 간의 관계와 데이터 흐름을 여전히 표현하는 좀 더 미묘한 애니메이션을 선택할 수 있습니다.


뷰 전환 유형으로 여러 뷰 전환 스타일 처리

브라우저 지원

  • Chrome: 125 <ph type="x-smartling-placeholder">
  • Edge: 125. <ph type="x-smartling-placeholder">
  • Firefox: 지원되지 않음
  • Safari: 18

특정 뷰에서 다른 뷰로 전환할 때 특별히 맞춤설정된 전환이 필요한 경우가 있습니다. 예를 들어 페이지 표시 시퀀스에서 다음 페이지 또는 이전 페이지로 이동할 때 시퀀스에서 더 높은 페이지로 이동하는지 더 낮은 페이지로 이동하는지에 따라 콘텐츠를 다른 방향으로 슬라이드할 수 있습니다.

<ph type="x-smartling-placeholder">
</ph>
페이지로 나누기 데모 녹화 이동하려는 페이지에 따라 다른 전환을 사용합니다.

이를 위해 뷰 전환 유형을 사용할 수 있습니다. 이 유형을 사용하면 활성 뷰 전환에 하나 이상의 유형을 할당할 수 있습니다. 예를 들어 페이징 시퀀스에서 더 높은 페이지로 전환할 때는 forwards 유형을 사용하고 더 낮은 페이지로 이동할 때는 backwards 유형을 사용합니다. 이러한 유형은 전환을 캡처하거나 실행할 때만 활성화되며 각 유형은 CSS를 통해 맞춤설정하여 서로 다른 애니메이션을 사용할 수 있습니다.

동일한 문서 뷰 전환에서 유형을 사용하려면 startViewTransition 메서드에 types를 전달합니다. 이를 위해 document.startViewTransition는 객체도 허용합니다. update는 DOM을 업데이트하는 콜백 함수이고 types는 유형이 있는 배열입니다.

const direction = determineBackwardsOrForwards();

const t = document.startViewTransition({
  update: updateTheDOMSomehow,
  types: ['slide', direction],
});

이러한 유형에 응답하려면 :active-view-transition-type() 선택기를 사용하세요. 타겟팅하려는 type를 선택기에 전달합니다. 이를 통해 하나의 선언이 다른 뷰의 선언을 방해하지 않고 여러 뷰 전환의 스타일을 서로 분리할 수 있습니다.

유형은 전환을 캡처하거나 실행할 때만 적용되므로 선택기를 사용하여 해당 유형의 뷰 전환에 대해서만 요소의 view-transition-name를 설정하거나 해제할 수 있습니다.

/* 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 (using the default root snapshot) */
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;
  }
}

다음 페이지 분류 데모에서는 이동하는 페이지 번호에 따라 페이지 콘텐츠가 앞뒤로 슬라이드합니다. 유형은 클릭 시 결정되어 document.startViewTransition에 전달됩니다.

유형과 관계없이 모든 Active View 전환을 타겟팅하려면 대신 :active-view-transition 의사 클래스 선택기를 사용하면 됩니다.

html:active-view-transition {
    
}

뷰 전환 루트에서 클래스 이름이 있는 여러 뷰 전환 스타일 처리

특정 유형의 뷰에서 다른 유형의 뷰로 전환할 때 특별히 조정된 전환이 필요한 경우가 있습니다. 또는 '뒤로' 탐색이 '앞으로' 탐색과 달라야 합니다.

'뒤로'로 이동할 때 전환이 다릅니다. 최소 데모 출처.

전환 유형 이전에는 이러한 사례를 처리하는 방법이 전환 루트에 클래스 이름을 일시적으로 설정하는 것이었습니다. document.startViewTransition를 호출할 때 이 전환 루트는 <html> 요소이며 JavaScript에서 document.documentElement를 사용하여 액세스할 수 있습니다.

if (isBackNavigation) {
  document.documentElement.classList.add('back-transition');
}

const transition = document.startViewTransition(() =>
  updateTheDOMSomehow(data)
);

try {
  await transition.finished;
} finally {
  document.documentElement.classList.remove('back-transition');
}

전환이 완료된 후 클래스를 삭제하기 위해 이 예에서는 전환이 종료 상태에 도달하면 해결되는 약속인 transition.finished를 사용합니다. 이 객체의 다른 속성은 API 참조에서 다룹니다.

이제 CSS에서 이 클래스 이름을 사용하여 전환을 변경할 수 있습니다.

/* 'Forward' transitions */
::view-transition-old(root) {
  animation: 90ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
    300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left;
}

::view-transition-new(root) {
  animation: 210ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in, 300ms
      cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
}

/* Overrides for 'back' transitions */
.back-transition::view-transition-old(root) {
  animation-name: fade-out, slide-to-right;
}

.back-transition::view-transition-new(root) {
  animation-name: fade-in, slide-from-left;
}

미디어 쿼리와 마찬가지로 이러한 클래스의 존재는 view-transition-name를 받는 요소를 변경하는 데도 사용할 수 있습니다.


다른 애니메이션을 동결하지 않고 전환 실행

동영상 전환 위치에 관한 이 데모를 살펴보세요.

<ph type="x-smartling-placeholder">
</ph>
동영상 전환 최소 데모 출처.

뭔가 문제가 있는 것을 발견하셨나요? 모르셨더라도 걱정하지 마세요. 속도를 늦춰서 보겠습니다.

동영상 전환이 느려집니다. 최소 데모 출처.

전환하는 동안 동영상이 멈춘 것처럼 보이다가 재생 버전의 동영상이 페이드 인됩니다. ::view-transition-old(video)는 이전 뷰의 스크린샷이고 ::view-transition-new(video)는 새 뷰의 라이브 이미지이기 때문입니다.

이를 수정할 수 있지만 먼저 수정할 가치가 있는지 자문해 보세요. 전환이 정상 속도로 재생될 때 '문제'가 보이지 않았다면 변경하지 않아도 됩니다.

정말로 문제를 해결하려면 ::view-transition-old(video)를 표시하지 마세요. ::view-transition-new(video)로 바로 전환합니다. 기본 스타일과 애니메이션을 재정의하면 됩니다.

::view-transition-old(video) {
  /* Don't show the frozen old view */
  display: none;
}

::view-transition-new(video) {
  /* Don't fade the new view in */
  animation: none;
}

이상입니다

동영상 전환이 느려집니다. 최소 데모 출처.

이제 전환이 진행되는 동안 동영상이 재생됩니다.


Navigation API(및 기타 프레임워크)와의 통합

뷰 전환은 다른 프레임워크나 라이브러리와 통합할 수 있는 방식으로 지정됩니다. 예를 들어 싱글페이지 애플리케이션(SPA)에서 라우터를 사용하는 경우 뷰 전환을 사용하여 콘텐츠를 업데이트하도록 라우터의 업데이트 메커니즘을 조정할 수 있습니다.

이 페이지로 나누기 데모에서 가져온 다음 코드 스니펫에서는 뷰 전환이 지원될 때 document.startViewTransition를 호출하도록 Navigation API의 가로채기 핸들러를 조정합니다.

navigation.addEventListener("navigate", (e) => {
    // Don't intercept if not needed
    if (shouldNotIntercept(e)) return;

    // Intercept the navigation
    e.intercept({
        handler: async () => {
            // Fetch the new content
            const newContent = await fetchNewContent(e.destination.url, {
                signal: e.signal,
            });

            // The UA does not support View Transitions, or the UA
            // already provided a Visual Transition by itself (e.g. swipe back).
            // In either case, update the DOM directly
            if (!document.startViewTransition || e.hasUAVisualTransition) {
                setContent(newContent);
                return;
            }

            // Update the content using a View Transition
            const t = document.startViewTransition(() => {
                setContent(newContent);
            });
        }
    });
});

일부 브라우저에서는 사용자가 스와이프 동작을 실행하여 탐색할 때 자체 전환을 제공합니다. 이 경우 사용자 환경이 저하되거나 혼란을 일으킬 수 있으므로 자체 뷰 전환을 트리거해서는 안 됩니다. 사용자는 두 개의 전환(브라우저에서 제공한 전환과 개발자에 의해 제공된 전환)이 연속으로 실행되는 것을 보게 됩니다.

따라서 브라우저가 자체 시각적 전환을 제공할 때는 보기 전환이 시작되지 않도록 하는 것이 좋습니다. 이렇게 하려면 NavigateEvent 인스턴스의 hasUAVisualTransition 속성 값을 확인합니다. 브라우저에서 시각적 전환을 제공한 경우 속성이 true로 설정됩니다. 이 hasUIVisualTransition 속성은 PopStateEvent 인스턴스에도 있습니다.

이전 스니펫에서 뷰 전환을 실행할지 여부를 결정하는 검사에서는 이 속성을 고려합니다. 동일 문서 뷰 전환이 지원되지 않거나 브라우저에서 이미 자체 전환을 제공한 경우에는 뷰 전환이 건너뜁니다.

if (!document.startViewTransition || e.hasUAVisualTransition) {
  setContent(newContent);
  return;
}

다음 녹음에서 사용자는 스와이프하여 이전 페이지로 돌아갑니다. 왼쪽 캡처에는 hasUAVisualTransition 플래그 검사가 포함되지 않습니다. 오른쪽 녹화에는 확인이 포함되어 있으므로 브라우저에서 시각적 전환을 제공했기 때문에 수동 뷰 전환이 건너뜁니다.

<ph type="x-smartling-placeholder">
</ph> <ph type="x-smartling-placeholder">
hasUAVisualTransition 확인이 없는 동일한 사이트 (왼쪽)와 너비 (오른쪽) 비교

JavaScript로 애니메이션 만들기

지금까지 모든 전환은 CSS를 사용하여 정의되었지만 CSS로는 충분하지 않은 경우도 있습니다.

<ph type="x-smartling-placeholder">
</ph>
서클 전환. 최소 데모 출처.

CSS만으로는 이러한 전환이 이뤄질 수 없는 부분도 있습니다.

  • 애니메이션은 클릭 위치에서 시작됩니다.
  • 원 가장자리가 가장 먼 모서리까지 반지름이 늘어나면서 애니메이션이 종료됩니다. 다만 향후 CSS에서도 이 작업이 가능하기를 바랍니다.

다행히 Web Animation API를 사용하여 전환을 만들 수 있습니다.

let lastClick;
addEventListener('click', event => (lastClick = event));

function spaNavigate(data) {
  // Fallback for browsers that don't support this API:
  if (!document.startViewTransition) {
    updateTheDOMSomehow(data);
    return;
  }

  // Get the click position, or fallback to the middle of the screen
  const x = lastClick?.clientX ?? innerWidth / 2;
  const y = lastClick?.clientY ?? innerHeight / 2;
  // Get the distance to the furthest corner
  const endRadius = Math.hypot(
    Math.max(x, innerWidth - x),
    Math.max(y, innerHeight - y)
  );

  // With a transition:
  const transition = document.startViewTransition(() => {
    updateTheDOMSomehow(data);
  });

  // Wait for the pseudo-elements to be created:
  transition.ready.then(() => {
    // Animate the root's new view
    document.documentElement.animate(
      {
        clipPath: [
          `circle(0 at ${x}px ${y}px)`,
          `circle(${endRadius}px at ${x}px ${y}px)`,
        ],
      },
      {
        duration: 500,
        easing: 'ease-in',
        // Specify which pseudo-element to animate
        pseudoElement: '::view-transition-new(root)',
      }
    );
  });
}

이 예에서는 전환 의사 요소가 성공적으로 생성되면 확인되는 프로미스인 transition.ready를 사용합니다. 이 객체의 다른 속성은 API 참조에서 다룹니다.


개선사항인 전환

View Transition API는 DOM 변경사항을 '래핑'하고 전환을 만들도록 설계되었습니다. 그러나 전환은 개선사항으로 취급되어야 하며, 앱에서 '오류'를 입력해서는 안 됩니다. DOM 변경에 성공하지만 전환은 실패하는 경우입니다. 전환이 실패하지 않는 것이 가장 좋지만 실패하더라도 나머지 사용자 환경이 손상되지 않아야 합니다.

전환을 개선사항으로 취급하려면 전환이 실패할 경우 앱이 발생하도록 전환 약속을 사용하지 않도록 주의하세요.

금지사항
async function switchView(data) {
  // Fallback for browsers that don't support this API:
  if (!document.startViewTransition) {
    await updateTheDOM(data);
    return;
  }

  const transition = document.startViewTransition(async () => {
    await updateTheDOM(data);
  });

  await transition.ready;

  document.documentElement.animate(
    {
      clipPath: [`inset(50%)`, `inset(0)`],
    },
    {
      duration: 500,
      easing: 'ease-in',
      pseudoElement: '::view-transition-new(root)',
    }
  );
}

이 예시의 문제는 전환이 ready 상태에 도달할 수 없는 경우 switchView()가 거부되지만 뷰가 전환되지 않았다는 의미는 아니라는 점입니다. DOM이 성공적으로 업데이트되었을 수도 있지만 중복된 view-transition-name이 있어 전환을 건너뛰었습니다.

다음을 수행합니다.

권장사항
async function switchView(data) {
  // Fallback for browsers that don't support this API:
  if (!document.startViewTransition) {
    await updateTheDOM(data);
    return;
  }

  const transition = document.startViewTransition(async () => {
    await updateTheDOM(data);
  });

  animateFromMiddle(transition);

  await transition.updateCallbackDone;
}

async function animateFromMiddle(transition) {
  try {
    await transition.ready;

    document.documentElement.animate(
      {
        clipPath: [`inset(50%)`, `inset(0)`],
      },
      {
        duration: 500,
        easing: 'ease-in',
        pseudoElement: '::view-transition-new(root)',
      }
    );
  } catch (err) {
    // You might want to log this error, but it shouldn't break the app
  }
}

이 예에서는 transition.updateCallbackDone를 사용하여 DOM 업데이트를 기다리고 실패하면 거부합니다. switchView는 전환이 실패해도 더 이상 거부되지 않고 DOM 업데이트가 완료되면 확인되고 실패하면 거부됩니다.

애니메이션 전환이 완료되거나 끝까지 건너뛴 것처럼 새 뷰가 '안정화'될 때 switchView가 확인되도록 하려면 transition.updateCallbackDonetransition.finished로 대체하세요.


폴리필은 아니지만…

이 기능은 쉽게 폴리필할 수 없습니다. 하지만 이 도우미 함수를 사용하면 뷰 전환을 지원하지 않는 브라우저에서 훨씬 더 쉽게 작업할 수 있습니다.

function transitionHelper({
  skipTransition = false,
  types = [],
  update,
}) {

  const unsupported = (error) => {
    const updateCallbackDone = Promise.resolve(update()).then(() => {});

    return {
      ready: Promise.reject(Error(error)),
      updateCallbackDone,
      finished: updateCallbackDone,
      skipTransition: () => {},
      types,
    };
  }

  if (skipTransition || !document.startViewTransition) {
    return unsupported('View Transitions are not supported in this browser');
  }

  try {
    const transition = document.startViewTransition({
      update,
      types,
    });

    return transition;
  } catch (e) {
    return unsupported('View Transitions with types are not supported in this browser');
  }
}

그리고 다음과 같이 사용할 수 있습니다.

function spaNavigate(data) {
  const types = isBackNavigation ? ['back-transition'] : [];

  const transition = transitionHelper({
    update() {
      updateTheDOMSomehow(data);
    },
    types,
  });

  // …
}

뷰 전환을 지원하지 않는 브라우저에서는 여전히 updateDOM가 호출되지만 애니메이션 전환은 없습니다.

전환 중에 <html>에 추가할 classNames를 제공하여 탐색 유형에 따라 전환을 더 쉽게 변경할 수도 있습니다.

뷰 전환을 지원하는 브라우저에서도 애니메이션을 원하지 않는 경우 trueskipTransition에 전달할 수 있습니다. 이 기능은 사이트에 전환을 사용하지 않는 사용자 환경설정이 있는 경우 유용합니다.


프레임워크 작업

DOM 변경을 추상화하는 라이브러리나 프레임워크로 작업하는 경우, DOM 변경이 언제 완료되었는지 아는 것은 까다로운 부분입니다. 다음은 다양한 프레임워크에서 위의 도우미를 사용하는 일련의 예입니다.

  • 반응: 여기서 핵심은 일련의 상태 변경사항을 동기식으로 적용하는 flushSync입니다. 예, 이 API 사용에 관한 큰 경고가 있지만 댄 아브라모프는 이 경우 적절하다고 확인해 주었습니다. React 및 비동기 코드와 마찬가지로 startViewTransition에서 반환된 다양한 약속을 사용할 때는 코드가 올바른 상태로 실행되고 있는지 확인해야 합니다.
  • Vue.js: 여기서 중요한 것은 DOM이 업데이트된 후에 실행되는 nextTick입니다.
  • Svelte: Vue와 매우 비슷하지만 다음 변경사항을 기다리는 메서드는 tick입니다.
  • Lit: 여기서 중요한 것은 구성요소 내의 this.updateComplete 약속으로, DOM이 업데이트되면 충족됩니다.
  • Angular: 여기서 키는 applicationRef.tick로, 대기 중인 DOM 변경사항을 플러시합니다. Angular 버전 17부터 @angular/router와 함께 제공되는 withViewTransitions를 사용할 수 있습니다.

API 참조

const viewTransition = document.startViewTransition(update)

ViewTransition를 시작합니다.

update는 문서의 현재 상태가 캡처되면 호출되는 함수입니다.

그런 다음 updateCallback에서 반환된 프로미스가 처리되면 다음 프레임에서 전환이 시작됩니다. updateCallback에서 반환된 프로미스가 거부되면 전환이 취소됩니다.

const viewTransition = document.startViewTransition({ update, types })

지정된 유형으로 새 ViewTransition를 시작합니다.

update는 문서의 현재 상태가 캡처되면 호출됩니다.

types는 전환을 캡처하거나 실행할 때 전환의 활성 유형을 설정합니다. 처음에는 비어 있습니다. 자세한 내용은 아래에 있는 viewTransition.types를 참고하세요.

ViewTransition의 인스턴스 멤버:

viewTransition.updateCallbackDone

updateCallback에서 반환된 프로미스가 처리되면 처리되고 거부되면 거부되는 프로미스입니다.

View Transition API는 DOM 변경사항을 래핑하고 전환을 만듭니다. 하지만 전환 애니메이션의 성공 여부는 중요하지 않고 DOM 변경이 발생하는지 여부와 시점만 알고 싶은 경우도 있습니다. updateCallbackDone는 해당 사용 사례에 사용됩니다.

viewTransition.ready

전환의 의사 요소가 생성되고 애니메이션이 시작되려고 하면 이 프로미스가 처리됩니다.

전환을 시작할 수 없는 경우 거부됩니다. 중복 view-transition-name와 같은 구성 오류 또는 updateCallback가 거부된 약속을 반환하는 경우 이러한 오류가 발생할 수 있습니다.

이는 JavaScript로 전환 의사 요소에 애니메이션을 적용하는 데 유용합니다.

viewTransition.finished

최종 상태가 사용자에게 완전히 표시되고 상호작용이 가능해지면 실행되는 약속입니다.

updateCallback가 거부된 약속을 반환하는 경우에만 거부됩니다. 이는 최종 상태가 생성되지 않았음을 나타냅니다.

그렇지 않고 전환이 시작되지 않거나 전환 중에 건너뛰어지면 종료 상태에 도달하므로 finished가 실행됩니다.

viewTransition.types

활성 뷰 전환 유형을 보유하는 Set 유형의 객체입니다. 항목을 조작하려면 해당 인스턴스 메서드 clear(), add(), delete()를 사용합니다.

CSS의 특정 유형에 응답하려면 전환 루트에서 :active-view-transition-type(type) 의사 클래스 선택기를 사용합니다.

뷰 전환이 완료되면 유형이 자동으로 삭제됩니다.

viewTransition.skipTransition()

전환의 애니메이션 부분을 건너뜁니다.

DOM 변경사항은 전환과 별개이므로 updateCallback 호출이 건너뛰지 않습니다.


기본 스타일 및 전환 참조

::view-transition
표시 영역을 채우고 각 ::view-transition-group를 포함하는 루트 의사 요소입니다.
::view-transition-group

절대 위치로 배치됩니다.

'이전' 사이의 widthheight를 전환합니다. 및 'after' 있습니다.

'이전' 뷰포트 공간 쿼드와 '이후' 뷰포트 공간 쿼드 간의 전환 transform입니다.

::view-transition-image-pair

그룹을 채우도록 절대적으로 배치됩니다.

mix-blend-mode가 이전 뷰와 새 뷰에 미치는 영향을 제한하는 isolation: isolate가 있습니다.

::view-transition-new::view-transition-old

래퍼의 절대 왼쪽 상단에 배치됩니다.

그룹 너비의 100%를 채우지만 높이는 자동이므로 그룹을 채우는 대신 가로세로 비율을 유지합니다.

실제 크로스페이드를 허용하는 mix-blend-mode: plus-lighter가 있습니다.

기존 뷰는 opacity: 1에서 opacity: 0로 전환됩니다. 새 뷰가 opacity: 0에서 opacity: 1로 전환됩니다.


의견

개발자 의견은 항상 환영합니다. 문의하려면 GitHub의 CSS 실무 그룹에 문제를 제출하여 제안 및 질문을 제출하세요. 문제 앞에 [css-view-transitions]를 붙입니다.

버그가 발생하면 대신 Chromium 버그를 신고하세요.