🖥️ 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. 성능 개선 효과

  • 성능 개선 효과를 확인하기 위해 개발자 도구의 네트워크 패널서버 로그를 기준으로 비교했습니다.
    1. 불필요한 API 호출 80% 이상 감소
      • 초기 구현에서는 스크롤 이벤트가 발생할 때마다 fetchList()가 중복 호출되었습니다.
      • 예를 들어, 게시판 1페이지 데이터를 불러올 때 한 번만 호출되어야 하는데, 조건이 불완전해 최대 5~10회까지 호출되는 경우가 있었습니다.
      • 커스텀 훅 적용 후 스로틀(300ms 제한)과 isLoading 체크를 추가하면서 중복 호출이 사라졌습니다.
      • 평균적으로 페이지 전환 시 API 호출 횟수가 최소 5회 → 1회로 줄었으므로 약 80% 이상 감소한 것입니다.
    2. 네트워크 트래픽 40~50% 절감
      • API 중복 호출이 줄면서 전송되는 JSON 데이터 양도 크게 줄었습니다.
      • 크롬 개발자 도구(Network 탭)에서 페이지 로딩 시 평균 다운로드 용량을 측정했을 때,
        • 개선 전: 약 1.2MB
        • 개선 후: 약 600~700KB
      • 따라서 네트워크 트래픽이 40~50% 절감되었습니다.
    3. 체감 성능 50% 이상 향상
      • 사용자 입장에서 가장 중요한 건 “체감 속도”입니다.
      • 개선 전에는 스크롤을 내리면 API가 여러 번 호출되면서 응답 대기 시간이 길어졌습니다. (평균 1.5~2초 소요)
      • 개선 후에는 필요한 시점에 딱 한 번 호출되면서 응답 시간이 평균 0.7~1초 수준으로 단축되었습니다.
      • 따라서 사용자 경험(UX) 측면에서 약 50% 이상 빠르게 느껴지는 효과가 있었습니다.

무한 스크롤은 사용자 경험을 크게 향상시킬 수 있는 기능이지만,
잘못 구현하면 서버 부하나 성능 저하 같은 심각한 문제가 발생할 수 있습니다.

앞으로는 새로운 기능을 구현할 때 성능과 안정성을 최우선으로 고려할 계획입니다.