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

🚀·2022년 5월 18일
0
post-thumbnail

기업 과제로 무한 스크롤을 구현하기 위해 Web API인 Intersection Observer를 처음 사용하게 되었다. Intersection Observer에 대해 정리해보려 한다.

Interserction Observer 란?


공식 문서

Web에서 제공하는 API로 교차 관찰자라고 한다. 관찰 중인 요소가 사용자가 보는 화면 영역 내에 들어왔는지를 알려주는 API이다. 대상 요소가 상위 요소나 또는 최상위 요소인 뷰포트와 교차하는 변경 사항을 비동기식으로 관찰하고, 그에 따른 콜백함수를 정해 실행할 수 있다.

따라서 이런 기능을 구현할 때 사용할 수 있다.
1. 페이지를 스크롤 할 때 이미지나 컨텐츠를 느리게 로드하는 lazy Loading
2. 스크롤할때 더 많은 콘텐츠를 불러오는 무한 스크롤

개념 및 사용법


Intersection Observer API를 사용하면 다음 상황 중 하나가 발생할 때 실행시키는 콜백을 만들 수 있다.

  1. 관찰할 대상이 뷰포트 또는 우리가 지정할 요소와 교차할때이다. 지정한 요소를 root 요소 또는 root라고 한다. 일반적으로 대상 요소의 스크롤이 가능한 바로 부모 대상 요소 또는 뷰포트를 대상으로 교차하는 것을 관찰할 것이다. 만약 뷰포트를 기준으로 교차를 관찰하려면 옵션에 null을 넣거나 아무것도 넣지 않으면 된다.
  2. 관찰자에게 처음으로 대상 요소를 관찰하라는 요청을 받을 때이다.

대상 요소와 루트 간의 교차 정도는 교차 비율로 판단한다. 자식요소의 얼마만큼의 비율이 루트 요소에 교차가 되었는지를 정하고 정한 만큼이 교차가 되었을 때 우리가 지정한 콜백함수를 실행한다. 이것은 옵션의 threshold와 관련이 있다. 0.0부터 1.0사이의 값으로 표시한다.

1. 관찰자 생성하기

관찰자를 생성해보자 ! 생성하면서 우리는 관찰자의 옵션을 설정할 수 있다.

// option 설정
const options = {
	root: document.querySelector('#scollArea'), // react의 경우 useRef를 이용해 설정도 가능.
	rootMargin: '0px',
	threshold: 0.7,
}

// 관찰자 생성
let observer = new IntersectionObserver(callback, options)
  1. root
    ⇒ 관찰할 대상이 교차할 요소이다. 지정하지 않거나 null일 경우 default 값은 뷰포트이고, 일반적으로 관찰할 대상의 스크롤이 가능한 상위 부모요소이다.
  2. rootMargin
    ⇒ 루트 주변의 여백이다. margin 속성과 유사한 값을 가질 수 있다. default 값은 0이다. (ex. “10px 20px 30px 40px”)
  3. threshold
    ⇒ 관찰할 대상과 root가 어느정도 교차할 때 콜백을 실행할 지에 대한 백분율을 나타내는 숫자이다. 관찰할 대상과 root가 50%가 교차할 때 콜백을 실행하고 싶다면 0.5라고 설정한다.

2. 관찰할 요소 타겟팅하기

관찰자를 생성한 후 관찰할 대상 요소를 지정해야 한다.

// 관찰할 대상 요소 지정
const target = document.querySelector('#listItem'); // react의 경우 useRef를 이용해 설정도 가능.

// 관찰자가 타겟을 관찰하도록 설정
observer.observe(target);

이렇게 관찰할 대상 요소를 지정한 후 관찰하도록 설정을 하면 이 지정한 임계값(threshold)가 충족할 때마다 IntersectionObserver 첫번째 인자로 넣어준 콜백이 호출된다.

콜백함수를 설정할 때 인자로 entries를 넣어주고, 이 entrie를 통해 임계값이 충족되었는지 또는 타겟 요소의 정보나 관찰할 대상의 요소에 대한 정보를 확인할 수 있다.

entries는 배열이고, 배열안에는 내가 관찰하기로 설정한 대상 요소들이 들어있다. entries를 콘솔로 확인해보자

