Intersection Observer로 무한 스크롤 구현하기

김혜진·2020년 2월 29일
132

javascript

목록 보기
6/9
post-thumbnail

잘못된 정보들이 있다면 지적 부탁드립니다.


최근 react를 사용한 면접 과제에서 여러 요구사항 중 하나로 페이지네이션 구현이 있었는데 window scroll 이벤트는 여러 번 써보기도 했고 이번에 새로 알게된 IntersectionObserver 적용하여 무한 스크롤을 구현보았다.

Intersection Observer

먼저 MDN의 설명에 따르면 Intersection Observer는 아래와 같다.

The Intersection Observer API provides a way to asynchronously observe changes in the intersection of a target element with an ancestor element or with a top-level document's viewport.
MDN 중

IntersectionObserver(교차 관찰자 API)는 타겟 엘레멘트와 타겟의 부모 혹은 상위 엘레멘트의 뷰포트가 교차되는 부분을 비동기적으로 관찰하는 API이다.

그렇다면 뷰포트는 무엇일까?

뷰포트(viewport)는 현재 화면에 보여지고 있는 다각형(보통 직사각형)의 영역입니다. 웹 브라우저에서는 현재 창에서 문서를 볼 수 있는 부분(전체화면이라면 화면 전체)을 말합니다. 뷰포트 바깥의 콘텐츠는 스크롤 하기 전엔 보이지 않습니다.
MDN 중

즉, Intersection Observer 란 화면(뷰포트) 상에 내가 지정한 타겟 엘레멘트가 보이고 있는지를 관찰하는 API이다.

 

어디에 적용할 수 있을까?

아래에서 MDN에서 나열한 IntersectionObserver를 적용할 수 있는 다양한 상황들을 보자.

  1. Lazy-loading of images or other content as a page is scrolled.
    페이지 스크롤 시 이미지를 Lazy loading할 때

  2. Implementing "infinite scrolling" web sites, where more and more content is loaded and rendered as you scroll, so that the user doesn't have to flip through pages.
    Infinite scrolling을 통해 스크롤을 하며 새로운 콘텐츠를 불러올 때

  3. Reporting of visibility of advertisements in order to calculate ad revenues.
    광고의 수익을 계산하기 위해 광고의 가시성을 참고할 때

  4. Deciding whether or not to perform tasks or animation processes based on whether or not the user will see the result.
    사용자가 결과를 볼 것인지에 따라 애니메이션 동작 여부를 결정할 때

출처 MDN

이중에서 나는 2번에 해당하는 Infinite scrolling 무한 스크롤을 통해 페이지를 넘기지 않고 콘텐츠를 불러오는 목적으로 사용하였으며, 글에서도 이에 대해 집중할 예정이다.

그런데 무한 스크롤 구현에서 이미 scroll evnet가 많이 사용되고 있는데 왜 굳이 IntersectionObserver를 사용해야 하는걸까?

 

왜 사용할까? - Scroll Event vs IntersectionObserver

Intersection Observer로 무한 스크롤을 구현함으로써 얻는 이득이 무엇이 있을지를 알기 위해서는 window.addEventListener을 이용한 스크롤 이벤트 구현 방식에 대해 알 필요가 있다.

내가 느끼고 또 공부하며 알게된 큰 차이점은 아래의 두 가지이다.

 

1. 호출 수 제한 방법 debounce, throttle을 사용하지 않아도 된다.

debouncethrottle은 스크롤 이벤트로 인해 발생하는 불필요한 함수 호출 수를 컨트롤하는 방법들인데 이들이 필요한 이유는

window.addEventListener('scroll', function() {
   return console.log('scroll!');
});

위의 코드를 콘솔 창에 입력하고 스크롤을 조금만 (정말 조금만) 해보면 단번에 이해할 수 있다.

위로 혹은 아래로 스크롤을 할 때마다 해당 함수가 수도 없이 호출된다.
대체로 이런 현상을 목적으로 스크롤 이벤트를 거는 것이 아니기 때문에 걷잡을 수 없이 호출되는 함수를debouncethrottle을 사용하여 컨트롤 하게 된다.

 

2. reflow를 하지 않는다.

스크롤 이벤트에서는 현재의 높이 값을 알기 위해offsetTop 을 사용하는데 정확한 값을 가져오기 위해 매번 layout을 새로 그리게 된다.

