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

Intersection Observer v2에는 자체적으로 교집합을 관찰할 수 있을 뿐만 아니라 교차 시 교차하는 요소가 보였는지 감지하는 기능이 추가되었습니다.

Intersection Observer v1은 널리 사랑받는 API 중 하나이며, 이제 Safari에서도 지원되므로 최종적으로 모든 주요 브라우저에서 보편적으로 사용할 수 있습니다. API를 간단히 다시 살펴보려면 아래에 삽입된 Intersection Observer v1에 관한 SurmaSupercharged Microtip을 시청해 보세요. Surma의 심층 도움말도 확인할 수 있습니다. 사람들은 이미지 및 동영상의 지연 로드, 요소가 position: sticky에 도달할 때 알림, 분석 이벤트 실행 등 다양한 사용 사례에 Intersection Observer v1을 사용했습니다.

자세한 내용은 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 등과 같은 시각적 효과에 의해 수정되었는지는 알려주지 않습니다.

최상위 문서에 있는 요소의 경우 이 정보는 자바스크립트를 통해 DOM을 분석(예: DocumentOrShadowRoot.elementFromPoint()를 통해 분석한 다음 더 자세히 분석)하여 확인할 수 있습니다. 반면에 문제의 요소가 서드 파티 iframe에 있으면 동일한 정보를 얻을 수 없습니다.

실제 가시성이 왜 그렇게 중요한가요?

불행히도 인터넷은 나쁜 의도를 가진 악의적인 행위자들을 유인하는 공간입니다. 예를 들어 콘텐츠 사이트에 클릭당 지불 광고를 게재하는 정형화된 게시자는 사용자가 광고를 클릭하도록 유도하여 게시자의 광고 지급액을 늘리도록 유도할 수 있습니다 (적어도 광고 네트워크가 광고를 감지할 때까지 잠시) 일반적으로 이러한 광고는 iframe에서 게재됩니다. 이제 게시자가 사용자가 이러한 광고를 클릭하도록 유도하려면 CSS 규칙 iframe { opacity: 0; }를 적용하고 사용자가 실제로 클릭하고 싶은 귀여운 고양이 동영상 같은 매력적인 항목 위에 iframe을 오버레이하여 광고 iframe을 완전히 투명하게 만들 수 있습니다. 이를 클릭재킹이라고 합니다. 이 데모의 상단 섹션에서 이러한 클릭재킹 공격의 작동 방식을 확인할 수 있습니다 (고양이 동영상을 '시청'하고 '트릭 모드' 활성화). 사용자가 (비자발적으로) 광고를 클릭했을 때 완전히 투명했음에도 iframe의 광고가 합법적인 클릭이 발생한 것으로 '판단'한 것을 확인할 수 있습니다.

광고 스타일을 투명하게 지정하고 매력적인 콘텐츠 위에 오버레이하여 사용자가 광고를 클릭하도록 유도하는 행위

Intersection Observer v2로 이 문제가 어떻게 해결되나요?

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

spec의 중요한 세부정보는 구현에서 거짓음성을 보고할 수 있다는 것입니다 (즉, 타겟 요소가 완전히 표시되고 수정되지 않은 경우에도 isVisiblefalse로 설정). 성능 또는 기타 이유로 브라우저는 경계 상자 및 직선 도형으로 작동하도록 제한합니다. border-radius과 같은 수정으로는 완벽한 픽셀의 결과를 얻으려고 하지 않습니다.

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

실제로 새 코드는 어떤 모습일까요?

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

여기서 중요한 점은 trackVisibilitytrue일 때 delay100 이상이어야 한다는 것입니다 (즉, 100밀리초마다 알림을 1개 이하로 사용). 앞서 언급했듯이 가시성은 계산에 비용이 많이 들며 이 요구사항은 성능 저하와 배터리 소모를 방지하기 위한 조치입니다. 책임이 있는 개발자는 지연에 허용되는 가장 큰 값을 사용합니다.

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

  • 관찰자의 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에 있는 Sergey Semin의 히어로 이미지입니다.