ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [web] Intersection Observer API와 활용
    programing/Web 2021. 5. 16. 14:39

    Intersection Observer API

    🚨 글을 작성하는 현재 시점에선 해당 API는 실험적인 기능임을 명심하자!

    특정 DOM 요소(이하 타겟 요소)와 그 상위 요소, 혹은 최상위 도큐먼트인 viewport(이하 루트 요소)와의 교챠영역에 대한 변화비동기적으로 감지할 수 있는 API.

    보통 스크롤이 어느 위치에 도달했을 때, 애니메이션 혹은 특정 로직을 트리거하기 위한 장치로 많이 사용한다.

    new IntersectionObserver 로 생성된 IntersectionObserver 인스턴스 객체는 설정된 비율 만큼의 가시성을 계속 감시한다. 이 생성자 함수는 다음과 같은 세가지 옵션을 설정할 수 있다.

    • root
      • 디폴트는 viewport. 교챠영역의 기준이 되는 요소.
    • rootMargin
      • 디폴트는 0, 0, 0, 0. 교챠영역을 계산할 때, 루트 요소에 더할 값.
    • threshold
      • 타겟 요소에 대한 루트 요소의 교챠 영역의 비율. 즉, 루트 요소 영역에 타겟 요소의 영역이 얼만큼 포함되어 있는지 나타내는 값. (배열도 가능하다.)
      • 교차 영역 비율이 해당 값을 지나갈 때(이상, 이하 둘 다), 콜백이 호출된다.
      • 0이라면 교차 영역이 1px이라도 넘으면 콜백을 호출한다는 뜻이며, 1이라면 교차 영역이 타겟 요소를 전부 포함해야 콜백을 호출한다는 뜻이다.

    타입들

    interface DomRectReadOnly {
      bottom: number,
      height: number,
      left: number,
      right: number,
      top: number,
      width: number,
      x: number,
      y: number,
    }
    interface IntersectionObserverEntry {
      boundingClientRect: DomRectReadOnly, // observable의 전체 레이아웃 정보
      intersectionRatio: number, // intersection root와 observable이 얼마나 교차하는지. (0 ~ 1)
      intersectionRect: DomRectReadOnly, // intersection root와 observable이 교차한 영역에 대한 레이아웃 정보
      isIntersecting: boolean, // intersection root와 교차 여부
      isVisible: boolean, 
      rootBounds: DomRectReadOnly, // intersection root의 레이아웃 정보
      target: HTMLElement, // observable 요소
      time: number, // IntersectionObserver 인스턴스가 생성된 시점을 기준으로, 이벤트가 일어난 시간
    }

    각 인터페이스에 대한 자세한 정보는 하단의 참고 섹션을 참고해주세요.

    전체적인 흐름

    1. 옵저버 객체를 생성한다.
    const observer = new IntersectionObserver((entries) => {
      const target = entries[0];
    });
    1. 옵저버 객체로 감시할 대상을 등록한다.
    const target = documnet.querySelector('#target');
    observer.observe(target);
    1. 콜백 함수를 로직에 맞게 수정한다.

    일반적인 경우(대상 요소가 완전히 벗어나거나 완전히 들어온 경우 트리거)에는 다음과 같은 사항만 체크하면 된다.

    • intersectionRatio1 인지 0 인지
    • isIntersectingtrue 인지 false 인지
    • isVisibletrue 인지 false 인지

    Navigation Bar 숨기기, 보이기

    💡 해당 섹션은 단일 요소를 관찰하는 방법에 대해 설명합니다.

    스크롤의 위치에 따라 네비게이션 바가 보여지고 숨겨지는 로직을 구현해보도록 하겠습니다.

    Next.js 에서는 컴포넌트 함수 내부에서는 document 객체에 접근할 수 없기 때문에, useEffect 내부에서 DOM 객체에 접근해야 합니다.

    useEffect(() => {
      // document 객체는 컴포넌트 함수 내에서 접근할 수 없다는 것을 명심!
      const preHeader = document.querySelector('#pre-header');
      const observer = new IntersectionObserver((entries) => {});
    
      return () => {
        observer.disconnect();
      };
    }, []);

    감시할 대상을 얻은 뒤, observer를 만듭니다. 클린업 함수에서 감시를 해제하는 것도 잊지 마세요!

    function isHTMLElement(target: any): target is HTMLElement {
      return target instanceof HTMLElement;
    }
    
    useEffect(() => {
      ...
      if (isHTMLElement(preHeader)) {
        observer.observe(preHeader);
      }
      ...
    }, []);

    타입가드 함수를 이용해 타입체크를 한 뒤, 감시합니다.

    const observer = new IntersectionObserver((entries) => {
        const target = entries[0];
    
        if (isHTMLElement(headerRef.current)) {
          if (target.intersectionRatio === 0) {
            headerRef.current.classList.add('scroll-down');
          }
          if (target.intersectionRatio > 0) {
            headerRef.current.classList.remove('scroll-down');
          }
        }
      });

    입맛에 맞게 콜백 함수를 수정합니다.

    결과

    스크롤을 내리면 경계선과 그림자가 생기며, 다시 스크롤이 맨 위로 붙으면 사라집니다.

    One Page Scroll

    💡 해당 섹션은 다중 요소를 관찰하는 방법에 대해 설명합니다.
    💡 해당 기능은 scroll-snap-type 속성과 scroll-snap-align 속성으로 쉽게 구현할 수 있습니다.

    이번에는 One Page Scroll (혹은 Full Page Scroll) 을 구현해보도록 하겠습니다.

    먼저, Body 컴포넌트를 구현합니다.

    import React, { useEffect } from "react";
    
    export function Body() {
      return (
        <div>
          <div className={"screen lightpink"}>first screen</div>
          <div className={"screen lightblue"}>second screen</div>
          <div className={"screen lightgreen"}>third screen</div>
          <div className={"screen lightcoral"}>fourth screen</div>
          <div className={"screen lightsteelblue"}>fifth screen</div>
        </div>
      );
    }

    여기서 각 스크린 요소가 100vh 를 가지도록 하는 것이 중요합니다.

    .screen {
      display: flex;
      align-items: center;
      justify-content: center;
      width: 100vw;
      height: 100vh;
      font-size: 4rem;
    }
    
    .lightpink {
      background-color: lightpink;
    }
    
    .lightblue {
      background-color: lightblue;
    }
    
    .lightgreen {
      background-color: lightgreen;
    }
    
    .lightcoral {
      background-color: lightcoral;
    }
    
    .lightsteelblue {
      background-color: lightsteelblue;
    }

    그리고 obserber 객체를 생성합니다.

    useEffect(() => {
        const observer = new IntersectionObserver(
          (entries) => {},
          {
            threshold: 0.1,
          }
        );
    
        return () => {
          observer.disconnect();
        };
      });

    이제 TS를 위한 유틸 함수를 정의합니다.

    function isHTMLElement(target: any): target is HTMLElement {
      return target instanceof HTMLElement;
    }
    
    function isNodeList(target: any): target is NodeList {
      return target instanceof NodeList;
    }

    useEffect 내에 관찰 로직을 구현합니다.

    const targets = document.querySelectorAll(".screen");
    if (isNodeList(targets)) {
      const nodeArray = Array.from(targets);
      const isAllDiv = nodeArray.every((node) => node.tagName === "DIV");
    
      if (isAllDiv) {
        nodeArray.forEach((node) => observer.observe(node));
      }
    }

    콜백 함수에 스크롤 기능을 구현합니다.

    (entries) => {
      const [observable] = entries.filter(
        (observable) => observable.isIntersecting
      );
    
      if (observable && isHTMLElement(observable.target)) {
        const offsetTop = observable.target.offsetTop;
        window.scroll({
          left: 0,
          top: offsetTop,
          behavior: "smooth",
        });
      }
    }

    결과

    번외! scroll-snap 이용해서 구현하기

    번외로, CSS의 scroll-snap 을 이용해, 더 편리하게 구현할 수 있습니다.

    먼저, 캐러셀처럼 크기가 동일한 자식 요소들과, 이것들을 담는 부모 요소를 정의합니다.

    <div className={"screen-container"}>
      <div className={"screen lightpink"}>first screen</div>
      <div className={"screen lightblue"}>second screen</div>
      <div className={"screen lightgreen"}>third screen</div>
      <div className={"screen lightcoral"}>fourth screen</div>
      <div className={"screen lightsteelblue"}>fifth screen</div>
    </div>

    여기서 중요한 점은, 부모 요소가 스크롤을 가질 수 있도록 하는 것입니다. 즉, 부모요소에 overflow 가 발생하면서, 해당 css 속성이 visible 같은 것이 아니여야 된다는 뜻이죠.

    .screen-container {
      overflow: scroll; /* visible 같은 속성은 피해주세요 */
      width: 100vw; 
      height: 100vh;
      scroll-snap-type: y mandatory; /* y축, 맨데토리 */
    }
    
    .screen {
      width: 100vw; /* 캐러셀처럼, 부모와 자식의 크기가 동일해야 보기 좋습니다. */
      height: 100vh;
      scroll-snap-align: start; /* 스냅 위치를 지정합니다. 자세히는 모르겠어요.. */
    }

    이렇게 구성하면 구현 끝!

    참고

    MDN - IntersectionObserver

    MDN - DOMRectReadOnly

    MDN - IntersectionObserverEntry

    댓글

Designed by black7375.