(React) 무한 스크롤 기능 구현하기 : used by Intersection Observer - 2

호두파파·2022년 3월 7일
69

React

목록 보기
34/38
post-thumbnail

1편에 이어서, Intersection Observer를 이용해 무한 스크롤을 구현하는 방법에 대해서 정리하려고 한다. 본격적으로 코드를 살펴보기에 앞서 Intersection Observer에서 다루고 있는 몇 가지 개념에 대해서 이해를 하고 넘어가야 한다.

🖱Intersection Observer API 사용방법

// 타겟 요소 관측 시, 실행될 콜백 함수
const callback = (entries, observer) => {
  console.log('콜백함수');
};

// Observer 선언
const observer = new IntersectionObserver(callback, options);

// 타겟 요소 관측 시작
observer.observe(TargetElement);

// 타겟 요소 관측 중단
observer.unobserve(TargetElement);

// 모든 요소 관측 중단
observer.disconnect();

// 관측 중인 모든 요소를 배열 형태로 반환
observer.takeRecords();

Intersection Observer 객체는 처음 생성될 때 타겟으로 지정한 요소 관측 시에 실행할 콜백 함수와 옵션 내용이 포함된 객체를 파라미터로 받는다. 옵션 객체는 문자 그대로 옵션이기 때문에 선택사항이며, 파라미터로 넘기지 않을 시 기본값이 적용된다.

🛠 intersection Observer 객체에서 사용 가능한 메서드

1. observe(파라미터로 targetElement)

파라미터로 넘긴 타겟 요소에 대한 관측을 담당한다. 동일한 옵저버 객체로 여러번 호출을 해서 다양한 타겟 요소에 대해 관측이 가능하다.

2. unobserve(파라미터로 targetElement)

타겟 요소에 대한 관측을 중지하는 역할을 담당.

3. disconnect

모든 타겟 요소에 대한 관측을 중지한다.

4. takeRecord

현재 관측 중인 모든 타겟 요소들을 배열 형태로 반환한다.

🛠 option

const option = {
  root: null,
  rootMargin: 0px, 0px, 0px, 0px,
  threshold: 0,
}

default 값으로만 구성된 option 객체의 예시이다.

  • root(viewport) : 타겟 요소와 교차 영역을 정의하기 위해 사용하는 상위 요소 프로퍼티다. 만약 값을 넣지 않거나 null일 경우에는 브라우저의 viewport(보여지고 있는 요소)가 root로 지정된다.

  • rootMargin : root 요소에 적용되는 margin 값을 정의하기 위한 프로퍼티. 지정한 값만큼 교차 영역이 계산되며 루트의 범위가 축소하게 된다.

  • thresholds : 콜백함수를 실행시키기 위한 루트 영역과 타겟 요소와의 교차 영역 비율을 지정하는 프로퍼티.
    0.0과 1.0 사이의 값으로 지정한다. 값이 0이라면 타겟 요소가 교차 영역에 진입했을 때를 의미하며, 0.5라면 타겟 요소의 절반이 교차 영역에 들어왔을 때, 1.0이라면 완전히 교차 영역에 진입했을 때 콜백 함수가 실행된다.


🖱 Intersection Observer API 코드로 살펴보기

Intersection Observer API를 사용해 화면에 띄어준 콘텐츠 중에 맨 마지막 요소를 관측해 콜백함수를 실행하는 원리로 무한스크롤을 구현하게 된다.

타겟이 관측되었다면, 실행되는 콜백 함수 내에서 해당 요소에 대한 관측을 중지하고,
새로운 콘텐츠를 리스트에 추가한다. 그리고 다시 마지막 콘텐츠에 대해 관측을 시작하는 것이다.

❗️나는 가장 보편적으로 사용되는 방법으로 list요소의 가장 아래에 빈 div요소를 생성하고, 그 요소에 ref를 달아주는 방법을 시용할 것이다. 이 ref를 통해서 교차시점을 확인할 수 있다.

