중첩된 뷰 전환 그룹을 사용하여 뷰 전환 시 클리핑 문제 방지

게시일: 2025년 9월 22일

뷰 전환을 시작하면 브라우저가 view-transition-name로 태그된 요소의 전후 스냅샷을 자동으로 가져옵니다. 이러한 스냅샷은 의사 요소 트리로 렌더링됩니다. 기본적으로 생성된 트리는 '플랫'입니다. 즉, DOM의 원래 계층 구조가 손실되고 캡처된 모든 뷰 전환 그룹이 단일 ::view-transition 의사 요소 아래에 있는 형제가 됩니다.

이 플랫 트리 접근 방식은 많은 사용 사례에 충분하지만 이를 통해 달성할 수 없는 스타일 지정 사용 사례도 있습니다. 다음은 플랫 트리에서 예상치 못한 시각적 효과를 줄 수 있는 효과의 예입니다.

  • 클리핑 (overflow, clip-path, border-radius): 클리핑은 요소의 하위 요소에 영향을 미치므로 뷰 전환 그룹 형제는 서로 클리핑할 수 없습니다.
  • opacity, mask-image, filter: 마찬가지로 이러한 효과는 각 항목에 개별적으로 영향을 미치지 않고 트리의 완전히 래스터화된 이미지에서 작동하도록 설계되어 하위 요소에 영향을 미칩니다.
  • 3D 변환 (transform-style, transform, perspective): 3D 변환 애니메이션의 전체 범위를 표시하려면 일부 계층 구조를 유지해야 합니다.

다음 예는 DOM 트리의 상위 요소에 의해 잘린 요소가 있는 플랫 의사 트리를 보여줍니다. 이러한 요소는 뷰 전환 중에 클리핑이 손실되어 시각적 효과가 깨집니다.

뷰 전환이 실행되는 동안 깨진 클리핑 효과를 녹화합니다. 텍스트가 대화상자에 의해 잘려야 하지만 잘리지 않습니다. 애니메이션 시간이 느려져 효과가 과장됩니다.

중첩된 뷰 전환 그룹은 뷰 전환의 확장 프로그램으로, ::view-transition-group 가상 요소를 서로 중첩할 수 있습니다. 뷰 전환 그룹이 중첩되면 전환 중에 클리핑과 같은 효과를 복원할 수 있습니다.

Browser Support

  • Chrome: 140.
  • Edge: not supported.
  • Firefox: not supported.
  • Safari: not supported.

플랫 유사 트리에서 중첩된 유사 트리로

다음 데모에서 사용자의 아바타를 클릭하면 해당 사용자에 대한 자세한 정보를 확인할 수 있습니다. 애니메이션은 클릭한 버튼을 대화상자로 변환하고, 아바타와 이름을 화면 전체로 이동하고, 대화상자의 단락을 위아래로 슬라이드하는 동일한 문서 뷰 전환에 의해 처리됩니다.

실시간 데모

데모 녹화

데모 녹화 (느린 버전)

데모를 자세히 살펴보면 전환에 문제가 있음을 알 수 있습니다. 설명이 포함된 단락이 DOM의 <dialog> 요소의 하위 요소임에도 불구하고 전환 중에 텍스트가 <dialog> 상자로 클리핑되지 않습니다.

<dialog id="info_bramus" closedby="any">
  <h2><img alt="…" class="avatar" height="96" width="96" src="avatar_bramus.jpg"> <span>Bramus</span></h2>
  <p>Bramus is …</p>
  <p>…</p>
</dialog>

<dialog>overflow: clip을 적용해도 아무 작업도 실행되지 않습니다.

문제는 뷰 전환이 의사 트리를 빌드하고 렌더링하는 방식입니다.

  • 가상 트리에서 기본적으로 모든 스냅샷은 서로 형제입니다.
  • 의사 트리는 전체 문서 위에 렌더링되는 ::view-transition 의사 요소에 렌더링됩니다.

이 데모의 경우 DOM 트리는 다음과 같습니다.

