신뢰는 좋고 관찰은 더 좋습니다: Intersection Observer v2

Intersection Observer v2는 교차 자체를 관찰하는 기능뿐만 아니라 교차 시 교차하는 요소가 표시되었는지 감지하는 기능도 추가합니다.

Intersection Observer v1은 전반적으로 인기 있는 API 중 하나이며 이제 Safari에서도 지원되므로 모든 주요 브라우저에서 사용할 수 있게 되었습니다. API를 빠르게 복습하려면 아래에 삽입된 IntersectionObserver v1에 관한 SurmaSupercharged Microtip을 시청하는 것이 좋습니다. Surma의 심층 도움말을 읽어보세요. Intersection Observer v1은 이미지 및 동영상 지연 로드, 요소가 position: sticky에 도달하면 알림 받기, 애널리틱스 이벤트 실행 등 다양한 사용 사례에 사용되었습니다.

자세한 내용은 MDN의 Intersection Observer 문서를 참고하세요. 간단히 참고로 가장 기본적인 경우에 Intersection Observer v1 API는 다음과 같습니다.

const onIntersection = (entries) => {
  for (const entry of entries) {
    if (entry.isIntersecting) {
      console.log(entry);
    }
  }
};

const observer = new IntersectionObserver(onIntersection);
observer.observe(document.querySelector('#some-target'));

Intersection Observer v1에서 어려운 점은 무엇인가요?

Intersection Observer v1은 훌륭하지만 완벽하지는 않습니다. API가 부족한 일부 특수한 경우가 있습니다. 자세히 살펴보겠습니다. Intersection Observer v1 API는 요소가 창의 뷰포인트로 스크롤되는 시점을 알려줄 수 있지만, 요소가 다른 페이지 콘텐츠로 가려졌는지(즉, 요소가 가려졌는지) 또는 요소의 시각적 디스플레이가 transform, opacity, filter와 같은 시각 효과로 수정되었는지(즉, 효과적으로 요소가 보이지 않게 되었는지) 알려주지는 않습니다.

최상위 문서의 요소의 경우 이 정보를 JavaScript를 통해 DOM을 분석(예: DocumentOrShadowRoot.elementFromPoint() 사용)한 후 더 자세히 살펴보면 확인할 수 있습니다. 반면 해당 요소가 서드 파티 iframe에 있는 경우에는 동일한 정보를 얻을 수 없습니다.

실제 노출 가시성이 중요한 이유는 무엇인가요?

안타깝게도 인터넷은 나쁜 의도로 악의적인 행위자를 끌어들이는 공간입니다. 예를 들어 콘텐츠 사이트에 클릭당 지불 광고를 게재하는 수상한 게시자는 광고 네트워크에서 적발될 때까지 최소한 단기간이라도 게시자의 광고 수익을 늘리기 위해 사용자를 속여 광고를 클릭하도록 유도할 수 있습니다. 일반적으로 이러한 광고는 iframe에서 게재됩니다. 사용자가 이러한 광고를 클릭하게 하려면 게시자가 CSS 규칙 iframe { opacity: 0; }을 적용하고 사용자가 실제로 클릭하고 싶어 할 귀여운 고양이 동영상과 같이 매력적인 것 위에 iframe을 오버레이하여 광고 iframe을 완전히 투명하게 만들 수 있습니다. 이를 클릭재킹이라고 합니다. 이 데모의 상단 섹션에서 이러한 클릭재킹 공격이 실제로 작동하는 것을 확인할 수 있습니다 (고양이 동영상을 '시청'하고 '트릭 모드'를 활성화해 보세요). iframe의 광고는 사용자가 (의도하지 않은 척) 클릭했을 때 완전히 투명하더라도 적법한 클릭을 받은 것으로 '생각'합니다.

투명하게 스타일을 지정하고 매력적인 내용 위에 오버레이하여 사용자가 광고를 클릭하도록 속입니다.

Intersection Observer v2는 이 문제를 어떻게 해결하나요?

Intersection Observer v2에서는 사람이 정의하는 타겟 요소의 실제 '가시성'을 추적하는 개념을 도입합니다. IntersectionObserver 생성자에서 옵션을 설정하면 교차하는 IntersectionObserverEntry 인스턴스에 isVisible라는 새 불리언 필드가 포함됩니다. isVisibletrue 값은 타겟 요소가 다른 콘텐츠에 의해 완전히 가려지지 않고 화면의 표시를 변경하거나 왜곡할 수 있는 시각 효과가 적용되지 않는다는 것을 기본 구현에서 강력하게 보장합니다. 반대로 false 값은 구현에서 이를 보장할 수 없음을 의미합니다.

사양의 중요한 세부정보는 구현에서 거짓 음성 (즉, 대상 요소가 완전히 표시되고 수정되지 않은 경우에도 isVisiblefalse로 설정)을 보고하도록 허용된다는 것입니다. 성능이나 기타 이유로 브라우저는 경계 상자와 직선 도형으로 작업하는 것으로 제한됩니다. border-radius와 같은 수정사항에 대해 픽셀 완벽한 결과를 얻으려고 하지 않습니다.