step 1: 관찰자를 생성한다.

const defaultOption = {
  root: null,
  threshold: 0.5,
  rootMargin: '0px'
};

const observer = new IntersectionObserver(checkIntersect, {
        ...defaultOption,
        ...option
      }

callback 함수 checkIntersect는 target을 주시하는 역할을 할 것이고, 두번째 파라미터로defaultOptin라는 객체값을 넘김으로써, 교차공간에 대한 프로퍼티를 전달해주었다.

step 2: 관찰대상 지정해주기

return (
    <Wrap>
      <Nav>
        ...
      </Nav>
      <ul>
        ...
      </ul>
      {isLoaded && <p ref={setRef}>Loading...</p>}
    </Wrap>

리스트 맨 아래에 관찰 대상을 만든다. 여기서 중요한 것은 ref={setRef}이다.

step 3: 관찰자를 만든다.

const checkIntersect = useCallback(([entry], observer) => {
    if (entry.isIntersecting) {
      onIntersect(entry, observer);
    }
  }, []);

관찰 대상은 하나이므로 콜백함수의 인자로 들어오는 [entry]observer를 파라미터로 전달해주었다. 그리고 이 entry의 속성인 isIntersecting을 이용해 조건을 검사하고, 조건에 해당하면 콜백 함수를 실행한다.

  • isIntersectig: 관찰 대상의 교차 상태를 boolean 값으로 반환한다.

📌 하지만, 관찰자, 관찰 대상, 조건, 콜백 함수를 만들었지만 이대로 실행한다면 원하는 결과를 얻을 수 없다. 왜냐하면 관찰 대상은 새로운 데이터를 가져올때마다 변해야 하기 때문이다.

❗️ 관찰 대상 수시로 변경해주기.

😇 리액트에서 의존배열의 값이 변할때마다 처리를 해주는 훌륭한 훅이 있으니, 바로 useEffect가 그것이다.
useEffect를 이용한 시나리오는 다음과 같다.

  1. 스크롤을 내린다.
  2. 관찰대상을 만나고, 조건을 만족시킨다.
  3. 새로운 데이터를 가져온다. 이때, 나는 리덕스를 사용했는데 파라미터로 함께 boolean값을 넘겨줘 관찰 대상이 사라지도록 만들어주었다.
  4. 새로운 데이터를 리스트에 추가한다. loading이 다시 false가 되었기 때문에 관찰 대상이 다시 랜더링된다.
  5. 관찰 대상은 다시 랜더링되었으나, 새롭게 만들어줬다. 이따 지워진 관찰대상은 리스트에서 제거했고, 새롭게 관찰대상을 지정해준다.

해당 시나리오를 진행할 로직은 다음과 같다.

  useEffect(() => {
    let observer; // (1)beserver 변수를 선언해주고
    if (ref) { // (2) 관찰대상이 존재하는지 체크한다.
      observer = new IntersectionObserver(checkIntersect, { 
        ...defaultOption,
        ...option
      }); // (3) 관찰대상이 존재한다면 관찰자를 생성한다.
      observer.observe(ref); // (4) 관찰자에게 타겟을 지정한다.
    }
    return () => observer  && observer.disconnect(); // 의존성에 포함된 값이 바뀔때 관찰을 중지한다.
  }, [ref, option.root, option.threshold, option.rootMargin, checkIntersect]);

🖱 로직 커스텀 훅으로 만들기

Intersection Observe는 오직 무한 스크롤을 구현하기 위한 개념이 아니다!
지연 로딩이나, 스켈레톤 UI를 구현할 때도 사용되기 때문에 로직을 분리해 사용한다면 프로잭트 곳곳에서
유용하게 사용할 수 있다.

import { useState, useEffect, useCallback } from 'react';
//  옵션 값을 지정한다.
const defaultOption = {
  root: null,
  threshold: 0.5,
  rootMargin: '0px'
};
//  커스텀 훅 부분
// 관찰 대상을 지정할 수 있도록 ref값을 useState 훅을 이용해 state로 관리해준다.
// 관찰자를 만들어준다.
const useIntersect = (onIntersect, option) => {
  const [ref, setRef] = useState(null);
  const checkIntersect = useCallback(([entry], observer) => {
    if (entry.isIntersecting) {
      onIntersect(entry, observer);
    }
  }, []);
// 관찰자가 언제 관찰하는지, 관찰을 종료하는지에 대해 로직을 구현해준다.
  useEffect(() => {
    let observer;
    if (ref) {
      observer = new IntersectionObserver(checkIntersect, {
        ...defaultOption,
        ...option
      });
      observer.observe(ref);
    }
    return () => observer  && observer.disconnect();
  }, [ref, option.root, option.threshold, option.rootMargin, checkIntersect]);
  return [ref, setRef];
}

export default useIntersect;

🖱 프로젝트에 적용해보기

import useIntersect from '../utils/useIntersect';

const Lists = () => {
  const dispatch = useDispatch();

  const { apiData, isLoaded, pageCount } = useSelector(state => ({
    apiData: state.apiData.data,
    isLoaded: state.apiData.isLoaded,
    pageCount: state.pageReducer.pageCount,
  }));

  const page = useRef(pageCount);

  useEffect(() => {
    dispatch(getDataFromApi(pageCount, true));
    dispatch(getPageData(page.current));
  }, []);

  const [_, setRef] = useIntersect(async(entry, observer) => {
    observer.unobserve(entry.target);
    await dispatch(getPageData(page.current++));
    await dispatch(getDataFromApi(page.current, true));
    observer.observe(entry.target);
  }, {});

  return (
    <Wrap>
      <Nav>
    	...
      </Nav>
      <ul>
        {apiData.map((item, idx) => {
          ...
        })}
      </ul>
      {isLoaded && <p ref={setRef}>Loading...</p>}
    </Wrap>
  );
};
  1. useEffect를 사용해 데이터를 로드한다.
  2. 데이터를 로드하며 전역에서 관리하고 있는 isLoaded 상태 값을 바꿔준다.
  3. 화면 최하단에 있는 엘리먼트를 ref로 잡아 isLoaded 상태 값에 따라 랜더링시켜준다.
  4. useEffect를 사용해 타겟 요소의 상태 변경을 감지한다.
  5. useEffect내에서 Intersection Observer 인스턴스를 생성한다.

결과는 다음과 같다.

Intersection Observer를 사용했을 때

Scroll event와 쓰로틀링을 사용했을 때


정리

Intersection Observer를 사용하면 reflow를 일으키지 않는다는 것만으로도 사용가치가 충분하다고 생각한다. 데이터 요청도 필요한 순간에만 이루어지고 있어 불필요한 캐싱을 방지하는데도 효과가 탁월했다.

막상 Intersection Observer를 사용하려고 했을때 숙지해야할 정보는 많았지만 훌륭하게 정보를 정리해준 글들이 많았기에 그리 어렵지 않게 코드를 작성할 수 있었다.

조금 더 쉽게 코드를 구현한 분들의 자료를 참조하면서 후에 한 번 더 리팩토링을 진행해봐야겠다.

이 글을 읽는 누군가가 Intersection Observer에 대한 부담감을 내려놓을 수 있을수 있기를 기대해본다. 😇


참조

profile
안녕하세요 주니어 프론트엔드 개발자 양윤성입니다.

4개의 댓글

comment-user-thumbnail
2022년 3월 17일

좋은 글 보고 프로젝트에 적용 시켰습니다☺️

1개의 답글
comment-user-thumbnail
2022년 12월 28일

리액트로 무한 스크롤을 구현해야 했는데, 덕분에 빠르게 적용할 수 있었습니다. 감사합니다 :)

답글 달기