React에서 효율적으로 react 무한스크롤 구현 하기 실무 가이드
react 무한스크롤 구현을 통해 대용량 목록을 부드럽게 로드하는 방법을 정리합니다. 개념, 코드 예제, 성능·보안 고려사항, 실무 체크리스트까지 포함한 실전 가이드입니다. 실무 적용 흐름과 자주 하는 실수까지 함께 확인할 수 있습니다.
react 무한스크롤 구현이 필요한 상황
react 무한스크롤 구현은 사용자가 스크롤하면서 추가 데이터를 점진적으로 불러와야 할 때 필요합니다. 이 글에서는 개념 이해, 실무 적용 코드, 서버-클라이언트 페이징 전략, 주의점과 체크리스트까지 설명해 바로 현업에 적용할 수 있는 방법을 제공합니다.
react 무한스크롤 구현 핵심 개념
무한스크롤은 기본적으로 클라이언트가 더 많은 데이터를 요청하도록 트리거를 만들고, 서버는 페이징 가능한 응답을 제공합니다. 흔히 사용하는 방법은 스크롤 이벤트를 직접 감지하거나 Intersection Observer API를 사용하는 것입니다. 핵심 요소:
- 트리거 포인트: 마지막 아이템 근처에 가상 엘리먼트를 두고 감지
- 페이징 전략: 페이지 번호 vs 커서(cursor)
- 로딩 상태와 에러 처리
- 중복 요청 방지 및 요청 취소
Intersection Observer를 이용한 예제
아래는 간단한 React 훅 기반 구현 예시입니다. 이 코드는 마지막 리스트 항목에 ref를 달아 Intersection Observer로 추가 로드를 트리거합니다.
// InfiniteScrollList.jsx import React, { useEffect, useRef, useState, useCallback } from 'react'; function useInfiniteScroll(fetchMore, hasMore) { const observerRef = useRef(); const lastRef = useCallback(node => { if (!hasMore) return; if (observerRef.current) observerRef.current.disconnect(); observerRef.current = new IntersectionObserver(entries => { if (entries[0].isIntersecting) { fetchMore(); } }, { rootMargin: '200px' }); if (node) observerRef.current.observe(node); }, [fetchMore, hasMore]); return lastRef; } export default function InfiniteScrollList({ fetchPage }) { const [items, setItems] = useState([]); const [page, setPage] = useState(1); const [hasMore, setHasMore] = useState(true); const [loading, setLoading] = useState(false); const fetchMore = useCallback(async () => { if (loading) return; setLoading(true); try { const res = await fetchPage(page); setItems(prev => [...prev, ...res.data]); setHasMore(res.hasMore); setPage(p => p + 1); } catch (e) { console.error(e); } finally { setLoading(false); } }, [fetchPage, page, loading]); const lastRef = useInfiniteScroll(fetchMore, hasMore); return ( <div> {items.map((item, idx) => ( <div key={item.id} ref={idx === items.length - 1 ? lastRef : null}> {item.title} </div> ))} {loading && <div>로딩중...</div>} {!hasMore && <div>마지막입니다</div>} </div> ); }
위 코드는 기본 패턴이며, 실제 서비스에서는 요청 취소(AbortController), 에러 UI, 재시도 전략 등을 추가해야 합니다.
react 무한스크롤 구현 실무 예제: React Query와 커서 페이징
React Query를 쓰면 캐싱과 중복요청 방지, 백그라운드 리페치가 편리합니다. 커서 기반 페이징을 사용하면 데이터 변경에 더 견고합니다.
// useInfiniteQuery 예시 (react-query) import { useInfiniteQuery } from 'react-query'; function usePosts() { return useInfiniteQuery('posts', ({ pageParam = null }) => fetch(`/api/posts?cursor=${pageParam}`).then(res => res.json()) , { getNextPageParam: lastPage => lastPage.nextCursor || undefined }); }
언제 쓰면 좋고 언제 피해야 할까
언제 쓰면 좋은가
- 사용자가 연속적으로 목록을 탐색하는 인터페이스(피드, 타임라인)
- 페이지 전환을 최소화하고 자연스러운 탐색 경험을 원할 때
- 서버에서 페이징 API를 제공할 수 있을 때
언제 피해야 하는가
- 사용자가 특정 위치로 바로 이동해야 하는 경우(예: 정확한 페이지 번호 이동)
- SEO가 중요해 서버 사이드 렌더링이 필수인 페이지(검색 엔진 크롤러가 콘텐츠를 읽어야 하는 경우)
- 리스트가 단기간에 자주 변경되어 캐싱/동기화 비용이 큰 경우
비교 기준표: 페이징 전략 비교
| 구분 | 항목1 | 항목2 | 항목3 |
|---|---|---|---|
| 내용 | 페이지 번호 방식: 구현 간단 | 커서 방식: 데이터 변경에 안전 | 오프셋 방식: 특정 위치 접근 쉬움 |
실무 체크리스트
- API가 페이징(페이지/커서) 정보를 명확히 제공하는가
- 중복 요청 방지를 위한 플래그 또는 라이브러리 사용 여부
- 요청 취소(AbortController) 구현 여부
- 로딩/에러/빈 목록 UI 정의 여부
- 스켈레톤 또는 로더로 레이아웃 전환 방지
- 접근성(키보드 포커스, 스크린리더) 고려 여부
성능·보안·운영 주의점
- 성능: 계속 누적되는 DOM 노드는 메모리/렌더 비용을 증가시킴 — 필요 시 목록 가상화(react-window, react-virtualized)를 도입
- 보안: 페이지 매개변수로 노출되는 민감 정보 금지, 페이징 파라미터 검증 필요
- 운영: 로그를 통해 API 사용량과 지연 시간 모니터링, 백오프 전략(지수 재시도) 적용
- 비용: 고객 트래픽 패턴에 따라 서버 비용이 급증할 수 있으므로 페이징 크기와 캐시 전략 조정
자주 묻는 질문
Q1: Intersection Observer와 스크롤 이벤트 중 무엇이 좋나요? A1: 일반적으로 Intersection Observer가 더 효율적이고 편리합니다. 스크롤 이벤트는 빈번한 핸들러로 성능 문제와 복잡한 디바운스 처리가 필요합니다.
Q2: SEO가 필요한 피드도 무한스크롤을 써도 되나요? A2: SEO가 중요하면 서버 사이드 렌더링이나 하이브리드 접근(초기 콘텐츠 SSR, 이후 클라이언트 로드)을 고려해야 합니다.
Q3: 리스트가 커졌을 때 DOM 문제를 어떻게 해결하나요? A3: react-window나 react-virtualized 같은 가상화 라이브러리를 사용해 렌더된 DOM 수를 제한하세요.
Q4: 네트워크 오류가 잦은 환경에서는 어떻게 해야 하나요? A4: 지수 백오프, 재시도 횟수 제한, 사용자에게 재시도 버튼 제공 등을 구현하세요.
정리
- react 무한스크롤 구현은 사용자 경험 향상에 효과적이며, Intersection Observer와 커서 페이징 조합을 추천합니다.
- 실무에서는 요청 취소, 중복 방지, 스켈레톤 UI, 목록 가상화가 핵심입니다.
- SEO, 메모리 사용, API 설계 등 운영 측면을 반드시 검토하세요.
- 체크리스트를 따라 구현하고, 모니터링으로 실제 트래픽에서 동작을 검증하세요.