본문으로 건너뛰기

무한 스크롤(Infinite Scroll) 구현 방법

질문

무한 스크롤(Infinite Scroll)은 어떻게 구현하며, 실무에서는 어떤 점을 고려해야 하나요?

답변 초안

무한 스크롤은 사용자가 페이지 하단까지 스크롤했을 때 다음 데이터를 자동으로 불러오는 UI 패턴입니다.

기존의 페이지네이션처럼 버튼을 눌러 다음 페이지를 이동하는 것이 아니라, 스크롤 위치를 기준으로 새로운 데이터를 이어 붙입니다.

즉,

"사용자가 끝까지 내려왔는가?"를 감지하고, 다음 페이지를 요청하여 기존 목록 뒤에 추가하는 방식입니다.

한 줄 비유 무한 스크롤은 책장을 넘기는 것이 아니라, 두루마리 종이를 계속 펼쳐 나가는 것과 같습니다.


무한 스크롤이 필요한 이유

게시글 목록이나 SNS 피드처럼 데이터가 많은 경우 모든 데이터를 한 번에 가져오면

  • 초기 로딩 시간이 길어지고
  • 네트워크 사용량이 증가하며
  • 브라우저 렌더링 비용도 커집니다.

반대로 페이지네이션은 사용자가 계속 버튼을 눌러야 하므로 사용성이 떨어질 수 있습니다.

무한 스크롤은 필요한 시점에만 데이터를 가져오기 때문에

  • 초기 로딩이 빠르고
  • 사용자 경험이 자연스러우며
  • 네트워크 사용량도 줄일 수 있습니다.

무한 스크롤의 동작 과정

기본적인 흐름은 다음과 같습니다.

초기 데이터 요청

목록 렌더링

사용자가 아래로 스크롤

마지막 요소가 화면에 가까워짐

다음 페이지 요청

기존 목록 뒤에 데이터 추가

반복

핵심은

"언제 다음 데이터를 요청할 것인가?"

입니다.


구현 방법 1. Scroll Event

가장 단순한 방법은 스크롤 이벤트를 감지하는 것입니다.

window.addEventListener('scroll', () => {
const reachedBottom =
window.innerHeight + window.scrollY >=
document.body.offsetHeight - 200;

if (reachedBottom) {
fetchNextPage();
}
});

스크롤 위치가 페이지 하단 근처에 도달하면 다음 데이터를 요청합니다.

장점

  • 구현이 쉽다.
  • 브라우저 호환성이 좋다.

단점

  • 스크롤 이벤트가 매우 자주 발생한다.
  • 불필요한 계산이 많다.
  • throttle이나 debounce가 필요할 수 있다.

구현 방법 2. Intersection Observer (권장)

최근에는 대부분 Intersection Observer를 사용합니다.

마지막 요소가 화면에 들어오는 순간을 감지할 수 있기 때문입니다.

const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
fetchNextPage();
}
}
);

observer.observe(lastElement);

브라우저가 교차 여부를 관리하기 때문에 Scroll Event보다 훨씬 효율적입니다.


React에서 구현하는 방법

React에서는 보통

  • 마지막 요소에 ref를 연결하고
  • Intersection Observer를 등록한 뒤
  • 화면에 보이면 다음 페이지를 요청합니다.

예를 들어

const observerRef = useRef(null);

useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
fetchNextPage();
}
}
);

if (observerRef.current) {
observer.observe(observerRef.current);
}

return () => observer.disconnect();
}, []);

그리고 마지막 요소에

<div ref={observerRef} />

를 연결합니다.


TanStack Query와 함께 사용하는 경우

실무에서는 직접 페이지 상태를 관리하기보다

useInfiniteQuery를 사용하는 경우가 많습니다.

const {
data,
fetchNextPage,
hasNextPage,
} = useInfiniteQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
getNextPageParam: (lastPage) =>
lastPage.nextCursor,
});

Intersection Observer에서

if (
entry.isIntersecting &&
hasNextPage
) {
fetchNextPage();
}

를 호출하면 됩니다.

페이지 상태와 캐싱을 직접 관리하지 않아도 되는 것이 큰 장점입니다.


Cursor 기반과 Page 기반

무한 스크롤은 데이터를 가져오는 방식에 따라 크게 두 가지가 있습니다.

Page 기반

?page=1
?page=2
?page=3

페이지 번호를 증가시키며 조회합니다.

구현은 간단하지만 데이터가 중간에 추가·삭제되면 중복이나 누락이 발생할 수 있습니다.


Cursor 기반

?cursor=100

마지막 데이터의 id나 생성 시간을 기준으로 다음 데이터를 조회합니다.

