Intersection Observer

Intersection Observer

인터섹션 옵저버. 교차관찰자 정리

인터섹션 옵저버란 한마디로 타겟으로 삼은 요소가 화면에 노출되었는지 여부를 간단하게 구독할 수 있는 API이다. 스크롤 이벤트로 이를 구현하려면 scroll이 일어날때마다, 그 요소가 존재하는지 매번 계산해야하는데 intersection observer를 사용하면 성능향상을 누릴 수 있다.
MDN에서는 인터섹션옵저버의 필요성을 다음과 같은 예를들어 설명하고 있다.

  1. 페이지 스크롤 시 이미지를 lazy-load할 때
  2. 무한스크롤을 통해 스크롤할 때 새로운 컨텐츠를 불러올 때
  3. 광고의 수익을 계산하기 위해 광고의 가시성을 참고할 때
  4. 사용자가 결과를 볼 것인지에 따라 애니메이션 동작여부를 결정할 때

3번의 예시와 비슷한 내용이 필요했는데 예제없이 내용을 이해하기는 힘들었고, 보통 주로 사용되는 예제들이 1번이나 2번뿐이었다. 그래서 이 인터섹션옵저버가 많이 괴롭혔고, 지금도 괴롭히고있고 앞으로 게속 내용을 보충하며 채워야겠다.
new IntersectionObserver()를 통해 인스턴스화 하여 관찰자를 초기화 하고 대상을 지정한다. 생성자는 2개의 인수 콜백함수와 옵션을 받는다.

const io = new IntersectionObserver(callback, options)
io.observe(element)

callback

관찰할 대상이 등록되거나 가시성에 변화가 생기면 콜백을 실행한다. 콜백은 2개의 인자 entires, observer 를 받는다

const io = new IntersectionObserver((entries, observer) => {}, options)

entries

entries는 intersectionObserverEntry 인스턴스의 배열이다. 엔트리는 읽기전용의 속성들을 포함한다.

  1. boundingClientRect
  2. intersectionRect
  3. intersectionRatio
  4. isIntersecting
  5. rootBounds
  6. target
  7. time
boundingClientRect

관찰 대상의 사각형 정보를 반환하며 이 값은 el.getBoundingClientRect()를 사용해 동일하게 얻을 수 있다. 단 getBoundingClientRect는 호출에서 reflow현상이 발생한다.

intersectionRect

관찰 대상의 교차한 영역 정보를 반환한다.

intersectionRatio

관찰 대상의 교차한 영역 백분율 즉 루트요소와 얼마나 겹치는지의 수치를 0과 1사이의 숫자로 반환한다.

isIntersecting

관찰 대상의 교차 상태를 반환한다 루트요소와 교차상태로 들어가있는지 아닌지 true 혹은 false값으로 반환한다.

rootBounds

지정한 루트 요소의 사각형 정보를 반환한다. 이는 옵션 rootMargin에 의해 값이 변경되며 별도의 루트 요소를 선언하지 않았을경우 null을 반환한다.

target

관찰대상을 반환한다.

time

문서가 작성된 시간을 기준으로 교차 상태 변경이 발생한 시간을 반환한다.

observer

콜백이 실행되는 해당 인스턴스를 참조한다.

options

root

타겟의 가시성을 검사하기 위해 뷰포트 대신 사용할 요소 객체를 지정한다. 타겟의 조상요소이어야 하며 지정하지 않거나 null 일 경우 브라우저의 뷰포트가 기본으로 사용된다 기본값은 null

const io = new IntersectionObserver(cb, {
  root: document.getElementById('root'),
})

rootMargin

바깥 여백을 이용해 루트 범위를 확장하거나 축소할 수 있다. css 마진과 같이 4단계로 여백을 설정할 수 있으며, px 또는 %로 나타낼 수 있다. 기본값은 0px 0px 0px 0px 이며 단위를 꼭 입력해야 한다.

const io = new IntersectionObserver(cb, {
  rootMargin: '200px 0px',
})

threshold

옵저버가 실행되기 위해 타겟의 가시성이 얼마나 필요한지 백분율로 표시한다. 기본값은 Array타입의 [0] 이지만 Number타입의 단일 값으로도 작성할 수 있다.
0: 타겟의 가장자리가 root 범위를 교차하는 순간 옵저버가 실행 0.5: 가시성이 50%일 때 옵저버가 실행 [0, 0.5, 1]: 가시성이 0, 50%, 100%일 때 모두 옵저버가 실행

const io = new IntersectionObserver(cb, {
  threshold: 0.5,
})

methods

observer()

대상 요소의 관찰을 시작한다.

const io1 = new IntersectionObserver(cb, options)
const io2 = new IntersectionObserver(cb, options)

const div = document.querySelector('div')
const li = document.querySelector('li')

