무한 스크롤은 현재 페이지에서 스크롤바가 마지막 콘텐츠 지점에 있을 때 다음 콘텐츠를 자동으로 불러오는 구현 방식을 말한다. ➊스크롤해서 가려진 영역의 높이와 ➋현재 화면(뷰포트)의 높이를 더한 값이 ➌전체 문서의 높이와 같다면 현재 스크롤이 가장 하단 끝에 도달했다는 걸 알 수 있다.
스크롤해서 가려진 콘텐츠 영역의 높이 : document.documentElement.scrollTop
현재 화면(뷰포트)의 높이
window.innerHeight
— 스크롤바 포함
document.documentElement.clientHeight
— 스크롤바 제외
전체 문서의 높이
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은 스크롤 이벤트를 감지해서 데이터를 더 받아올지 여부를 알려주는 역할을 한다.
➊스크롤 이벤트가 발생하면 현재 스크롤 위치가 전체 문서의 끝에 도달했는지 확인한다. ➋스크롤이 문서 끝에 위치 했다면 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;
리스트를 출력하는 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]