그러나 어떤 경우에도 거짓양성허용되지 않습니다(즉, 타겟 요소가 완전히 표시되지 않고 수정되지 않은 경우 isVisibletrue로 설정).

실제로 사용되는 새 코드는 어떤 형식인가요?

이제 IntersectionObserver 생성자가 추가 구성 속성 두 개(delaytrackVisibility)를 사용합니다. delay는 지정된 타겟에 대한 관찰자의 알림 간에 발생하는 최소 지연 시간(밀리초)을 나타내는 숫자입니다. trackVisibility는 관찰자가 대상의 공개 상태 변경사항을 추적할지 나타내는 불리언입니다.

여기서 중요한 점은 trackVisibilitytrue인 경우 delay100 이상이어야 한다는 것입니다(즉, 100ms마다 알림이 하나 이상 전송되지 않음). 앞서 언급했듯이 가시성은 계산 비용이 많이 들고, 이러한 요구사항은 성능 저하 및 배터리 소모에 대한 예방 조치입니다. 담당 개발자는 지연에 허용 가능한 최대 값을 사용합니다.

현재 사양에 따라 공개 상태는 다음과 같이 계산됩니다.

  • 관찰자의 trackVisibility 속성이 false이면 타겟이 표시되는 것으로 간주됩니다. 이는 현재 v1 동작에 해당합니다.

  • 타겟에 2D 변환 또는 비례 2D 업스케일링 이외의 유효한 변환 행렬이 있는 경우 타겟은 보이지 않는 것으로 간주됩니다.

  • 타겟 또는 포함 블록 체인의 요소에 1.0이 아닌 유효 불투명도가 있는 경우 타겟은 보이지 않는 것으로 간주됩니다.

  • 타겟 또는 타겟에 포함된 블록 체인의 요소에 필터가 적용된 경우 타겟은 보이지 않는 것으로 간주됩니다.

  • 구현에서 대상이 다른 페이지 콘텐츠에 의해 완전히 가려지지 않음을 보장할 수 없는 경우 대상은 보이지 않는 것으로 간주됩니다.

즉, 현재 구현은 가시성을 보장하는 데 매우 보수적입니다. 예를 들어 filter: grayscale(0.01%)와 같이 거의 눈에 띄지 않는 그레이 스케일 필터를 적용하거나 opacity: 0.99를 사용하여 거의 보이지 않는 투명도를 설정하면 모두 요소가 보이지 않게 렌더링됩니다.

다음은 새 API 기능을 보여주는 짧은 코드 샘플입니다. 데모의 두 번째 섹션에서 클릭 추적 로직이 작동하는 모습을 확인할 수 있습니다(지금은 강아지 동영상을 '시청'해 보세요). '트릭 모드'를 다시 활성화하여 즉시 사기성 게시자로 전환하고 Intersection Observer v2가 적법하지 않은 광고 클릭이 추적되는 것을 방지하는 방법을 확인하세요. 이번에는 Intersection Observer v2가 지원해 줍니다. 🎉

Intersection Observer v2를 사용하여 의도하지 않은 광고 클릭을 방지합니다.

<!DOCTYPE html>
<!-- This is the ad running in the iframe -->
<button id="callToActionButton">Buy now!</button>
// This is code running in the iframe.

// The iframe must be visible for at least 800ms prior to an input event
// for the input event to be considered valid.
const minimumVisibleDuration = 800;

// Keep track of when the button transitioned to a visible state.
let visibleSince = 0;

const button = document.querySelector('#callToActionButton');
button.addEventListener('click', (event) => {
  if ((visibleSince > 0) &&
      (performance.now() - visibleSince >= minimumVisibleDuration)) {
    trackAdClick();
  } else {
    rejectAdClick();
  }
});

const observer = new IntersectionObserver((changes) => {
  for (const change of changes) {
    // ⚠️ Feature detection
    if (typeof change.isVisible === 'undefined') {
      // The browser doesn't support Intersection Observer v2, falling back to v1 behavior.
      change.isVisible = true;
    }
    if (change.isIntersecting && change.isVisible) {
      visibleSince = change.time;
    } else {
      visibleSince = 0;
    }
  }
}, {
  threshold: [1.0],
  // 🆕 Track the actual visibility of the element
  trackVisibility: true,
  // 🆕 Set a minimum delay between notifications
  delay: 100
}));

// Require that the entire iframe be visible.
observer.observe(document.querySelector('#ad'));

감사의 말씀

이 도움말을 검토해 주신 시몬 빈센트, 요아브 와이스, 마티아스 바이넨스님과 Chrome에서 이 기능을 검토하고 구현해 주신 스테판 자거님께 감사드립니다. Unsplash의 세르게이 세민님 제공 히어로 이미지