무한 스크롤
Intersection Observer
- 특정 DOM 요소가 View Port에 진입했는지 또는 벗어났는지를 비동기적으로 관찰할 수 있게 해주는 브라우저 내장 API
- 스크롤 이벤트 없이도 효율적으로 요소의 가시 상태 추적 가능
작동 흐름
1. 로딩 트리더용 div를 맨 아래에 렌더링, IntersectionObserver로 감시
2. 해당 요소가 화면에 보이면(isIntersecting === true), 다음 페이지 데이터 요청
3. 데이터를 받아오면 기존 리스트에 추가하고, 페이지 증가
4. 데이터가 없으면 옵저버 등록 해제(observer.unobserve)로 불필요한 호출 막기
function InfiniteScrollList() {
const [items, setItems] = useState([]); // 아이템 목록
const [page, setPage] = useState(1); // 현재 페이지
const [hasMore, setHasMore] = useState(true); // 더 가져올 데이터 있는지 확인
const loader = useRef(null); // 옵저버 대상 요소
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting && hasMore) {
loadMore(); // 다음 페이지 요청
}
},
{ threshold: 1.0 }
);
const el = loader.current;
if (el) observer.observe(el);
return () => {
if (el) observer.unobserve(el); // 클린업
};
}, [loader, hasMore]);
const loadMore = async () => {
const res = await fetch(url);
const data = await res.json();
setItems(prev => [...prev, ...data.items]);
setPage(prev => prev + 1);
if (data.items.length === 0) setHasMore(false); // 더 이상 아이템 없으면 종료
};
return (
<div>
{items.map((item, idx) => (
<div key={idx}>{item.title}</div>
))}
<div ref={loader} /> {/* 관찰 대상 */}
</div>
)
}
const [items, setItems] = useState([]);
- 불러온 아이템 목록 상태
- API에서 받은 데이터를 누적해서 렌더링
const [page, setPage] = useState(1);
- 현재 페이지 번호 상태
- 서버에 요청할 때 쿼리 파라미터로 사용 (?page=1, ?page=2)
const [hasMore, setHasMore] = useState(true);
- 더 불러올 데이터가 있는지 여부
- 더 이상 데이터가 없으면 로딩 중단 (옵저버 작동 중지)
const loader = useRef(null);
- 로딩 트리거용 요소를 참조
- <div ref={loader} 요소가 뷰포트에 보일 때 옵저버가 감지함
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting && hasMore) {
loadMore(); // 뷰포트에 들어오면 다음 페이지 요청
}
},
{ threshold: 1.0 }
);
- 옵저버 인스턴스 생성
- entry.isIntersecting : 요소가 화면에 보이면 true
- threshold: 1.0 : 요소가 100% 보여질 때 감지
- loadMore() : 새로운 데이터 요청
const el = loader.current;
if (el) observer.observe(el);
- 옵저버에 로딩 요소 등록
- 이 요소가 뷰포트에 들어오면 loadMore 호출
return () => {
if (el) observer.unobserve(el);
};
- 정리 함수 (unmount 또는 deps 변경 시)
- 옵저버 해제
const loadMore = async () => {
- 데이터 요청 함수
- 뷰포트에 요소가 감지되었을 때 실행
setItems(prev => [...prev, ...data.items]);
- 기존 목록에 새 데이터 추가 (누적)
setPage(prev => prev + 1);
- 다음 페이지 번호로 증가 (다음 요청 대비)
if (data.items.length === 0) setHasMore(false);
- 불러온 데이터가 없으면 hasMore를 false로 설정해 더 이상 요청하지 않도록 막음
페이지네이션 API 설계 시 고려사항
| limit | 페이지당 데이터 수 | LIMIT 10 |
| offset | 건너뛸 데이터 수 | OFFSET 20 (3페이지 -> (3-1)*10) |
| totalCount | 전체 데이터 수 | SELECT COUNT(*) FROM articles; |
| currentPage | 현재 요청한 페이지 번호 | /api/articles?page=3 |
| totalPages | 전체 페이지 수 | Math.ceil(totalCount / limit); |
| hasNextPage | 다음 페이지 유무 | boolean |
{
"items": [/* ... */],
"pagination": {
"currentPage": 3,
"totalPages": 5,
"limit": 10,
"totalCount": 45,
"hasNextPage": true
}
}

- 무한스크롤에서는 cursor 기반 페이징이 더 안정적
'카테캠 > 2단계' 카테고리의 다른 글
| [7/22] React Query (1) | 2025.07.22 |
|---|---|
| [7/22] 무한 스크롤 옵저버 (0) | 2025.07.22 |
| [7/18] Fetch API Wrapping (0) | 2025.07.18 |
| [7/17] local storage (0) | 2025.07.18 |
| [7/17] 로그인 axios post (3) | 2025.07.18 |