Intersection Observer API를 활용한 무한스크롤

2021-05-10

클론프로젝트를 하면서 꼭 한번 해보고 싶었던 무한 스크롤 기능과 로딩바 기능을 구현해보았다!

기존 scroll event의 문제점

스크롤을 끊임없이 감지하고 동기적으로 실행되기 때문에 메인 스레드에 영향을 준다.
실제로 스크롤 이벤트로 무한스크롤기능을 구현할때의 상황.
scroll이 맨 밑에 있을때 감지해주는 함수를 달아놨는데 스크롤이 계속 맨 밑에 머물러있을때 끊임없이 함수가 호출되고 있다.

Intersection Observer API

간단히 말하자면 타겟이 화면에 노출되었는 지의 여부를 관찰할 수 있는 API
교차되었을때 실행할 callback 함수와 option을 인자로 받는다.

const observer = new IntersectionObserver(callback, options);

callback

타겟 엘리먼트가 교차되었을 때 실행할 함수

options

observer 콜백이 호출되는 상황을 조작할 수 있다.

  • root
    교차 영역의 기준이 될 root 엘리먼트.
    기본값은 브라우저의 viewport
  • rootMargin
    root엘리먼트의 margin값
    threshold
    타겟에 대한 교차 영역 비율
    0의 경우 : 타겟 엘리먼트가 교차영역에 진입했을 시점에 observer를 실행
    1의 경우 : 타켓 엘리먼트 전체가 교차영역에 들어왔을 때 observer를 실행

method

  • IntersectionObserver.observe(target)
    • 타겟 엘리먼트에 대한 IntersectionObserver를 등록 (관찰 시작)
  • IntersectionObserver.unobserve(target)
    • 타겟 엘리먼트에 대한 관찰 중지
  • IntersectionObserver.disconnect()
    • 모든 타겟 엘리먼트에 대한 관찰 중지

React hook에 적용해보기

우선 target, 로딩중인지 아닌지를 판별해줄 isLoading, 현재 아이템 갯수를 나타내는 itemCount를 state로 지정해준다.

const [itemCount, setItemCount] = useState(1);
const [isLoading, setIsLoading] = useState(false);
const [target, setTarget] = useState(null);

target에 변화가 있을때마다 실행해줄 useEffect에 observer 객체를 선언한다.

useEffect(() => {
  let observer;
  if (target) {
    observer = new IntersectionObserver(callback, { threshold: 1 });
    observer.observe(target);
  }
  return () => observer && observer.disconnect();
}, [target]);

callback함수의 인자로 entry와 observer를 받는다.
타겟의 교차 상태를 boolean값으로 반환하는 entry.isIntersectingtrue일때 list를 fetch하는 함수를 호출하고 itemCount에 1을 더해 setState해준다.
(난 한페이지당 1개의 리스트만 호출하고 스크롤이 끝에 다다랐을때 하나씩 더 보여주는 무한스크롤을 구현 예정이다.)

const callback = ([entry], observer) => {
  if (entry.isIntersecting) {
    fetchBidsLists();
    setItemCount(itemCount + 1);
    observer.observe(target);
  }
};

리스트를 fetch해오는 함수.
우선 isLoadingtrue로 setState해주고, fetch를 통해 백엔드로부터 데이터를 받아온다.
setTimeout을 통해 약간의 시간차를 줘 로딩바가 더 잘보일 수 있도록 했다.
list의 갯수가 현재 itemCount와 같으면 loading이 끝난것이기 때문에 다시 false로 바꿔주었고, list를 itemCount의 수만큼 slice해 setState해주었다.

const fetchBidsLists = () => {
  setIsLoading(true);
  setTimeout(() => {
    fetch(`${BIDS_API}/${day}`)
      .then((res) => res.json())
      .then((bidsData) => {
        setIsLoading(!(bidsData.auctions.length === itemCount));
        setBidsLists(bidsData.auctions.slice(0, itemCount));
      });
    setIsLoading(false);
  }, 400);
};

마지막으로 컴포넌트가 언마운트될때 isLoadingfalse로 setState해준다.

useEffect(() => {
  return () => setIsLoading(false);
}, []);

로딩바부분.
isLoadingtrue일때만 로딩바가 보여야하므로 조건부렌더링을 걸어주었고 setTarget을 forwardRef로 지정해주었다.
forwardRef는 React 컴포넌트에 ref prop을 넘겨서 그 내부에 있는 HTML 엘리먼트에 접근을 하게 해준다.

{
  isLoading && <Loading forwardRef={setTarget} />;
}