
최근 프로젝트에서 무한 스크롤 기능을 구현하게 됐다.
여러 방법을 비교해본 끝에 최종적으로 Intersection Observer를 사용했고, 만족스러워서 기록 겸 정리해본다.
window의 스크롤 이벤트를 감지해서,
스크롤이 바닥 근처에 도달하면 데이터를 추가로 불러오는 방식.
거의 모든 브라우저에서 동작
구현이 단순하고 직관적
스크롤 이벤트가 너무 자주 발생 → 성능 이슈
→ 쓰로틀링(throttling)이나 디바운싱(debouncing)이 필수
여러 스크롤 영역(모달, 특정 div 등)에 적용하려면 관리가 복잡
코드가 지저분해지기 쉬움
특정 DOM 요소가 화면에 보이는지 여부를 감지해서,
화면에 나타나면 데이터를 추가로 로드하는 방식.
브라우저 레벨에서 최적화됨 → 성능 매우 우수
코드가 간결하고 유지보수에 유리
React 같은 프레임워크와 궁합이 좋음
구형 브라우저(IE 등)에서는 미지원
관찰 대상 요소 위치, threshold 등 세부 조정이 필요
(예: react-infinite-scroll-component, react-intersection-observer 등)
위 두 방식을 내부적으로 캡슐화한 컴포넌트/훅 형태로 제공.
더 쉽게 사용할 수 있도록 추상화돼 있음.
코드가 매우 간결
로딩/에러 처리, 마지막 페이지 감지 등 부가기능이 잘 갖춰져 있음
프로젝트에 따라 무거울 수 있음
커스터마이징이 제한적일 수 있음
이번 프로젝트에서는 Intersection Observer API를 직접 사용해서 구현했다.
코드도 깔끔하고 성능도 좋아서, 앞으로도 이 방식으로 구현할 듯하다.
const observer = new IntersectionObserver(callback, options);
observer.observe(targetElement);
callback: 대상 요소가 화면에 들어오거나 나갈 때 실행되는 함수options: 감지 기준을 설정하는 옵션 객체observe(): 감지할 대상 DOM 요소 지정root: 기준이 되는 요소 (null이면 브라우저 뷰포트 기준)threshold: 요소가 얼마나 보여야 감지할지 비율 (0.0 ~ 1.0)rootMargin: root 기준 여백 설정 (ex: "0px 0px 100px 0px" → 아래쪽 100px 미리 감지)import React, { useEffect, useRef, useState } from "react";
import styled from "styled-components";
// 더미 데이터 100개 생성 (id, 제목, 이미지)
const dummy = Array.from({ length: 100 }, (_, i) => ({
id: i + 1,
title: `카드 ${i + 1}`,
img: `https://picsum.photos/seed/${i + 1}/200/120`,
}));
function BlogInfiniteScrollDemo() {
// 한 번에 보여줄 카드 개수 상태
const [visible, setVisible] = useState(8);
// loader div를 참조할 ref
const loader = useRef(null);
useEffect(() => {
// Intersection Observer로 loader가 화면에 보이면 카드 추가
const io = new IntersectionObserver(
([e]) => {
if (e.isIntersecting) setVisible((v) => Math.min(v + 8, dummy.length));
},
{ threshold: 0.2 } // loader가 20% 보이면 트리거
);
if (loader.current) io.observe(loader.current);
return () => io.disconnect(); // 언마운트 시 observer 해제
}, []);
return (
<Wrap>
{/* 카드 리스트 (grid) */}
<Grid>
{dummy.slice(0, visible).map((item) => (
<CardBox key={item.id}>
<Img src={item.img} alt="" />
<Title>{item.title}</Title>
</CardBox>
))}
</Grid>
{/* loader: 화면에 보이면 추가 카드 로드 */}
<Loader ref={loader}>
{visible < dummy.length ? "불러오는 중..." : "끝"}
</Loader>
</Wrap>
);
}
export default BlogInfiniteScrollDemo;
observe(): 감지할 요소 등록isIntersecting: true면 요소가 화면 안에 들어왔다는 의미threshold: 감지 민감도 설정 → 0이면 살짝 보이기만 해도 감지rootMargin: 감지 범위를 키워서 프리로딩 효과 가능