무한스크롤 (Intersection Observer API)

Jinux·2022년 9월 16일
0

내 컴포넌트

목록 보기
1/1

Intersection Observer API를 사용한 무한스크롤 구현 과정을 기록했습니다.

무한스크롤이란 사용자가 특정 페이지 하단에 도달했을 때, API가 호출되며 콘텐츠가 계속 로드되는 UX를 말합니다. 페이지를 클릭하면 다음 페이지 주소로 이동하는 페이지네이션과 달리 스크롤만으로 새로운 컨텐츠를 볼 수 있게 되죠.

페이지네이션은 사용자에게 통제감을 제공하고 특정 항목의 위치를 파악할 수 있죠. 하지만 특정 컨텐츠를 찾는 것이 아닌 많은 컨텐츠를 탐색한 뒤 원하는 항목을 발견하는 걸 원할 경우엔 무한스크롤이 유용할 것 입니다.

알아보기

무한스크롤을 구현하는 데 간단한 방법으론 스크롤이벤트를 이용하여 구현해볼수도 있겠습니다. 저의 경우엔 Intersection Observer API를 사용하여 구현해보겠습니다.

스크롤이벤트 구현시 주의점

스크롤이벤트로 구현할때 주의할 점은 첫 데이터의 사이즈가 충분하지 않다면 스크롤바가 노출되지 않는 점과 스크롤 이벤트 핸들러에 쓰로틀 기법을 사용하여 API 호출빈도를 줄여야 합니다.
또, 쓰로틀이 setTimeout기반으로 동작할 것이기 때문에 콜스택 상태에 따라 원하는대로 동작하지 않을 수 있습니다. 이를 requestAnimationFrame을 사용하여 해결해야 합니다. requestAnimationFrame의 콜백은 태스크 큐보다 우선순위가 높은 Animation Frames에서 처리되어 setTimeout보다 실행시간을 보장할 수 있습니다.

Intersection Observer API는 스크롤이벤트로 무한스크롤을 구현했을 때 생기는 리플로우에 의한 렌더링 성능과 기대한 대로 동작하지 않을 수 있는 문제점을 해결하기 위해 사용하는 API입니다.

기본적으로 브라우저의 Viewport와 Target으로 설정한 요소의 교차점을 관찰하여 Target이 Viewport에 포함되는지 구별하는 기능을 제공합니다.

Intersection Observer API는 파라미터로 콜백과 옵션값을 받습니다.

new IntersectionObserver(callback[, options])

callback

타겟 엘리먼트가 교차되었을 때 실행할 함수로 entriesobserver를 파라미터로 받습니다.

entries

entries는 IntersectionObserverEntry 객체의 리스트로 배열 형식으로 반환합니다. 따라서 단일 타켓의 경우와 아닐 경우를 고려해야 합니다.

IntersectionObserverEntry 객체

이 객체의 정보로 어떤 동작을 등록할 때 유용하게 사용할 수 있습니다.

  • IntersectionObserverEntry.intersectionRatio: 교차 영역에 타겟 엘리먼트가 얼마나 교차되어 있는지(비율, 0.0~1.0)에 대한 정보를 반환합니다.
  • IntersectionObserverEntry.isIntersecting: 타겟 엘리먼트가 교차 영역에 있는 동안 true를 반환하고, 그 외의 경우 false를 반환합니다.
  • IntersectionObserverEntry.target: 타겟 엘리먼트를 반환합니다.
  • IntersectionObserverEntry.time: 교차가 기록된 시간을 반환합니다.

observer

콜백함수가 호출되는 IntersectionObserver를 나타냅니다.

options

/**
 * @param root target의 가시성을 확인할 때 사용되는 상위 속성 이름- null 입력 시, 기본값으로 브라우저의 Viewport가 설정됨
 * @param rootMargin root에 마진값을 주어 범위를 확장 가능- 기본값은 0px 0px 0px 0px이며, 반드시 단위 입력 필요
 * @param threshold 콜백이 실행되기 위해 target의 가시성이 얼마나 필요한지 백분율로 표시- 기본값은 배열 [0] 이며, Number 타입의 단일 값으로도 작성 가능
 */
