무한스크롤을 구현하는 방법으로는 scroll이벤트인 onScroll을 사용하는 방법이 있고, Intersection Observer API를 활용하는 방법이 있다.
하지만 onScroll을 사용하면 scroll이 일어날때마다 이벤트가 실행돼서 성능 저하의 문제가 생기기 때문에 Intersection Observer API를 활용한 무한스크롤 기능을 구현했다.
Intersection Observer API는 Javasctipt 내장 API로, 기본적으로 브라우저 뷰포트와 설정한 요소의 교차점을 관찰해서 target 요소가 root 요소와 교차가 일어나는지(뷰포트에 포함되는지 안 되는지)를 판단하여 콜백 함수를 실행할 수 있다. 따라서, target 요소를 적절한 위치에 배치를 하면 사용자가 스크롤을 이동하는 것에 따라 데이터 요청을 할 수 있다.
사용법은 객체 상태변화를 관찰하는 관찰자들(옵저버)을 등록하여 상태변화가 있을 때마다 각 옵저버에 알리는 방식이다.
<Container>
{
list.map((p, i) => {
return <div key={i}>
<img src={p.image} alt="상품 이미지"/>
<p className='product-name'>{p.seller_store}</p>
<p className='product'>{p.product_name}</p>
<span className='product-price'>{p.price}</span>
<span>원</span>
</div>
})
}
{moreData ? <div ref={target}></div> : null}
</Container>
useEffect(() => {
let observer;
if (target.current) {
const handleInterSect = async ([entry], observer) => {
if (entry.isIntersecting) {
observer.unobserve(entry.target); // 타겟요소에 대한 옵저버를 멈춘다. Lazy Loading이 시작되면 관찰을 멈추는 등의 용도가 있다.
await getData();
observer.observe(entry.target)
}
};
observer = new IntersectionObserver(handleInterSect, { threshold: 0.6, });
observer.observe(target.current) // 타겟요소에 대한 Intersection Observer 를 등록한다.
}
return () => observer && observer.disconnect(); // 다중 타겟요소들의 옵저빙을 동시에 멈추기 위해 사용되는 메서드다.
}, [target, page])
Intersection Observer는 아래와 같은 문법으로 인스턴스를 생성한 뒤 사용한다.
observer = new IntersectionObserver(callback, options);
callback : 교차시에 실행되는 함수이다. 로딩구현이나 패치 등의 함수가 통상 할당된다.
options : Intersection Observer에 관한 설정을 할 수 있는 부분이다.
root : 교차를 감지하는 root 요소. observe로 등록할 요소의 상위요소여야 한다. 기본값은 null(이 땐 브라우저 viewport)
rootMargin : root 요소의 마진값. 기본값은 0px.
threshold : 0.0 ~ 1.0 사이의 숫자들을 배열로 받는다. 이는 %로 치환되어, 해당 비율만큼 교차된 경우 콜백이 실행된다.
boundingClientRect : 관찰 대상의 사각형 정보(DOMRectReadOnly)
intersectionRect : 관찰 대상의 교차한 영역 정보(DOMRectReadOnly)
intersectionRatio : 관찰 대상의 교차한 영역 백분율(intersectionRect 영역에서 boundingClientRect 영역까지 비율, Number)
isIntersecting : 관찰 대상의 교차 상태(Boolean)
rootBounds : 지정한 루트 요소의 사각형 정보(DOMRectReadOnly)
target : 관찰 대상 요소(Element)
time : 변경이 발생한 시간 정보(DOMHighResTimeStamp)
const getData = async () => {
await api.get(`/products/?page=${page}`).then((res) => {
setList((prev) => prev.concat(res.data.results))//리스트 추가
setPage(prev => prev + 1)
}).catch((error) => {
setMoreData(false)
return;
})
}
{moreData ? <div ref={target}></div> : null}
function MainGrid() {
const [page, setPage] = useState(1)
const [list, setList] = useState([])
const [moreData, setMoreData] = useState(true)
const target = useRef(null);
const getData = async () => {
await api.get(`/products/?page=${page}`).then((res) => {
setList((prev) => prev.concat(res.data.results))
setPage(prev => prev + 1)
}).catch((error) => {
setMoreData(false)
return;
})
}
useEffect(() => {
let observer;
if (target.current) {
const handleInterSect = async ([entry], observer) => {
if (entry.isIntersecting) {
observer.unobserve(entry.target);
await getData();
observer.observe(entry.target)
}
};
observer = new IntersectionObserver(handleInterSect, { threshold: 0.6, });
observer.observe(target.current)
}
return () => observer && observer.disconnect();
}, [target, page])
return (
<Container>
{
list.map((p, i) => {
return <div key={i}>
<img src={p.image} alt="상품 이미지" />
<p className='product-name'>{p.seller_store}</p>
<p className='product'>{p.product_name}</p>
<span className='product-price'>{p.price}</span>
<span>원</span>
</div>
})
}
{moreData ? <div ref={target}></div> : null}
</Container>
)
}
참고 👇