리액트에서 페이징 처리를 해야하는 상황이였다...
주로 앱을 작업하였기 때문에 Swift 에서는 tablecell 로 스크롤이 마지막에 닿으면 그때 쓰레드를 통한 페이징처리를 작업하였고
Java에서도 역시 recyclerview 를 이용한 페이징 처리를 진행하였다.
하지만 웹에서는 어떻게 스크롤 끝을 감지하여 페이징하는지 잘 몰라서 검색해보았다..
무한 스크롤을 통해서 페이징 처리를 하려고 했기 때문에 정보가 필요했다.
뭔가 스크롤 끝 감지해서 그 끝에서 로직을 처리해주면 되는데 스크롤 끝은 감지하는 부분을 몰랐다.
무한스크롤 구현 !
1. Scroll Event ( 스크롤 이벤트 )
처음에는 scroll 이벤트를 감지하다가, 페이지 가장 아래에 닿았을 때 쯤 API 요청을 하면 되지 않을까? 싶었다.
하지만 이렇게 구현하게 되면 scroll 이벤트를 계~속 감지해야하기 때문에 성능 저하로 이어질 수 있는 문제가 있다.
이를 해결하기 위해 debounce나 throttle를 적용해 일부 성능을 개선할 수 있다. 두가지 모두 시간의 텀을 두어 조금이나마 이벤트 감지 횟수를 줄일 수 있게 한다.
Debounce 적용
import React, { useState, useEffect, useRef, useCallback } from "react";
import _ from "lodash"; // lodash 라이브러리 사용
const fetchData = async (page) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(Array.from({ length: 10 }, (_, i) => `Item ${page * 10 + i + 1}`));
}, 1000);
});
};
const InfiniteScrollDebounced = () => {
const [items, setItems] = useState([]);
const [page, setPage] = useState(1);
const [loading, setLoading] = useState(false);
const listRef = useRef(null);
useEffect(() => {
const loadMore = async () => {
setLoading(true);
const newItems = await fetchData(page);
setItems((prev) => [...prev, ...newItems]);
setLoading(false);
};
loadMore();
}, [page]);
// ✅ `debounce` 적용 (300ms 동안 스크롤 이벤트 연속 발생 시, 마지막 이벤트만 실행)
const handleScroll = useCallback(
_.debounce(() => {
if (!listRef.current) return;
const { scrollTop, scrollHeight, clientHeight } = listRef.current;
if (scrollTop + clientHeight >= scrollHeight - 100 && !loading) {
setPage((prev) => prev + 1);
}
}, 300),
[loading]
);
useEffect(() => {
const container = listRef.current;
if (!container) return;
container.addEventListener("scroll", handleScroll);
return () => container.removeEventListener("scroll", handleScroll);
}, [handleScroll]);
return (
<div
ref={listRef}
style={{
height: "500px",
overflowY: "auto",
border: "1px solid black",
padding: "10px",
}}
>
<h2>Infinite Scroll (Debounce 적용)</h2>
<ul>
{items.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
{loading && <p>Loading...</p>}
</div>
);
};
export default InfiniteScrollDebounced;
throttle 적용
const handleScroll = useCallback(
_.throttle(() => {
if (!listRef.current) return;
const { scrollTop, scrollHeight, clientHeight } = listRef.current;
if (scrollTop + clientHeight >= scrollHeight - 100 && !loading) {
setPage((prev) => prev + 1);
}
}, 500), // 500ms마다 실행
[loading]
);
🔥 debounce vs throttle 차이
방식동작 방식사용 예시
Debounce | 이벤트 발생 후 일정 시간이 지나면 실행 (마지막 이벤트만 처리) | 검색창 입력, 스크롤 감지 |
Throttle | 일정 간격마다 실행 (연속 실행 제한) | 무한 스크롤, 마우스 이동 감지 |
💡 무한 스크롤에서는 throttle이 적합하지만, 너무 자주 호출되면 debounce도 고려할 수 있음!
2. Intersection Observer API ( 인터섹션 옵저버 )
우선은 Intersection Observer API를 사용하는 방법을 택하기로 한다.
Intersection Observer API는 타겟 요소와 상위 요소 또는 최상위 document 의 viewport 사이의 intersection 내의 변화를 비동기적으로 관찰하는 방법입니다. (출처: MDN)
해당 API를 통해 스크롤을 내리다가 타겟이 viewport에 들어오면 데이터를 호출하는 식으로 구현할 수 있다.
📌 주요 기능
- 스크롤이 하단에 도달하면 다음 페이지의 데이터를 불러옴
- IntersectionObserver를 사용하여 특정 요소가 화면에 보일 때 API 호출
- React의 useState와 useEffect를 활용
✅ 코드 구현 (React + Intersection Observer)
import React, { useEffect, useState, useRef, useCallback } from "react";
const fetchData = async (page) => {
// 임시 API 데이터 호출 시뮬레이션
return new Promise((resolve) => {
setTimeout(() => {
resolve(Array.from({ length: 10 }, (_, i) => `Item ${page * 10 + i + 1}`));
}, 1000);
});
};
const InfiniteScroll = () => {
const [items, setItems] = useState([]); // 아이템 목록
const [page, setPage] = useState(1); // 현재 페이지
const [loading, setLoading] = useState(false); // 로딩 상태
const observerRef = useRef(null); // Intersection Observer 참조
const lastItemRef = useCallback(
(node) => {
if (loading) return;
if (observerRef.current) observerRef.current.disconnect();
observerRef.current = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) {
setPage((prev) => prev + 1);
}
});
if (node) observerRef.current.observe(node);
},
[loading]
);
useEffect(() => {
const loadMore = async () => {
setLoading(true);
const newItems = await fetchData(page);
setItems((prev) => [...prev, ...newItems]);
setLoading(false);
};
loadMore();
}, [page]);
return (
<div>
<h1>Infinite Scroll with Intersection Observer</h1>
<ul>
{items.map((item, index) => (
<li key={index} ref={index === items.length - 1 ? lastItemRef : null}>
{item}
</li>
))}
</ul>
{loading && <p>Loading...</p>}
</div>
);
};
export default InfiniteScroll;
✅ 코드 설명
- fetchData(page): 페이지 단위로 데이터를 가져오는 함수 (API 요청을 가정한 비동기 처리)
- useState:
- items: 불러온 데이터 목록
- page: 현재 페이지 번호
- loading: 데이터를 불러오는 중인지 여부
- useEffect:
- page가 변경될 때마다 새로운 데이터를 로드
- IntersectionObserver:
- useCallback을 사용하여 lastItemRef가 마지막 아이템을 감지
- 마지막 아이템이 화면에 보이면 setPage를 증가시켜 다음 데이터를 가져옴
- 기존 observerRef.current를 disconnect 후 새로운 observer를 등록하여 중복 감지 방지

