[React] Infinite Scroll 구현하기 : Intersection Observer API
인피니티 스크롤이란 컴포넌트를 한 번에 불러오는 것이 아니라 스크롤 하면서새 컴포넌트가 렌더되는 것을 말한다.
탐색 위주의 모바일 친화적이며, 컨텐츠를 소비하는 속성을 가진 웹사이트라면 인피니트 스크롤을 적용하는 것이 쾌적한 사용자 경험을 제공하는 데 도움이 된다.
Intersection Observer API
The Intersection Observer API provides a way to asynchronously observe changes in the intersection of a target element with an ancestor element or with a top-level document's viewport.
Intersection Observer API는 브라우저의 뷰포트와 대상 요소의 교차점을 관찰하며 변경 사항을 비동기적으로 관찰하는 방식을 제공한다. 쉽게 말하면 우리는 관찰 대상 요소를 정하고 뷰포트에 그 요소가 보여졌을 때 Intersection Observer를 통해 관찰할 수 있는 것이다. 이를 통해 지정한 요소가 화면에 나타나면 컴포넌트를 렌더링하도록 만들 수 있다. Intersection Observer는 다음과 같은 상황에서 유용하게 쓰일 수 있다.
- 페이지를 스크롤할 때 이미지 또는 기타 콘텐츠를 lazy-loading위해
- 스크롤하면서 점점 더 많은 콘텐츠가 로드 및 렌더링되는 무한 스크롤 웹 사이트 구현하기 위해
- 광고 가시성을 확인하여 수익 계산을 위해
- 사용자가 결과를 볼 수 있는지 여부에 따라 작업 또는 애니메이션 프로세스를 수행할지 여부를 결정하기 위해
사용법
Intersection Observer 옵션
let options = {
root: document.querySelector('#scrollArea'),
rootMargin: '0px',
threshold: 1.0
}
let observer = new IntersectionObserver(callback, options);
- root
대상의 가시성을 확인하기 위한 뷰포트로 사용되는 요소입니다. 관찰 대상의 상위 항목이어야 합니다. 지정되지 않았거나 브라우저 뷰포트인 경우 기본값 null입니다.
- rootMargin
루트 주변의 margin 속성.
- threshold
관찰자의 콜백이 실행되어야 하는 대상 가시성의 백분율을 나타내는 단일 숫자 또는 숫자 배열입니다. 가시성이 50% 표시를 통과할 때만 감지하려면 0.5 값을 사용할 수 있습니다.
Intersection Observer 타겟팅
let target = document.querySelector('#listItem');
observer.observe(target);
관찰 대상 요소 지정하기
let callback = (entries, observer) => {
entries.forEach(entry => {
// Each entry describes an intersection change for one observed
// target element:
// entry.boundingClientRect
// entry.intersectionRatio
// entry.intersectionRect
// entry.isIntersecting
// entry.rootBounds
// entry.target
// entry.time
});
};
대상이 지정한 임계값을 충족할 때마다 IntersectionObserver 콜백이 호출됩니다. 콜백은 IntersectionObserverEntry객체 목록과 관찰자를 수신합니다.
코드 활용
내가 프로젝트에 활용한 무한 스크롤 코드는 다음과 같다.
const SHOW_QUIZS_QUERY = gql`
...
`;
export default function QuizList() {
const isQuizLoadEndVar = makeVar(false);
const { loading, data, refetch, fetchMore } = useQuery<showQuizs>(
SHOW_QUIZS_QUERY,
{
//한 번에 15개씩 불러오기
variables: { take: 15 },
onCompleted: () => {
isQuizLoadEndVar(false);
},
}
);
//대상요소 선택하기
const loaderRef = useRef<any>();
const handleObserver = useCallback(
async (entries) => {
const isQuizLoadEnd = isQuizLoadEndVar();
if (isQuizLoadEnd) {
return;
}
const target = entries[0];
//화면에 대상 요소가 겹친다면
if (data?.showQuizs && target.isIntersecting) {
const lastId = data.showQuizs[data.showQuizs.length - 1].id;
//더 불러오기
const more = await fetchMore({
variables: {
lastId,
},
});
//해당 요소가 마지막이면
if (more?.data?.showQuizs?.length === 0) {
//더 이상 불러오지 않게 한다
isQuizLoadEndVar(true);
}
}
},
[data]
);
useEffect(() => {
//관찰자의 콜백이 호출되는 상황을 제어하는 옵션
const option = {
root: null,
rootMargin: "0px",
threshold: 0,
};
//관찰자를 생성하고 handleObserver라는 콜백함수와 option이라는 인수를 받는다
const observer = new IntersectionObserver(handleObserver, option);
//대상 요소가 보이면 대상요소를 관찰한다
if (loaderRef.current) {
observer.observe(loaderRef.current);
}
}, [handleObserver]);
return (
<div className="grid gap-4 pb-4 sm:grid-cols-2 md:grid-cols-3 ">
{data?.showQuizs?.map((post, index) => {
return <Quiz key={index} post={post} />
})}
//Intersection Observer가 관찰할 대상 요소를 마지막에 빈 <div>로 넣기
<div ref={loaderRef}></div>
</div>
);
}
느낀 점
이렇게나 간단하게 설명했지만 사실 Intersection Observer API는 MDN에 어마어마하게 방대하게 소개되어있다. (긴글주의)
하나씩 읽어보고 정리하면서 다시 한 번 공식 문서의 중요성을 깨달았다.
참고문서
https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API