html
  ├─ ::view-transition
  │  ├─ ::view-transition-group(card)
  │  │  └─ ::view-transition-image-pair(card)
  │  │     ├─ ::view-transition-old(card)
  │  │     └─ ::view-transition-new(card)
  │  ├─ ::view-transition-group(name)
  │  │  └─ ::view-transition-image-pair(name)
  │  │     ├─ ::view-transition-old(name)
  │  │     └─ ::view-transition-new(name)
  │  ├─ ::view-transition-group(avatar)
  │  │  └─ ::view-transition-image-pair(avatar)
  │  │     ├─ ::view-transition-old(avatar)
  │  │     └─ ::view-transition-new(avatar)
  │  ├─ ::view-transition-group(paragraph1.text)
  │  │  └─ ::view-transition-image-pair(paragraph1.text)
  │  │     └─ ::view-transition-new(paragraph1.text)
  │  └─ ::view-transition-group(paragraph2.text)
  │     └─ ::view-transition-image-pair(paragraph2.text)
  │        └─ ::view-transition-new(paragraph2.text)
  ├─ head
  └─ body
        └─ …

::view-transition-group(.text) 의사 요소는 ::view-transition-group(card) 의사 요소의 후속 형제이므로 카드 위에 그려집니다.

::view-transition-group(card) 클립 ::view-transition-group(.text)이 되려면 ::view-transition-group(.text) 의사 요소가 ::view-transition-group(card)의 하위 요소여야 합니다. 이를 위해 생성된 ::view-transition-group() 의사 요소에 '상위 그룹'을 할당할 수 있는 view-transition-group를 사용합니다.

상위 그룹을 변경하는 방법에는 두 가지가 있습니다.

  • 상위 요소에서 view-transition-groupcontain로 설정하여 view-transition-name가 있는 모든 하위 요소를 포함하도록 합니다.
  • 모든 하위 요소에서 view-transition-group을 상위 요소의 view-transition-name로 설정합니다. nearest를 사용하여 가장 가까운 상위 그룹을 타겟팅할 수도 있습니다.

따라서 이 데모에서 중첩된 뷰 전환 그룹을 사용하려면 코드가 다음과 같이 됩니다.

button.clicked,
dialog {
  view-transition-group: contain;
}

또는

button.clicked,
dialog *,
  view-transition-group: nearest;
}

이 코드를 사용하면 ::view-transition-group(.text) 의사 요소가 이제 ::view-transition-group(card) 의사 요소 내에 중첩됩니다. 이는 모든 중첩된 의사 선택자를 함께 유지하는 추가 ::view-transition-group-children(…) 의사 선택자에서 실행됩니다.

html
  ├─ ::view-transition
  │  ├─ ::view-transition-group(card)
  │  │  ├─ ::view-transition-image-pair(card)
  │  │  │  ├─ ::view-transition-old(card)
  │  │  │  └─ ::view-transition-new(card)
  │  │  └─::view-transition-group-children(card)
  │  │    ├─ ::view-transition-group(paragraph1.text)
  │  │    │  └─ ::view-transition-image-pair(paragraph1.text)
  │  │    │     └─ ::view-transition-new(paragraph1.text)
  │  │    └─ ::view-transition-group(paragraph2.text)
  │  │       └─ ::view-transition-image-pair(paragraph2.text)
  │  │          └─ ::view-transition-new(paragraph2.text)
  │  ├─ ::view-transition-group(name)
  │  │  └─ ::view-transition-image-pair(name)
  │  │     ├─ ::view-transition-old(name)
  │  │     └─ ::view-transition-new(name)
  │  └─ ::view-transition-group(avatar)
  │     └─ ::view-transition-image-pair(avatar)
  │        ├─ ::view-transition-old(avatar)
  │        └─ ::view-transition-new(avatar)
  ├─ head
  └─ body
        └─ …

마지막으로 ::view-transition-group(card)가 단락을 의사 클립하도록 하려면 ::view-transition-group-children(card) 의사에 overflow: clip을 적용합니다.

::view-transition-group-children(card) {
  overflow: clip;
}

결과는 다음과 같습니다.