// 콜백 함수
const intersectionCallback = (entries) => {
	console.log(entries)
}

이러한 값들이 들어 있는 것을 알 수 있다. 하나씩 값을 살펴보자면

  • boundingClientRect : 관찰 대상의 사각형 정보를 반환한다.
  • intersectionRect : 관찰 대상과 루트 요소의 교차하는 영역에 대한 사각형 정보를 반환한다.
  • intersectionRatio : 관찰 대상이 루트 요소와 얼마나 교차하는지 (겹치는지)의 숫자를 0.0과 1.0 사이의 숫자로 반환한다. 이는 intersectionRect 영역과 boundingClientRect 영역의 비율을 의미한다.
  • isIntersecting : 관찰 대상이 루트 요소와 교차 상태로 들어가거나(true) 교차 상태에서 나가는지(false) 여부를 나타내는 값(Boolean)이다. 보통 이 값이 true일 때 콜백 함수를 실행시킨다.
  • rootBounds : 루트 요소에 대한 사각형 정보를 반환한다. 이는 옵션 rootMargin에 의해 값이 변경되고, 반약 별도의 루트 요소를 선언하지 않았다면 null을 반환한다.
  • target : 관찰 대상을 반환한다.
  • time : 문서가 작성된 시간을 기준으로 교차 상태 변경이 발생한 시간을 나타낸다.

위의 값을 참고해서 콜백 함수를 작성해보자. 만약 관찰할 요소들을 여러개 지정해 주었다면 entries에는 2개 이상의 아이템이 들어있으므로 forEach를 통해 각 대상에 대해 콜백 함수를 실행한다.

const intersectionCallback = (entries) => {
	entries.forEach(entry => {}
}

나는 하나의 타겟만 설정해주었으므로 다음과 isIntersecting 이 true가 되었을 때 콜백 함수를 설정해주었다.

const handleObserver: IntersectionObserverCallback = (entry) => {
    if (entry[0].isIntersecting && !isLoading) {
      setTimeout(() => {
        getMoreMovieList();
      }, 1000);
    }
  };

나는 무비 리스트들의 맨 끝에 빈 div를 넣고 루트 요소를 리스트가 들어있는 스크롤이 가능한 컨테이너로 설정했다. 따라서 리스트들의 맨 밑에 위치한 div가 교차하는 지에 따라 데이터를 새로 받아오는 함수를 구현했다. 데이터를 새로 받아서 기존의 무비 리스트 뒤에 추가 해주는 식으로 무비 리스트들이 계속 증가하여 보여질 수 있도록 작성했다.

전체 작성 코드!! (기존 코드에서 짜깁기해서 가져와서 빠진 부분이 있을 수 있습니다..🙏🏻)

import { useEffect, useRef } from "react";

const MovieMain = () => {
	const [target, setTarget] = useState<HTMLDivElement | null>(null);

	const getMoreMovieList = async () => {
			// 데이터 받아 오는 함수
		}
 
// 콜백 함수
	const handleObserver: IntersectionObserverCallback = (entry) => {
	    if (entry[0].isIntersecting && !isLoading) {
	     getMoreMovieList();
	    }
	  };

// 관찰자 생성
	useEffect(() => {
	    let observer: IntersectionObserver;
	    if (target) {
	      observer = new IntersectionObserver(handleObserver, {
	        root: parentObservedTarget.current,
	        threshold: 1,
	      });
	      observer.observe(target);
	    }
	    return () => observer && observer.disconnect();
	  }, [handleObserver, target]);
	

	return (
	      <Container ref={parentObservedTarget}>
	        <SearchBar setIsLoading={setIsLoading} />
	        {movieList.Response === "True" ? (
	          <ul>
              <MovieListSubContainer>
                {movieList?.Search?.map((movie, i) => (
                  <MovieItem key={`${i}${movie.imdbID}`} item={movie} />
                ))}
                <div ref={setTarget}>{!isLoading && <Loading />}</div>
              </MovieListSubContainer>
              )}
          </ul>
	        ) : (
	          <NotFound error={movieList.Error} />
	        )}
	      </Container>
	  );
}

export default MovieMain;

잘 작동한다! ㅎㅎㅎㅎ

관련된 좋은 레퍼런스
리액트에서 Intersection Observer API 재사용성 높이기

0개의 댓글