1) 스크롤 이벤트를 사용한 방법


무한 스크롤은 현재 페이지에서 스크롤바가 마지막 콘텐츠 지점에 있을 때 다음 콘텐츠를 자동으로 불러오는 구현 방식을 말한다. ➊스크롤해서 가려진 영역의 높이와 ➋현재 화면(뷰포트)의 높이를 더한 값이 ➌전체 문서의 높이와 같다면 현재 스크롤이 가장 하단 끝에 도달했다는 걸 알 수 있다.

알아야 할 기하 프로퍼티

Untitled

  1. 스크롤해서 가려진 콘텐츠 영역의 높이 : document.documentElement.scrollTop

  2. 현재 화면(뷰포트)의 높이

    window.innerHeight — 스크롤바 포함

    document.documentElement.clientHeight — 스크롤바 제외

  3. 전체 문서의 높이

    const scrollHeight = Math.max(
      document.body.scrollHeight, document.documentElement.scrollHeight,
      document.body.offsetHeight, document.documentElement.offsetHeight,
      document.body.clientHeight, document.documentElement.clientHeight
    );
    

    왜 이런 방식으로 문서 전체 높이를 구해야 하는 걸까요? 이유는 알아보지 않는 게 낫습니다. 이런 이상한 계산법은 아주 오래 전부터 있었고 그다지 논리적이지 않은 이유로 만들어졌기 때문입니다. — JavaScript Info

Custom Hook 작성

컴포넌트에 무현 스크롤 구현 코드를 직접 작성하기보단 재사용할 수 있도록 Custom Hook으로 만드는 게 좋다. 무한 스크롤 Custom Hook은 스크롤 이벤트를 감지해서 데이터를 더 받아올지 여부를 알려주는 역할을 한다.

➊스크롤 이벤트가 발생하면 현재 스크롤 위치가 전체 문서의 끝에 도달했는지 확인한다. ➋스크롤이 문서 끝에 위치 했다면 isFetching 상태를 true로 변경하고, ➌파라미터로 받은 callback을 호출한다. callback 함수는 다음 데이터를 받아오는 작업을 수행한다.

import { useEffect, useState } from 'react';
import { getScrollHeight } from '../../lib/utils';

const useInfiniteScroll = callback => {
  const [isFetching, setIsFetching] = useState(false);

  const handleScroll = () => { // ⑵ 스크롤 이벤트가 발생하면 전체 문서의 끝에 위치했는지 확인
    const scrollHeight = getScrollHeight(); // 전체 문서의 높이를 구하는 유틸 함수
    const currentScroll = window.innerHeight + document.documentElement.scrollTop;
    if (currentScroll === scrollHeight && !isFetching) setIsFetching(true);
  };

  useEffect(() => { // ⑴ 스크롤 이벤트 감지
    window.addEventListener('scroll', handleScroll);
    return () => window.removeEventListener('scroll', handleScroll);
  }, []);

  useEffect(() => {
    if (isFetching) callback(); // ⑶ 다음 데이터를 불러오는 callback 실행
  }, [isFetching]);

  return [isFetching, setIsFetching];
};

export default useInfiniteScroll;

Custom Hook 적용

리스트를 출력하는 InfiniteScrollNonIO 컴포넌트에선 다음 데이터를 받아오는 작업을 수행하는 함수를 Custom Hook의 인자(callback)로 넘긴다. 그럼 스크롤이 문서 끝에 도달했을 때 인자로 넘긴 함수가 실행되고 다음 데이터를 받아온 후 화면에 렌더링한다.

다음 데이터를 받아오는 작업을 수행하기 전 커스텀 훅 안에선 먼저 isFetching 상태를 true로 변경한 후 반환한다. 이를 받은 InfiniteScrollNonIO 컴포넌트는 “로딩 중...” 문구를 표시한다. 데이터를 모두 받아오면 isFetching 상태가 변경돼서 “로딩 중...” 문구가 사라지고, 새로 받아온 데이터를 화면에 출력한다.

import React, { useState } from 'react';
import useInfiniteScroll from './useInfiniteScroll';

export default function InfiniteScrollNonIO() {
  const [listItems, setListItems] = useState(Array.from(Array(30).keys(), n => n + 1));
  const [isFetching, setIsFetching] = useInfiniteScroll(fetchMoreListItems);

  function fetchMoreListItems() {
    setTimeout(() => {
      setListItems(prevState => [
        ...prevState,
        ...Array.from(Array(20).keys(), n => n + prevState.length + 1),
      ]);
      setIsFetching(false);
    }, 2000); // 2초간 딜레이
  }

  return (
    <section className="flex flex-col justify-center items-center p-4">
      <ul className="space-y-4 mb-4">
        {listItems.map((item, i) => (
          <li key={i} className="border text-center w-56 h-12 grid place-content-center">
            List Item {item}
          </li>
        ))}
      </ul>
			<div className={`${isFetching ? 'visibility' : 'invisible'}`}>
        🔍️ Fetching more items...
      </div>
    </section>
  );
}

🔍️ Array.keys() 메서드는 배열의 각 인덱스를 키 값으로 가지는 새로운 Array Iterator 객체(next 메서드가 구현되어 있는 이터레이터 객체)를 반환한다. 이터러블이므로 for of 반복문으로 순회할 수 있다.

for (const key of Array(3).keys()) console.log(key) // 0, 1, 2
Array.from(Array(3).keys(), n => n * 2) // [0, 2, 4]

스로틀 적용