티스토리 뷰

개요

  • 무한스크롤을 구현하는 방법에는 스크롤의 높이와 클라이언트(화면)의 높이를 계산하는 스크롤 이벤트 기반 구현 방식도 있지만 (참고 사이트1,참고 사이트 2)
  • 저는 IntersectionObserver 라는 WebAPI를 이용해서 무한스크롤을 구현해보았습니다.
  • 스크롤 이벤트 기반 방식은 스크롤이 될 때마다 자바스크립트 엔진이 계속해서 함수를 호출하게 되는데
  • Intersection observer는 브라우저단에서만 동작하고 필요할 때만 자바스크립트 코드를 불러오므로 성능상에서도 이점이 있고
  • 무한 스크롤 외에도 여러 곳에서 사용할 여지가 있는 확장성이 있는 API라는 생각이 들었기 때문입니다!

 

동작 방식

  • IntersectionObserver의 동작 방식은 매우 직관적이고 심플합니다.
  1. 관찰자(observer) 생성
  2. 관찰 대상(target) 생성
  3. 관찰 대상이 조건을 만족할 때 콜백 함수 실행
  • 조건은 아래와 같은 3가지 경우입니다.
    • 관찰 대상(target)이 등록됐을 때
    • 대상이 화면에서 안보이다가 나타났을 때
    • 대상이 화면에서 보이다가 사라졌을 때
// 1.
const observer = new IntersectionObserver(callback, options) => {}, options)

// 2.
observer.observe(target);
  • 무한 스크롤을 구현할 때는 관찰 대상을 리스트 끝에 생성해두고 관찰 대상이 발견되었을 때(스크롤을 리스트 끝까지 내렸을 때) 새로운 데이터를 추가적으로 요청(callback)하면 되겠네요!

 

구현

useIntersectionObserver.tsx

const useIntersectionObserver = (callback: () => void) => {
  const [observeTarget, setObserveTarget] = useState(null);

  const observer = useRef<MutableRefObject<Element> | any>(null);

  useEffect(() => {
    observer.current = new IntersectionObserver(
      ([entry]) => {
        if (!entry.isIntersecting) return;
        callback();
      },
      { threshold: 1 },
    );
  });
  • 먼저 observer를 생성하고 엔트리를 탐색합니다.

  • 옵저버는 위와 같은 옵션을 갖습니다.
 
Options
  • root : 기본값 null인 경우 브라우저의 뷰포트. 타겟의 가시성을 검사하기 위한 객체를 지정하는 값입니다
  • rootMargin: root의 범위를 margin값을 이용해 확장하거나 축소할 수 있습니다.
  • threshold: 옵저버가 탐지를 하기 위해 타겟의 가시성이 얼마나 필요한지를 나타낸 숫자입니다. 0인 경우 root가 타겟을 만나는 즉시 실행되고 1인 경우 타겟이 완전히 노출되었을 때 실행됩니다.

 

  • entry는 IntersectionObserverEntry 인스턴스 배열인데 관찰하는 요소의 정보와 루트 요소의 정보가 들어있습니다.

Options
  • boundingClientRect: 관찰 대상의 사각형 정보
  • intersectionRect: 관찰 대상의 교차한 영역 정보
  • intersectionRatio: 관찰 대상의 교차한 영역 백분율(intersectionRect 영역에서 boundingClientRect 영역까지 비율)
  • isIntersecting: 관찰 대상의 교차 상태

  • rootBounds: 지정한 루트 요소의 사각형 정보)
  • target: 관찰 대상 요소
  • time: 변경이 발생한 시간 정보

 

useIntersectionObserver.tsx

  useEffect(() => {
    const currentTarget = observeTarget;
    const currentObserver = observer.current;

    if (currentTarget) currentObserver.observe(currentTarget);

    return () => {
      if (currentTarget) {
        currentObserver.unobserve(currentTarget);
      }
    };
  }, [observeTarget]);

  return setObserveTarget;
  • observe는 관찰 대상을 관찰하기 시작함을 의미하고 unobserve는 관찰을 중지함을 의미합니다.

 

InfiniteScroll.tsx

const InfiniteScroll = () => {
  const url = 'https://developer-lostark.game.onstove.com/markets/items';

  const [fetchData, page, setPage] = useFetch(url, 'infinite');
  const [observe, setObserve] = useState(false);

  console.log(fetchData);

  const pageHandler = () => {
    setPage(page + 1);
  };

  useEffect(() => {
    pageHandler();
    setObserve(false);
  }, [observe]);

  const setObserveTarget: React.Dispatch<SetStateAction<any>> =
    useIntersectionObserver(() => {
      setObserve(true);
    });

  return (
    <section>
      <h2 className="text-3xl text-center text-orange-700 my-8">
        infinite scroll page
      </h2>
      <DataList data={fetchData} usage={'infinite'} />
      <div
        ref={setObserveTarget}
        style={{ backgroundColor: 'blue', padding: '10px' }}
      />
    </section>
  );
};
  • 맨 아래에 div태그로 타겟을 지정해놓았습니다. 스타일을 준 것은 눈으로 타겟을 확인해보기 위해서입니다.
  • 이제 데이터 리스트 아래에 놓인 타겟이 탐지되면 page+1을 한 후 그 값을 body에 넣어서 추가적인 API 요청을 하게 됩니다.

 

useFetch

type FetchReturnType = [
  data: ItemsProps[],
  page: number,
  setPage: React.Dispatch<React.SetStateAction<number>>,
];

const useFetch = (url: string, usage: string): FetchReturnType => {
  const [fetchData, setFetchData] = useState<ItemsProps[]>([]);
  const [page, setPage] = useState(1);
  const APIkey = process.env.NEXT_PUBLIC_LOSTARK_API_KEY;

  useEffect(() => {
    fetch(url, {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${APIkey}`,
        'content-type': 'application/json;charset=UTF-8',
      },
      body: JSON.stringify({
        Sort: 'RECENT_PRICE',
        CategoryCode: 40000,
        CharacterClass: '',
        ItemTier: null,
        ItemGrade: '',
        ItemName: '',
        PageNo: page,
        SortCondition: 'ASC',
      }),
    })
      .then((res) => res.json())
      .then((data) =>
        usage === 'pagination'
          ? setFetchData(data?.Items)
          : setFetchData([...fetchData, ...data?.Items]),
      );
  }, [page]);

  return [fetchData, page, setPage];
};
  • 페이지네이션은 페이지가 넘어갔을 때 한 페이지당 정해진 데이터만 보여주면 되지만
  • 무한 스크롤은 데이터가 점점 쌓여서 스크롤이 생기는 형식이기 때문에 데이터를 누적시켜주어야 합니다.
  • 따라서 usage 에 따라 데이터를 다르게 저장하도록 하였습니다.

참고 자료

반응형
댓글
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/09   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30
글 보관함