좀 더 살펴보자면,
1. Target
const [target, setTarget] = useState(null);
const targetStyle = { width: "100%", height: "200px" };
return (
<div>
<div ref={setTarget} style={targetStyle}>
This is Target.
</div>
</div>
);
2. Observer
useEffect(() => {
let observer;
if (target) {
observer = new IntersectionObserver();
observer.observe(target);
}
}, [target]);
3. Callback
const onIntersect = async ([entry], observer) => {
if (entry.isIntersecting) {
observer.unobserve(entry.target);
await /* 데이터 페칭 함수*/
observer.observe(entry.target);
}
};
4. Fetching
const page = 1;
const fetchData = async () => {
const response = await fetch(`/api/db/${page}`);
const data = await response.json();
setItems((prev) => prev.concat(data.results));
page++;
};
이렇게 적용해보면 어떨지 싶다.. !!
진짜 많이 발전한듯하다 코드들이..
스크롤 감지 부분은 옛날 코드에서는 좀 길었던 것 같긴 했었다... ( 내가 정보력이 없었나 ??)
근데 이 코드 하나만으로 맨 끝 감지할 수 있어서 정말로 편했다.
Swift , Aos 같은 경우는 Collectionview , Recyclerview 의 리스너에서 맨 밑 감지하도록 높이나 메소드를 가져와서 하는데 React 는 자바스크립트 기반이라 바로 한 코드로 할 수 있어서 편했다.
'개발 > React' 카테고리의 다른 글
React_ Semantic 구조 (2) | 2024.08.07 |
---|---|
React_ npm module @types 의 의미 (0) | 2024.07.29 |
React 프로젝트 생성 feat. Typescript (0) | 2024.06.13 |
React 새로고침 데이터 저장 (1) | 2024.06.09 |
React useMemo.. useCallback.. memo.. (1) | 2024.06.09 |