interface IntersectionObserverInit {
   root?: Element | Document | null;
   rootMargin?: string;
   threshold?: number | number[];
}

정리하자면

  • root: 기본값으로 viewport를 가르킵니다.
  • rootMargin: root에 마진값을 주어 범위를 확장할 수 있습니다.
  • threshold: target의 가시성을 백분율로 표시합니다.

Methods

  • IntersectionObserver.observe(targetElement)
    타겟 엘리먼트에 대한 IntersectionObserver를 등록할 때(관찰을 시작할 때) 사용합니다.
  • IntersectionObserver.unobserve(targetElement)
    타겟 엘리먼트에 대한 관찰을 멈추고 싶을 때 사용하면 됩니다. 예를 들어 Lazy-loading(지연 로딩)을 할 때는 한 번 처리를 한 후에는 관찰을 멈춰도 됩니다. 이 경우에는 처리를 한 후 해당 엘리먼트에 대해 unobserve(targetElement)을 실행하면 이 엘리먼트에 대한 관찰만 멈출 수 있습니다.
  • IntersectionObserver.disconnect()
    다수의 엘리먼트를 관찰하고 있을 때, 이에 대한 모든 관찰을 멈추고 싶을 때 사용하면 됩니다.
  • IntersectionObserver.takerecords()
    IntersectionObserverEntry 객체의 배열을 리턴합니다.

구현하기

전체 코드


type IntersectHandler = (entry: IntersectionObserverEntry, observer: IntersectionObserver) => void;

/**
 * @param root target의 가시성을 확인할 때 사용되는 상위 속성 이름- null 입력 시, 기본값으로 브라우저의 Viewport가 설정됨
 * @param rootMargin root에 마진값을 주어 범위를 확장 가능- 기본값은 0px 0px 0px 0px이며, 반드시 단위 입력 필요
 * @param threshold 콜백이 실행되기 위해 target의 가시성이 얼마나 필요한지 백분율로 표시- 기본값은 배열 [0] 이며, Number 타입의 단일 값으로도 작성 가능
 */
interface IntersectionObserverInit {
  root?: Element | Document | null;
  rootMargin?: string;
  threshold?: number | number[];
}

export const useIntersect = (onIntersect: IntersectHandler, options?: IntersectionObserverInit) => {
  const ref = useRef<HTMLDivElement>(null);
  const callback = useCallback(
    (entries: IntersectionObserverEntry[], observer: IntersectionObserver) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) onIntersect(entry, observer);
      });
    },
    [onIntersect]
  );

  useEffect(() => {
    if (!ref.current) return;
    const observer = new IntersectionObserver(callback, options);
    observer.observe(ref.current);
    return () => observer.disconnect();
  }, [ref, options, callback]);

  return ref;
};

설명

  1. target이 옵저버로 감지되었다면 실행할 콜백을 만들어줍니다. root와 target이 교차상태인지 확인하는 isIntersecting 값을 사용합니다.
  const callback = useCallback(
    (entries: IntersectionObserverEntry[], observer: IntersectionObserver) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) onIntersect(entry, observer);
      });
    },
    [onIntersect]
  );
  1. 옵저버를 생성해 target을 등록합니다. 이 때 target이 사라진다면 등록을 취소해 관찰을 중지합니다.
    const observer = new IntersectionObserver(callback, options);
    observer.observe(ref.current);
    return () => observer.disconnect();
  1. 생성한 커스텀 훅을 호출하여 ref를 생성하고 target DOM 요소에 ref를 등록해줍니다. 저의 경우엔 loading 상태가 아닐 경우엔 target을 loading 인 경우엔 loading 컴포넌트를 렌더링 하였습니다.
  const ref = useIntersect(async (entry, observer) => {
    observer.unobserve(entry.target);
    if (!loading) {
      fetchData(page, 4);
    }
  });

...
      {loading ? <Loading /> : <Target ref={ref} />}
...

구현결과

참조

https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API
https://tech.kakaoenterprise.com/149
http://blog.hyeyoonjung.com/2019/01/09/intersectionobserver-tutorial/

0개의 댓글