io1.observe(div) // div관찰
io2.observe(li) // li관찰

unobserve()

대상 요소의 관찰을 중지한다. 관찰을 중지할 하나의 대상 요소를 인자로 지정해야 한다. 만약 관찰하고있지 않은 대상을 인자로 넣게되면 아무런 동작도 하지 않는다.

const io1 = new IntersectionObserver(cb, options)
const io2 = new IntersectionObserver(cb, options)

const div = document.querySelector('div')
const li = document.querySelector('li')

io1.observe(div) // div관찰
io2.observe(li) // li관찰

io1.unobserve(div) // 관찰중지
io1.unobserve(li) // 아무일 없어요

콜백의 두번째 인자 observer가 해당 인스턴스를 참조하므로, 이렇게 작성할 수 있다.

const io = new IntersectionObserver((entiries, observer) => {
  entries.forEach(entry => {
    // 가시성의 변화가 있을때 콜백실행
    // 관찰 대상의 교차상태가 false이면 실행하지 않는다.
    if (!entry.isIntersecting) {
      return
    }
    // 교차상태 true일때 내용
    // 1회 관찰 후 관찰 중지한다면
    observer.unobserve(entry.target)
  })
})

disconnect()

인터섹션옵저버 인스턴스가 관찰하는 모든 요소의 관찰 중단.

const io1 = new IntersectionObserver(cb, options)

const div = document.querySelector('div')
const li = document.querySelector('li')

io1.observe(div) // div관찰
io1.observe(li) // li관찰

io1.disconnect() // div, li 관찰중지

takeRecords()

IntersectionObserverEntry 객체의 배열을 반환한다. 일반적인 상활에서 쓸 일이 없다고 한다.

노출 확인

위에서도 그랬지만 예제들을 둘러보면 무한스크롤이나 이미지레이지로딩은 예제들이 많았는데 세부적인 예제는 없고 React에서 적용하려니 쉽지 않았다.

요소들 관찰 등록하기

예를들어 화면에 이미지들이 뷰포트상에 노출되었는지 알고싶다면 어떻게해야할까 고민을했다. 처음에 했던 생각은 이와같다.

function checkFunc() {
  const io = new IntersectionObserver((entries, observer) => {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        console.log('화면에 보여요')
        observer.unobserve(entry.target)
      }
    })
  }, options)

  const imgList = document.querySelectorAll('img')

  imgList.forEach(el => {
    io.observe(el)
  })
}

이렇게 쿼리셀렉터로 이미지들을 긁어모아 관찰 등록을 했다. 하고 나니 관찰이 안되더라. 정확한 이유는 잘 모르겠지만 아마 이미지들은 마운트 되고 그려지는데 약간 시간이 걸리는데 이미지가 그려지기 전에 등록이 되어서 그랬던것같다. setTimeout함수로 그 시간을 벌면 등록이 되고 관찰도 된다. 하지만.. 리액트 프로젝트를 진행하고있는데 쿼리셀렉터는 아니다. 아마 분명히 다른 방법이 있지않을까. ref로 달아서 사용할 수 있지 않을까 고민을 해보았다.

...

const observer = () => {
  const [data, setData] = useState([])

  const refs = useRef(data.map(React.createRef))
  // 데이터배열의 수만큼 레프를 배열에 담는다

  console.log(refs)
  // 잘 담기는지 확인

  const checkIntersect = (entries, observer) => {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        observer.unobserve(entry.target)
        // 1회 관찰 후 관찰 삭제
        console.log('화면에 보여요')
      } else {
        console.log('화면에 안보여요')
      }
    })
  }

  useEffect(() => {
    let observer;

    // refs.current 배열에 요소들이 채워질 때 실행한다
    if (refs.current.length > 0) {
      observer = new IntersectionObserver(checkIntersect, {
        threshold: 0.5
      });
    // 50퍼센트 이상 노출 시 콜백 실행

    // 관찰 등록
      refs.current.forEach(el => {
        observer.observe(el.current)
      })
    }
  }, [refs])

  ....

  return (
    <div>
    {data.map((el,index) => (
        <img
        ref={refs.current[index]}
        src={el.img}
        alt={img}
        />
    ))}
    </div>
  )
}

가볍게 정리했던 내용이다. 처음에는 ref하나 잡고 반복문 돌면서 ref값을 넣어주니 계속 마지막 값 하나만 들어가서 적용이 안되었는데. 이렇게 각 요소들에 ref를 달아서 ref배열을 만들고 그 요소들을 관찰자에 등록을 해준다. 마법이 완성되었다. 이 과정이 매우 어려웠는데 일단 관찰이 잘 작동되서 좋고 천천히 가다듬어야겠다.


Written by@jaeyoung-son
배운것을 기록하는 공간입니다.

GitHub