SNS나 채팅처럼 데이터가 계속 변경되는 서비스에서는 Cursor 기반이 더 안정적입니다.

실무에서는 Cursor 기반을 사용하는 경우가 많습니다.


실무에서 고려해야 할 사항

1. 중복 요청 방지

이미 요청 중이라면 추가 요청을 보내지 않아야 합니다.

if (isFetchingNextPage) return;

2. 마지막 페이지 처리

더 이상 데이터가 없다면 요청을 중단해야 합니다.

if (!hasNextPage) return;

3. Observer 해제

컴포넌트가 제거되면 Observer도 반드시 해제해야 합니다.

observer.disconnect();

그렇지 않으면 메모리 누수가 발생할 수 있습니다.


4. 로딩 UI 제공

다음 데이터를 가져오는 동안

불러오는 중...

과 같은 로딩 표시를 제공하는 것이 좋습니다.


5. 가상 스크롤(Virtual Scroll) 고려

무한 스크롤이 오래 지속되면 DOM 요소가 계속 증가합니다.

수천 개의 요소가 렌더링되면 브라우저 성능이 저하될 수 있습니다.

이 경우에는

  • react-window
  • TanStack Virtual

같은 가상 스크롤 라이브러리를 함께 사용하는 것이 좋습니다.


무한 스크롤과 페이지네이션 비교

무한 스크롤페이지네이션
사용자 경험이 자연스럽다원하는 페이지 이동이 쉽다
SNS, 쇼핑몰 피드에 적합게시판, 검색 결과에 적합
구현이 조금 더 복잡하다구현이 단순하다
DOM이 계속 증가할 수 있다DOM 개수가 일정하다

실무에서 자주 보는 조합

실무에서는 보통 다음과 같은 조합을 많이 사용합니다.

  • Intersection Observer
  • TanStack Query의 useInfiniteQuery
  • Cursor 기반 API
  • Virtual Scroll (데이터가 매우 많을 경우)

이 조합이 성능과 유지보수 측면에서 가장 많이 사용됩니다.


면접에서 자주 나오는 질문

왜 Scroll Event보다 Intersection Observer를 사용하나요?

Scroll Event는 스크롤이 발생할 때마다 이벤트가 실행되어 불필요한 계산이 많습니다.

반면 Intersection Observer는 브라우저가 요소의 화면 진입 여부를 효율적으로 관리하므로 성능이 더 좋습니다.


Cursor 방식이 Page 방식보다 좋은 이유는?

페이지 번호 방식은 데이터가 중간에 추가되거나 삭제되면 중복 조회나 누락이 발생할 수 있습니다.

Cursor 방식은 마지막으로 조회한 데이터를 기준으로 가져오기 때문에 이러한 문제가 적습니다.


무한 스크롤의 단점은 무엇인가요?

  • DOM이 계속 증가하여 성능이 저하될 수 있습니다.
  • 특정 위치로 다시 이동하기 어렵습니다.
  • 페이지 번호가 없어 공유나 북마크가 어렵습니다.

그래서 상황에 따라 페이지네이션이 더 적합한 경우도 있습니다.


실무 주의점

  • Scroll Event보다 Intersection Observer를 사용하는 것이 일반적입니다.
  • 중복 요청을 방지하기 위해 isFetchingNextPage를 확인해야 합니다.
  • 마지막 페이지 여부(hasNextPage)를 반드시 확인해야 합니다.
  • Observer는 컴포넌트 언마운트 시 해제해야 합니다.
  • 데이터가 많아질 경우 Virtual Scroll을 함께 적용하는 것이 좋습니다.
  • 실무에서는 useInfiniteQuery와 Cursor 기반 API를 함께 사용하는 경우가 많습니다.

면접 답변

"무한 스크롤은 사용자가 목록 하단에 도달했을 때 다음 데이터를 자동으로 가져와 기존 목록 뒤에 이어 붙이는 방식입니다. 과거에는 Scroll Event를 많이 사용했지만, 현재는 성능상 이점 때문에 Intersection Observer를 사용하는 경우가 대부분입니다. React에서는 마지막 요소를 Observer로 감시하고 화면에 들어오면 fetchNextPage()를 호출하는 방식으로 구현하며, TanStack Query의 useInfiniteQuery와 함께 사용하는 경우가 많습니다. 또한 중복 요청을 방지하기 위해 isFetchingNextPage를 확인하고, hasNextPage로 마지막 페이지 여부를 판단해야 합니다. 데이터가 매우 많아질 경우에는 Virtual Scroll을 함께 적용해 렌더링 성능을 최적화합니다."