프로젝트를 진행하면서 페이징 기능이 필요하여 무한 스크롤로 기능을 구현해보았습니다.
무한 스크롤은 많은 양의 콘텐츠를 스크롤 해서 볼 수 있게 만든 기술입니다.
한 페이지를 아래로 스크롤 하면 새로운 화면을 보여줍니다.
JS에서 무한스크롤 구현방법이 여러가지 있지만, 저는 Intersection Observer API를 활용해서 구현했습니다.
Intersection Observer API를 사용한 이유입니다.
Intersection Observer API는 타겟 요소와 상위 요소 또는 최상위 document의 viewport 사이의
intersection 내의 변화를 비동기적으로 관찰하는 방법입니다.
Intersection Observer를 생성할 때는 옵션을 설정할 수 있습니다.
옵션에는 root, rootMargin, threshold가 있습니다.
mdn 자료 설명
- root : 대상 객체의 가시성을 확인할 때 사용되는 뷰포트 요소입니다. 이는 대상 객체의 조상 요소여야 합니다. 기본값은 브라우저 뷰포트이며, root 값이 null 이거나 지정되지 않을 때 기본값으로 설정됩니다.
- rootMargin : root 가 가진 여백입니다. 이 속성의 값은 CSS의 margin 속성과 유사합니다. e.g. "10px 20px 30px 40px" (top, right, bottom, left). 이 값은 퍼센티지가 될 수 있습니다. 이것은 root 요소의 각 측면의 bounding box를 수축시키거나 증가시키며, 교차성을 계산하기 전에 적용됩니다. 기본값은 0입니다.
- threshold : observer의 콜백이 실행될 대상 요소의 가시성 퍼센티지를 나타내는 단일 숫자 혹은 숫자 배열입니다. 만일 50%만큼 요소가 보여졌을 때를 탐지하고 싶다면, 값을 0.5로 설정하면 됩니다. 혹은 25% 단위로 요소의 가시성이 변경될 때마다 콜백이 실행되게 하고 싶다면 [0, 0.25, 0.5, 0.75, 1] 과 같은 배열을 설정하세요.
기본값은 0이며(이는 요소가 1픽셀이라도 보이자 마자 콜백이 실행됨을 의미합니다). 1.0은 요소의 모든 픽셀이 화면에 노출되기 전에는 콜백을 실행시키지 않음을 의미합니다.
import React, { useState, useEffect, useRef, useCallback } from "react";
// ...
// data 배열을 받아 옵니다.
function GridView({ data }) {
const [list, setList] = useState(data.slice(0, 20));
const [isScroll, setIsScroll] = useState(false);
const loadRef = useRef(null);
const observerRef = useRef(null);
const onIntersect = useCallback(
async (entry, observer) => {
if (entry[0].isIntersecting) {
observer.unobserve(entry[0].target);
await new Promise((resolve) => setTimeout(resolve, 1000));
setList((list) =>
list.concat(data.slice(list.length, list.length + 10))
);
observer.observe(entry[0].target);
}
},
[data]
);
useEffect(() => {
if (loadRef.current && list.length !== data.length) {
setIsScroll(true);
observerRef.current = new IntersectionObserver(onIntersect, {
threshold: 0.5,
});
if (isScroll) {
observerRef.current.observe(loadRef.current);
}
}
return () => {
setIsScroll(false);
observerRef.current && observerRef.current.disconnect();
};
}, [list, data , onIntersect]);
return (
<>
// ...
<Load ref={loadRef}>
{isScroll && <InfiniteLoading type="spin" color="#2f3640" />}
</Load>
</>
);
}
export default GridView;
const Load = styled.div`
${({ theme }) => theme.common.flexRow};
width: 100%;
background-color: ${({ theme }) => theme.colors.white};
`;
const InfiniteLoading = styled(ReactLoading)`
width: 3rem;
height: 3rem;
z-index: 999;
`;
useEffect 부분
- useRef를 활용해 observerRef와 loadRef를 만들어 줍니다.
- observerRef: intersection Observer 담아줌
- loadRef: 관찰할 대상(target)
- loadRef가 보이면 isScrollintersection Observer를 생성하여 observerRef에 담아 주어 observer가 관찰할 대상(loadRef)을 observer.observe함수로 지정합니다.
조건문에list.length !== data.length
는 데이터를 모두 불러와 스크롤 마지막이면 더이상 작동하지 말라고 작성해 놓은 코드입니다.- 스크롤을 내려 새로운 목록을 받아오게 되므로 관찰할 대상이 바뀌기 때문에 observer.disconnect 함수로 관찰요소를 없애고 새로 지정하게 됩니다.
- isScroll useState를 만든 이유는 해당 프로젝트에서 router 이동시 메모리 lack 에러가 발생하여 useEffect의 cleanup을 해주기 위함입니다.
onIntersect 부분
무한 스크롤을 비동기 처럼 보이게 해주기 위해 SetTimeout함수를 이용하여 1초 후 데이터를 불러옵니다.
list.length를 활용해 불러온 데이터의 이후 데이터 10개씩을 concat 메소드를 활용해 list state에 할당해주었습니다.Load
스크롤할 때 isScroll state를 활용해 스크롤 중일 땐 로딩 중이라는 표시를 주어 사용자가 데이터를 불러올 동안 기다릴 수 있게 해주었습니다.