🖥️ Frontend/React
[React] 무한 스크롤(Infinite Scroll) 적용 – 시행착오와 최적화
hjwjo
2025. 9. 16. 10:27
프로젝트에서 게시판 목록을 불러오는 기능을 구현하면서 무한 스크롤(Infinite Scroll)을 적용했습니다.
처음에는 단순히 스크롤 이벤트와 데이터 요청(fetch)을 한 컴포넌트에 모두 넣었는데, 코드가 복잡해지고 유지보수가 어려워졌습니다.
이번 글에서는 제가 겪은 시행착오와 개선 과정을 정리해 보았습니다.
1. 초기 구현 – 컴포넌트 내부 처리
처음에는 게시판 컴포넌트 안에 스크롤 이벤트와 API 호출을 모두 넣었습니다.
useEffect(() => {
const container = containerRef.current;
if (!container) return;
const handleScroll = () => {
const { scrollTop, scrollHeight, clientHeight } = container;
if (scrollHeight - scrollTop - clientHeight < 150 && hasMore && !isLoading) {
fetchList(); // 데이터 불러오기
}
};
container.addEventListener("scroll", handleScroll);
return () => container.removeEventListener("scroll", handleScroll);
}, [fetchList]);
문제점
- 스크롤 이벤트가 너무 자주 발생해 성능 저하 우려
- fetchList 의존성으로 인해 이벤트가 불필요하게 재바인딩
- 데이터 로직과 UI가 섞여 있어 가독성이 떨어짐
2. 개선 – 커스텀 훅으로 분리
이 문제를 해결하기 위해 스크롤 로직을 커스텀 훅으로 분리했습니다.
// useInfiniteScroll.js
import { useEffect, useRef } from "react";
export function useInfiniteScroll(ref, callback, hasMore, isLoading) {
const throttleRef = useRef(null);
useEffect(() => {
const container = ref.current;
if (!container) return;
const handleScroll = () => {
if (throttleRef.current || !hasMore || isLoading) return;
throttleRef.current = setTimeout(() => {
throttleRef.current = null;
const { scrollTop, scrollHeight, clientHeight } = container;
if (scrollHeight - scrollTop <= clientHeight + 150) {
callback();
}
}, 300); // 300ms 제한
};
container.addEventListener("scroll", handleScroll);
return () => {
container.removeEventListener("scroll", handleScroll);
if (throttleRef.current) clearTimeout(throttleRef.current);
};
}, [ref, callback, hasMore, isLoading]);
}
개선 포인트
- 스로틀(throttle) 적용 → 이벤트 과도 호출 방지
- isLoading / hasMore 체크 → 중복 호출 방지
- 로직 분리 → 다른 리스트 화면에서도 재사용 가능
3. 게시판 컴포넌트에서의 사용 (예시)
import { useInfiniteScroll } from "./useInfiniteScroll";
function BoardList() {
const containerRef = useRef(null);
useInfiniteScroll(containerRef, loadData, hasMore, isLoading);
return (
<div ref={containerRef} style={{ height: "500px", overflowY: "auto" }}>
<ul>
{data.map(item => (
<li key={item.id}>{item.title}</li>
))}
</ul>
{isLoading && <p>로딩 중...</p>}
{!hasMore && <p>마지막 페이지입니다.</p>}
</div>
);
}
이제 컴포넌트는 데이터 렌더링에만 집중할 수 있게 되었습니다.
4. 트러블슈팅
무한 스크롤을 구현하면서 심각한 문제를 겪었습니다.
현상
- 스크롤을 한 번 내렸는데 API가 무한히 호출 → 서버 과부하
- 간헐적으로 스크롤이 멈추고 추가 데이터가 안 불러와지는 현상
원인
- 이벤트 중복 등록 (클린업 처리 누락)
- 조건문 미비 → 스크롤 끝 검증이 부정확
해결
- 스로틀 적용 → 300ms 제한 , 불필요한 추가 이벤트 차단
- 클린업 처리 → 언마운트 시 이벤트 제거
- 조건식 수정 → scrollHeight - scrollTop <= clientHeight + margin 기준 사용
5. 성능 개선 효과
- 성능 개선 효과를 확인하기 위해 개발자 도구의 네트워크 패널과 서버 로그를 기준으로 비교했습니다.
- 불필요한 API 호출 80% 이상 감소
- 초기 구현에서는 스크롤 이벤트가 발생할 때마다 fetchList()가 중복 호출되었습니다.
- 예를 들어, 게시판 1페이지 데이터를 불러올 때 한 번만 호출되어야 하는데, 조건이 불완전해 최대 5~10회까지 호출되는 경우가 있었습니다.
- 커스텀 훅 적용 후 스로틀(300ms 제한)과 isLoading 체크를 추가하면서 중복 호출이 사라졌습니다.
- 평균적으로 페이지 전환 시 API 호출 횟수가 최소 5회 → 1회로 줄었으므로 약 80% 이상 감소한 것입니다.
- 네트워크 트래픽 40~50% 절감
- API 중복 호출이 줄면서 전송되는 JSON 데이터 양도 크게 줄었습니다.
- 크롬 개발자 도구(Network 탭)에서 페이지 로딩 시 평균 다운로드 용량을 측정했을 때,
- 개선 전: 약 1.2MB
- 개선 후: 약 600~700KB
- 따라서 네트워크 트래픽이 40~50% 절감되었습니다.
- 체감 성능 50% 이상 향상
- 사용자 입장에서 가장 중요한 건 “체감 속도”입니다.
- 개선 전에는 스크롤을 내리면 API가 여러 번 호출되면서 응답 대기 시간이 길어졌습니다. (평균 1.5~2초 소요)
- 개선 후에는 필요한 시점에 딱 한 번 호출되면서 응답 시간이 평균 0.7~1초 수준으로 단축되었습니다.
- 따라서 사용자 경험(UX) 측면에서 약 50% 이상 빠르게 느껴지는 효과가 있었습니다.
- 불필요한 API 호출 80% 이상 감소
무한 스크롤은 사용자 경험을 크게 향상시킬 수 있는 기능이지만,
잘못 구현하면 서버 부하나 성능 저하 같은 심각한 문제가 발생할 수 있습니다.
앞으로는 새로운 기능을 구현할 때 성능과 안정성을 최우선으로 고려할 계획입니다.