[React] 무한 스크롤 구현
React로 개발하는 웹 페이지에서 매 렌더링마다 데이터를 가져오게 된다면 불필요하게 많은 데이터 읽기가 발생할 수 있다. 이를 방지하기 위해 Pinterest 무한스크롤처럼 스크롤이 하단에 닿을 때마다 일정 개수만큼 데이터를 가져오도록 구현한 과정이다.
Scroll Sensor 구현
<div className={styles.pageEnd} ref={pageEnd}/>
보이기 쉽게 빨간색으로 표현한 부분이 sensor div이다.
화면에서 sensor div가 보이는지는 IntersectionObserver api을 사용하여 관찰한다.
IntersectionObserver 객체와 콜백함수 생성
const pageEnd = useRef(null);
const observer = new IntersectionObserver(onIntersect, { threshold: 0 });
useEffect(() => {
if (pageEnd.current) observer.observe(pageEnd.current);
}, []);
페이지가 완전히 렌더링된 후 useEffect 안에서 observer 객체가 pageEnd.current를 대상으로 관찰할 수 있도록 렌더링한다. threshold는 대상 div의 어느 정도만큼 보일 때 함수를 호출할지를 결정하는 인자이다. (0.5면 절반이 보일 때 함수 호출)
const onIntersect = async ([entry], observer) => {
if (entry.isIntersecting) {
observer.unobserve(entry.target);
await getMorePhotos();
setTimeout(() => {
observer.observe(entry.target);
}, 800);
}
};
observer 객체에게 전달하는 콜백함수는 화면과 sensor div가 intersect할 때 호출되는 함수이다.
우리는 다음 데이터를 더 로드하도록 하는 getMorePhotos()를 호출한다.
이 때, 순간적으로 콜백함수가 여러 번 호출되는 것을 막기 위해 onIntersect 안에서 잠시 observe를 해지하고, 일정시간 setTimeout 후 다시 observe를 걸어주었다.
getMorePhotos 구현
let timeStamp = useRef(null);
const getMorePhotos = async () => {
console.log("photo request");
let queryTemp;
if (!timeStamp) {
//first query
queryTemp = query(
collection(db, "Photos"),
orderBy("timestamp", "desc"),
limit(10)
);
} else {
queryTemp = query(
collection(db, "Photos"),
orderBy("timestamp", "desc"),
startAfter(timeStamp),
limit(10)
);
}
setIsLoading(true);
let dataSnapShot;
try {
dataSnapShot = await getDocs(queryTemp);
} catch (error) {
console.log(error);
}
const dataList = dataSnapShot.docs.map((doc) => doc.data());
const length = dataList.length;
if (length) {
timeStamp = dataSnapShot.docs[length - 1];
setPhotos((prev) => [...prev, ...dataList]);
} else {
setEndOfData(true);
}
setIsLoading(false);
};
가져오는 데이터에는 timestamp field가 존재한다. 데이터를 시간순으로 display하기 위해 timestamp를 기준으로 쿼리를 작성한다.
if (!timeStamp) {
//first query
queryTemp = query(
collection(db, "Photos"),
orderBy("timestamp", "desc"),
limit(10)
);
가장 처음 데이터를 읽을 때는 시간순으로 정렬된 데이터를 처음부터 (최대)10개를 읽어온다.
timestamp는 useState로 상태관리를 하려했지만, 미상의 이유로 잘 작동하지 않아 useRef로 저장하며 사용하였다.
else {
queryTemp = query(
collection(db, "Photos"),
orderBy("timestamp", "desc"),
startAfter(timeStamp), //timestamp 뒤에 있는 데이터들
limit(10)
);
}
이후 데이터를 읽을 때는 timeStamp를 기준으로 (최대)10개의 데이터를 읽어온다.
const length = dataList.length;
if (length) {
timeStamp = dataSnapShot.docs[length - 1];
setPhotos((prev) => [...prev, ...dataList]);
else {
setEndOfData(true);
}
그 이후 가져온 데이터의 길이를 받아 timeStamp를 업데이트한다.
만약 받아온 데이터가 10개보다 적다면 endOfData flag를 업데이트한다.
모든 데이터 로드가 끝났을 때
{!endOfData && <div className={styles.pageEnd} ref={pageEnd} />}
{endOfData && (<div
className={styles.footer}>
모든 데이터가 로드됐을 때는 sensor div의 관찰이 중지되어야 한다.
useEffect를 사용하여 unobserve 처리를 해주어도 좋고, 나는 단순히 요소를 제거해버렸다.
그리고 최종적으로 스크롤이 하단에 닿았으므로 그 자리에 footer를 생성한다.
edited by 정정환
Subscribe to my newsletter
Read articles from 정정환 directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by