무한스크롤이란 화면을 아래로 스크롤 하였을 때, 콘텐츠가 끊이지 않고 계속해서 로드되는 것을 의미합니다. 다음 버튼을 눌러 페이지가 로딩되는 것(페이지네이션)을 기다리지 않아도 되어 사용자 경험을 높일 수 있습니다.
인스타그램이나 유튜브에서도 무한스크롤을 사용하고 있는데요. 끊임없이 새로운 콘텐츠가 아래에서 로드되는 것을 알 수 있습니다.
무한스크롤을 구현하는 방식에는 여러 가지가 있지만, 그중에서도 저는 intersection observer API
를 이용하여 구현하였습니다.
뷰포트와 대상 요소 사이의 교차 상태를 감지하는 기능을 제공하여 무한 스크롤과 같은 기능을 구현하는 데 유용합니다.
IntersectionObserver(callback, options)
observe()
disconnect()
root
threshold
rootMargin
일단 저는 무한스크롤은 여러 페이지에서 재사용될 수 있기 때문에 커스텀 훅으로 작성하였습니다.
// useIntersectionObserver 커스텀 훅
const defaultOption = {
root: null,
threshold: 0.5,
rootMargin: "0px",
};
export default function useIntersectionObserver(onIntersect, options, isLoading) {
const targetRef = useRef(null);
const checkIntersect = useCallback(([entry]) => {
if (entry.isIntersecting) {
onIntersect();
}
}, []);
useEffect(() => {
let io;
if (targetRef.current && isLoading) {
io = new IntersectionObserver(checkIntersect, {
...defaultOption,
...options,
}); // 관찰자 생성
io.observe(targetRef.current); // 관찰할 대상 등록
}
return () => {
io && io.disconnect();
};
}, [targetRef, isLoading]);
return targetRef;
}
// Home.jsx
export default function Home() {
const [productListData, setProductListData] = useState([]);
const [page, setPage] = useState(0);
const [isLoading, setIsLoading] = useState(true);
const getProductList = async (page) => {
try {
const res = await getProductList(page); // 상품리스트 불러오는 API
setProductListData((prev) => [...prev, ...res.data.results]);
} catch (err) {
if (err.response.data.detail === "페이지가 유효하지 않습니다.") // 더이상 불러올 데이터가 없는 경우
setIsLoading(false);
}
};
const targetRef = useIntersectionObserver(
() => {
setPage((prev) => prev + 1);
},
{ threshold: 0.5 },
isLoading
);
useEffect(() => {
if (page === 0) return;
getProductList(page);
}, [page]);
return (
<MainLayout>
<ProductList productListData={productListData} />
<div ref={targetRef} />
</MainLayout>
);
}
<div ref={targetRef}
가 화면에 감지되면 observer가 setPage((prev) => prev + 1)
을 실행하여 다음 페이지의 데이터를 가져옵니다.isLoading
을 통해 모든 데이터가 다 불러와지면 관찰자는 참조하는 대상과의 연결을 끊습니다.// ProductList.jsx
export default function ProductList({ productListData }) {
return (
<ProductUl>
{productListData.map((product) => (
<li key={product.product_id}>
<ProductImg src={product.image} alt="상품이미지" />
<ProductName>{product.product_name}</ProductName>
<ProductPrice>{product.price}</ProductPrice>
</li>
))}
</ProductUl>
);
}