무한 스크롤(Infinite Scroll)은 사용자가 특정 버튼을 클릭하지 않아도 스크롤을 내릴 때 새로운 데이터를 불러오는 기법이다.
10,000개 이상의 대량 데이터를 다룰 경우, 성능 최적화와 렌더링 최적화가 매우 중요하다.
이 글에서는 대량 데이터 무한 스크롤을 구현할 때 고려해야 할 핵심 요소를 정리한다.
📌 한 번에 모든 데이터를 렌더링하면 성능 저하 발생
✅ 해결 방법: Pagination (페이지네이션) 사용
limit과 offset을 설정하여 필요한 데이터만 가져옴💡 React Query의 useInfiniteQuery를 사용하면 효율적인 데이터 페이징이 가능
import { useInfiniteQuery } from "@tanstack/react-query";
import axios from "axios";
const fetchPosts = async ({ pageParam = 1 }) => {
const res = await axios.get(`/api/posts?limit=20&page=${pageParam}`);
return res.data;
};
const InfiniteScrollComponent = () => {
const { data, fetchNextPage, hasNextPage } = useInfiniteQuery({
queryKey: ["posts"],
queryFn: fetchPosts,
getNextPageParam: (lastPage, pages) => lastPage.nextPage,
});
return (
<div>
{data?.pages.map((group, i) =>
group.posts.map((post) => <div key={post.id}>{post.title}</div>)
)}
{hasNextPage && <button onClick={() => fetchNextPage()}>더 보기</button>}
</div>
);
};
✔️ useInfiniteQuery는 데이터를 페이지 단위로 불러오면서 필요한 경우에만 요청을 수행
✔️ getNextPageParam을 사용하여 다음 페이지의 존재 여부를 판단
📌 모든 데이터를 DOM에 렌더링하면 성능 문제가 발생
✅ 해결 방법: React Virtualization 사용
react-window, react-virtualized💡 react-window를 사용한 리스트 가상화 예제
import { FixedSizeList as List } from "react-window";
const Row = ({ index, style }) => (
<div style={style}>Row {index}</div>
);
const VirtualizedList = () => (
<List height={500} itemCount={10000} itemSize={35} width={"100%"}>
{Row}
</List>
);
export default VirtualizedList;
✔️ 항목이 10,000개여도 실제로 렌더링되는 항목은 화면에 보이는 개수만큼 제한
✔️ itemSize로 각 항목의 높이를 지정하여 최적화
📌 스크롤 이벤트를 직접 감지하면 성능 문제가 발생
onscroll 이벤트를 직접 사용하면 스크롤할 때마다 계속해서 이벤트가 실행됨✅ 해결 방법: Intersection Observer API 활용
💡 Intersection Observer를 활용한 무한 스크롤 예제
import { useEffect, useRef } from "react";
import { useInfiniteQuery } from "@tanstack/react-query";
import axios from "axios";
const fetchPosts = async ({ pageParam = 1 }) => {
const res = await axios.get(`/api/posts?limit=20&page=${pageParam}`);
return res.data;
};
const InfiniteScrollComponent = () => {
const { data, fetchNextPage, hasNextPage } = useInfiniteQuery({
queryKey: ["posts"],
queryFn: fetchPosts,
getNextPageParam: (lastPage) => lastPage.nextPage,
});
const observerRef = useRef(null);
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting && hasNextPage) {
fetchNextPage();
}
},
{ threshold: 1.0 }
);
if (observerRef.current) observer.observe(observerRef.current);
return () => {
if (observerRef.current) observer.unobserve(observerRef.current);
};
}, [fetchNextPage, hasNextPage]);
return (
<div>
{data?.pages.map((group) =>
group.posts.map((post) => <div key={post.id}>{post.title}</div>)
)}
<div ref={observerRef} />
</div>
);
};
✔️ IntersectionObserver를 사용하여 특정 요소(observerRef)가 화면에 나타날 때 fetchNextPage() 실행
✔️ 불필요한 스크롤 이벤트 핸들링을 방지하여 성능 최적화
📌 데이터 요청을 최적화하여 네트워크 트래픽 감소
✅ 해결 방법: React Query의 데이터 캐싱 활용
staleTime을 설정하여 일정 시간 동안 새 요청을 방지isFetching 상태를 활용해 로딩 상태를 표시💡 React Query의 데이터 캐싱 설정
const { data, isFetching } = useInfiniteQuery({
queryKey: ["posts"],
queryFn: fetchPosts,
staleTime: 60000, // 60초 동안 캐싱 유지
});
✔️ staleTime: 60000 설정 시, 60초 동안 동일한 데이터를 다시 요청하지 않음
✔️ isFetching을 활용하여 로딩 UI 추가 가능
✅ 1) 스켈레톤 UI 적용
isFetching을 활용하여 구현 가능✅ 2) API 요청 한계 처리
debounce 또는 throttle을 활용하여 요청 빈도 조절 가능✅ 3) 사용자의 네트워크 상태 고려
대량 데이터를 활용한 무한 스크롤을 구현할 때는 렌더링 성능과 데이터 요청 최적화가 핵심이다.
✔️ Pagination을 활용하여 적절한 양의 데이터만 로드
✔️ Virtualization을 적용하여 화면에 보이는 요소만 렌더링
✔️ Intersection Observer를 활용하여 불필요한 스크롤 이벤트 방지
✔️ React Query를 사용하여 데이터 캐싱 및 네트워크 요청 최적화