위의 자료에서 볼 수 있듯이 layout을 새로 그린다는 것은 렌더 트리를 재생성한다는 뜻인데, reflow라고도 불리우는 이 일련의 과정이 반복되면 당연히 브라우저의 성능이 저하되고 화면의 버벅거림이 생길 수 밖에 없다.

The simple reason is that whenever you scroll the browser needs to draw your site or application to the screen. That means we have the opportunity to minimize the work the browser has to do to get everything drawn and, by extension, maximize the page performance.
Scrolling Performance

찾아보니 작년 이맘때까지는 safari 지원이 안되었던 거 같은데 현재는 IE를 제외한 모든 브라우저에서 사용 가능하다ㅎㅎ..

브라우저 호환성 볼 때마다 생각나는 짤...ㅋㅋㅋ..

 

어떻게 적용할 수 있을까?

가장 중요하다.. 그래서 도대체 어떻게 사용할 수 있는가?..

parameters methods, properties

new IntersectionObserver (callback, options);

 

- parameters

callback: 관찰이 시작되는 시점에서 실행되는 함수. 2개의 parameters를 가진다.

entries: IntersectionObserverEntry 객체들을 배열로 반환
observer: IntersectionObserver instance

options: 관찰이 시작되는 상황에 대해 옵션을 설정할 수 있다. 기본 값이 정해져 있으므로 필수는 아님.

root: 교차 기준이 되는 엘리먼트. observer의 상위 엘리먼트여야 한다
default: null


rootMargin: root로 지정된 엘리먼트의 margin 값 설정
default: 상하좌우 모두 0px


thredhold: root 엘리먼트와 observer 엘리먼트가 얼만큼 교차되었는지
           0은 전혀 교차되지 않음, 1은 전체가 교차됨을 의미한다
default: 0

 
따라서 callback과 options를 작성했을 때 아래와 같은 코드가 된다.

new IntersectionObserver ((entires, observer) => {
  // ...
}, { rootMargin: 100px, thredhold: 0.5 });


// callback, options 별도 작성한 코드도 동일한 기능을 구현한다. 
new IntersectionObserver (_onIntersection, options);

const _onIntersection = (entires, observer) => {
  // ...
};

const options = { rootMargin: 100px, thredhold: 0.5 };

 

- methods

IntersectionObserver.observe(target): 관찰 시작
IntersectionObserver.unobserve(target): 관찰 종료
IntersectionObserver.disconnect(target): 관찰 멈추기

메소드는 이외에도 더 있다. 메소드명이 매우 직관적이넹..

무한 스크롤에서도 그랬지만 특수한 상황이 아니라면 observeunobserve 두가지를 가장 많이 쓰지 않을까 싶다.

 

- IntersectionObserverEntry properties

위에서 IntersectionObservercallback 함수의 파라미터에 배열로 들어가는 entries들이 사용할 수 있는 프로퍼티들이다. 여기서entirestargetElements로 이해해도 될 것 같다.

boundingClientRect: reflow현상 없이 Element.getBoundingClientRect()와 동일한 정보를 반환
isIntersecting: target이 root 영역에 교차되고 있는지의 정보를 boolean으로 반환

내가 시도하고 사용했던 프로퍼티는 위의 두 가지 뿐인데 이외에도 intersectionRect, target, time 등 다양한 프로퍼티들이 있다.

 

적용 코드

여러가지 방법으로 시도하였는데 최종적으로 react hooks의useState, useEffectref를 사용하여 구현된 코드를 설명하자면 이러하다.

  1. useEffectcomponentDidMount로 사용하여 최초의 데이터를 불러온다.

  2. 데이터를 불러오는 _fetchProductItems 함수에서 로딩 상태를 알 수 있도록 isLoaded상태 값을 dispatch 시킨다.

  3. 화면 최하단에 있는 엘레먼트를 ref로 잡아 isLoaded 상태 값에 따라 toggle되도록 하며 toggle시 setTarget(setState) 하여 내부 상태를 업데이트 한다.

  4. useEffectcomponentDidUpdate 로 사용하여 target 상태 변경을 감지할 수 있도록 한다.

  5. 위의 useEffect내에서 IntersectionObserver 인스턴스를 생성한다.

import React, { useEffect, useState, useContext } from 'react';
// ...

