[React] Intersection Observer API : 무한 스크롤 구현
🤔Intersection Observer API란?
Intersection Observer API는 타겟 요소와 상위 요소 또는 최상위 document 의 viewport사이의 교차점 (intersection) 내의 변화를 비동기적으로 관찰하는 방법이다.
즉, 내가 지정한 요소가 화면(뷰포트) 상에 보이고 있는지를 관찰하는 API이다.
왜 사용할까?
기존의 scroll 같은 이벤트 기반의 요소 관찰에서 발생하는 렌더링 성능이나 이벤트 연속 호출 같은 문제 없이 사용 가능
- scroll 이벤트는 동기적으로 실행 -> 메인 스레드에 큰 부하를 줄 수 있다.
- getBoundingClientRect 사용시 reflow 발생
reflow : 엘리먼트가 문서내에서 어디에 위치하는지에 대한 정보를 계산해서 다시 렌더링을 하는 현상
Intersection Observer API는 비동기적으로 실행하여 메인 스레드에 영향을 주지 않는다.
IntersectionObserverEntry의 속성을 활용하면, reflow 없이 element의 위치를 감지할 수 있다.
어떤 상황에 사용할까?
- Lazy-loading
- infinite scrolling
- 광고 수익 계산을 위한 광고의 가시성 확인
- 사용자가 결과를 봤는지에 따라 애니메이션 혹은 어떠한 동작을 수행할지 안할지 결정
어떻게 사용할까?
intersection observer 생성
const io = new IntersectionObserver(callback, options);
io.observe(element);
new IntersectionObserver()를 통해 생성한 인스턴스(io)로 관찰자(Observer)를 초기화하고 관찰 대상(element) 지정
Methods
io.observe(element) // => element 관찰 시작
io.unobserve(element) // => element 관찰 중단
io.disconnect() // => 모든 element 관찰 중단
callback
관찰 대상이 등록되거나 가시성(Visibility, 보이는지 보이지 않는지) 변화가 생기면 콜백 실행
콜백은 2개의 인수(entries, observer)를 가진다.
entries
const io = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
entry.boundingClientRect // => element.getBoundingClientReac()
entry.intersectionRect // => element의 감지된 부분의 사각형 정보
entry.intersectionRatio // => element가 루트 요소와 얼마나 겹치는 지 0~1 사이
entry.isIntersecting // => element의 교차 상태
entry.rootBounds // => 루트 요소의 사각형 정보
entry.target // => element
entry.time // => element와 루트 요소의 교차 발생시간
})
}, options)
io.observe(element)
observer
콜백이 실행되는 해당 인스턴스를 참조
options
root => 기본값 null, 관찰 대상을 검사하기 위해 뷰포트 대신 사용할 요소 지정
rootMargin => 기본값 0px 0px 0px 0px, root의 margin 값 설정
threshold => 기본값 0, 콜백이 실행되기 위한 백분율 지정 0 ~ 1, 혹은 배열
✨리액트로 무한 스크롤 구현
const [list, setList] = useState([]);
const [target, setTarget] = useState(null);
const [pageNumber, setPageNumber] = useState(0);
// 초기값 렌더링, pageNumber 바뀔 때마다 호출
useEffect(() => {
// ~데이터 가져오는 로직~
// setList((list) => list.concat(data))
}, [pageNumber]);
const callback = useCallback(
(entry, observer) => {
if (entry[0].isIntersecting) {
setPageNumber((prev) => prev = prev + 1)
observer.unobserve(entry[0].target);
}
},
[setPageNumber]
);
useEffect(() => {
if (!target) return;
const io = new IntersectionObserver(callback);
io.observe(target);
return () => observer.disconnect();
}, [callback, target]);
return (
<>
<div>
{list.map((item) => (
// list item 요소 출력
))}
</div>
<div ref={setTarget}></div>
</>
);
clean-up
- 스크롤을 내려 새로운 목록을 받아오게 되므로 관찰할 대상이 바뀌기 때문에 disconnect()로 관찰요소를 없애고 새로 지정
useCallback
- 재렌더링 시 함수가 새로 만들어지지 않는다. 함수 안에서 사용하는 state 또는 props가 있다면 deps 배열에 포함
custom hook으로 만들기
// useIntersectionObserver.jsx
import { useEffect, useState } from "react";
const useIntersectionObserver = ({ callback }) => {
const [target, setTarget] = useState(null);
useEffect(() => {
if (!target) return;
const observer = new IntersectionObserver(callback);
observer.observe(target);
return () => observer.unobserve(target);
}, [onIntersect, target]);
return { setTarget };
};
export default useIntersectionObserver;
// 사용할 곳
const callback = useCallback((entry, observer) => {생략}, [])
const { setTarget } = useIntersectionObserver({ callback });
return (
<>
// 생략
<div ref={setTarget}></div>
</>
);
✍️개인적인 정리
+ 렌더링이 두 번 발생
구현하다가 처음에 데이터 호출이 두 번씩 돼서 왜 그런가 했더니 React.StrictMode가 적용되어서 그랬다.
creat-react-app으로 생성하면 잠재적인 문제를 알아내기 위한 도구로 StrictMode가 기본으로 적용된다.
개발 모드에서만 활성화되기 때문에, 프로덕션 빌드에는 영향을 끼치지 않는다고 한다. 지워주면 한 번만 호출이 된다.
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById("root")
);