실시간 데모

데모 녹화

데모 녹화 (느린 버전)

::view-transition-group-children 의사 클래스는 중첩된 그룹이 사용될 때만 표시됩니다. 원래 요소의 border-box 크기로 지정되며, 이전 예시의 card와 같이 가상 요소를 생성한 요소와 동일한 모양과 테두리 두께의 투명한 테두리가 지정됩니다.

클리핑 등

중첩된 뷰 전환 그룹은 클리핑 효과가 아닌 다른 곳에서 사용됩니다. 3D 효과도 그 예입니다. 다음 데모에는 전환 중에 카드를 3D로 회전하는 옵션이 있습니다.

html:active-view-transition-type(open) {
    &::view-transition-old(card) {
        animation-name: rotate-out;
    }
    &::view-transition-new(card) {
        animation-name: rotate-in;
    }
}
html:active-view-transition-type(close) {
    &::view-transition-old(card) {
        animation-name: rotate-in;
    }
    &::view-transition-new(card) {
        animation-name: rotate-out;
    }
}

중첩된 뷰 전환 그룹이 없으면 아바타와 이름이 카드와 함께 회전하지 않습니다.

실시간 데모

데모 녹화

데모 녹화 (느린 버전)

아바타와 이름 가상 요소를 카드 안에 중첩하면 3D 효과를 복원할 수 있습니다. 하지만 이것이 전부가 아닙니다. ::view-transition-old(card)::view-transition-new(card) 의사 선택자를 순환하는 것 외에도 ::view-transition-group-children(card) 의사 선택자도 순환해야 합니다.

html:active-view-transition-type(open) {
    &::view-transition-group-children(card) {
        animation: rotate-in var(--duration) ease;
        backface-visibility: hidden;
    }
}
html:active-view-transition-type(close) {
    &::view-transition-group-children(card) {
        animation: rotate-out var(--duration) ease;
        backface-visibility: hidden;
    }
}

실시간 데모

데모 녹화

데모 녹화 (느린 버전)

더 많은 데모

다음 예에서는 중첩된 뷰 전환 그룹을 사용하여 카드가 상위 스크롤러에 의해 잘리도록 합니다. 포함된 컨트롤을 사용하여 중첩된 뷰 전환 그룹의 사용을 사용 설정 또는 중지할 수 있습니다.

실시간 데모

데모 녹화

이 데모의 흥미로운 점은 모든 ::view-transition-group(.card) 의사 요소가 상위 ::view-transition-group(cards) 의사 요소 내부에 중첩되고 상위 ::view-transition-group(cards) 의사 요소에 의해 클리핑된다는 것입니다. #targeted-card::view-transition-group(cards)에 의해 진입/종료 애니메이션이 잘리면 안 되므로 제외됩니다.

/* The .cards wrapper contains all children */
.cards {
  view-transition-name: cards;
  view-transition-group: contain;
}

/* Contents that bleed out get clipped */
&::view-transition-group-children(cards) {
  overflow: clip;
}

/* Each card is given a v-t-name and v-t-class */
.card {
  view-transition-name: match-element;
  view-transition-class: card;
}

/* The targeted card is given a unique name (to style the pseudo differently)
   and shouldn't be contained by the ::view-transition-group-children(cards) pseudo */
#targeted-card {
  view-transition-name: targeted-card;
  view-transition-group: none;
}

요약

중첩된 뷰 전환을 사용하면 의사 요소를 구성할 때 DOM 트리의 일부 토폴로지를 유지할 수 있습니다. 이를 통해 이전에는 뷰 전환으로 불가능했던 다양한 효과를 사용할 수 있으며, 그중 일부는 여기에 설명되어 있습니다.

중첩은 뷰 전환이 구성되는 모델을 변경하며 고급 효과를 만드는 데 사용됩니다. 앞서 언급한 것처럼 요소 범위 뷰 전환을 사용하면 더 간단한 모델로 효과의 일부를 구현할 수도 있습니다. 두 기능을 모두 사용해 보고 필요에 가장 적합한 기능을 결정하는 것이 좋습니다.