function ProductPage () {
  const [ target, setTarget ] = useState(null);
// ...

  const _fetchProductItems = () => {
    const productItems = apiProductItems(itemLength);

    if (!productItems.length) {
      actions.isLoaded(dispatch)(false);
      return;
    }

    // ...
  };

  useEffect(() => {
    let observer;
    if (target) {
      observer = new IntersectionObserver(_onIntersect, { threshold: 1 });
      observer.observe(target);
    }

    return () => observer && observer.disconnect();
  }, [ target ]);

  const _onIntersect = ([ entry ]) => {
    if (entry.isIntersecting) {
      _fetchProductItems();
    }
  };

// ...

  return (
    <>
      // ...
      {state.isLoaded && <div ref={setTarget}>loading</div>}
    </>
  );
}

export default ProductPage;

reflow 비용이 들지 않는다는 것만으로도 IntersectionObserver의 가치는 엄청나다고 생각한다.

또한 각각의 parameters, method, properties를 실험하며 파악해보면 사용하는 데에 큰 어려움은 없는 것 같다. 근데 막상 나도 코드를 짤 때에는 이것 저것 조합해서 불필요한 코드도 몇 줄 넣어뒀었다 허허 포스팅 쓰며 공부하다보니 뭔가 코드가 이상한 것 같은 느낌이 스믈스믈.. 들었는데 불필요한 것이 맞았다. 적기 위해 공부하다보니 생존할 수 있었다.. 적자생존.. 암튼 공부는 꾸준히!

2월 내로 이 포스팅을 끝내겠다고 다짐했는데 출근을 하다보니..😂 주말도 그리 여유롭지는 않지만 그래도 어떻게든 끝내 보고자 씀.. 그나마 출근 전에 좀 써놓을 것이 있어서 다행이었다.

최근 리액트 최적화 관련해서 스터디를 잠시 했었는데 위와 관련된 코드들을 다시 보니 적용할 수 있는 굉장히 부분들이 많다는 것을 깨달았다. 고로 다음 포스팅은 최적화로...


참고 및 더 읽을거리
MDN Intersection Observer
MDN scroll
Intersection Observer API의 사용법과 활용방법
Intersection Observer API
Scrolling Performance - reflows and repaints
Debouncing and Throttling Explained Through Examples

profile
꿈꿀 수 있는 개발자가 되고 싶습니다

10개의 댓글

comment-user-thumbnail
2020년 3월 1일

IE에선 polyfill을 사용하면 되겠군요!

https://github.com/w3c/IntersectionObserver/tree/master/polyfill

추후에 벨로그에서도 적용해보겠습니다!
유익한 포스트 감사합니다 😁

1개의 답글
comment-user-thumbnail
2020년 4월 8일

좋은글 감사합니다.

답글 달기
comment-user-thumbnail
2020년 7월 13일

좋은 글 잘 봤습니다! 감사합니다 :)

의문점이 있어서 댓글 남깁니다 ㅎㅎ

스크롤 이벤트에서는 현재의 높이 값을 알기 위해offsetTop 을 사용하는데 정확한 값을 가져오기 위해 매번 layout을 새로 그리게 된다.

라는 말이 있는데요,

스크롤이벤트가 일어날 때 마다 매번 layout을 그리고 reflow가 일어나는 얘기는 처음들었어서요!

개발자도구 퍼포먼스 탭에서 봤을때는 reflow가 안일어나는데, 혹시 offsetTop 값을 가져올 때 layout을 새로그린다는 정보관련해서 링크 달아 주실 수 있으실까요?

감사합니다 :)

2개의 답글
comment-user-thumbnail
2020년 11월 13일

IntersectionObserver에 관해서 깔끔하게 정리해주셔서 감사합니다 ㅎㅎ 큰 도움이 되었어요
예시를 보다가 궁금한 점이 생겼느데, 적용코드 3번에서 ref를 useRef와 쓰지 않으시고 useTarget 값을 걸어놓으셨는데요! 제가 리액트에서 dom 참조하는 부분은 useRef 사용법 밖에 몰라서 ㅜㅜ 혹시 실례가 안된다면 작성하신 부분의 참조 자료 같은 걸 링크로 공유해주실 수 있으신가요 ? 어떤 원리인지 궁금해서요 :)
감사합니다!

2개의 답글
comment-user-thumbnail
2022년 3월 7일

잘 봤습니다!

답글 달기