
실무에서 페이지네이션이 없는 테이블을 사용했을 때, 테이블의 데이터가 많아질 수록 로딩이 느려지는 현상이 있었습니다.
이를 해결하기 위해 테이블에Intersection Observe를 활용했고, 무한스크롤을 적용하며 알게된 지식을 작성했습니다.
이번 글에서는 Intersection Observer API를 활용해 React에서 무한 스크롤을 구현하는 방법을 다룹니다.
사용자가 페이지 하단에 도달하면 자동으로 콘텐츠를 로드하는 기술입니다.
Intersection Observer는 브라우저 API로, 특정 요소가 뷰포트 또는 다른 요소(ex. div)와 교차하는 상태를 비동기로 감지합니다.
이를 통해 스크롤 이벤트를 직접 감지하지 않고도, 요소가 화면에 보이는지 여부를 알 수 있습니다.
-> 그럼 스크롤 계산 로직은 필수가 아니게 됩니다.
-> 데이터를 불러오는 교차지점을 감시한다는 개념만 기억해두시면 이해하기 쉬울 것 같습니다.
Intersection Observer는 두 가지 매개변수를 설정해 사용합니다:
관찰 중인 요소의 교차 상태가 변할 때 실행되는 함수입니다.
entries와 observer 두 개의 인자를 받습니다.
const callback: IntersectionObserverCallback = (entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
console.log('요소가 화면에 보입니다.');
}
});
};
관찰 동작을 제어하는 설정값으로 구성됩니다
root: 관찰 기준이 되는 요소. 기본값은 null로 뷰포트를 기준으로 설정됩니다.rootMargin: 관찰 기준의 여백. CSS 단위로 작성하며, "0px 0px -50px 0px"처럼 사용합니다. threshold: 요소가 어느 정도 보일 때 callback이 실행될지 결정합니다. (0~1 사이 값)const options = {
root: null,
rootMargin: '0px',
threshold: 1.0,
};
fetchPins 함수를 작성해 데이터를 가져옵니다. 무한 스크롤에서는 데이터를 계속 쌓아야 하므로 이전 데이터를 유지하면서 새로운 데이터를 추가해야 합니다. 이를 위해 pins라는 state를 사용합니다.
const Main = () => {
const [pins, setPins] = useState([]); // 데이터를 담는 state
const [page, setPage] = useState(1); // 페이지 번호 state
const [loading, setLoading] = useState(false); // 로딩 상태 관리
// 데이터를 가져오는 함수
const fetchPins = async (page: number) => {
const API_KEY = "YOUR_API_KEY_HERE"; // Unsplash API 키
const res = await fetch(
`https://api.unsplash.com/photos/?client_id=${API_KEY}&page=${page}&per_page=10`
);
const data = await res.json();
setPins(prev => [...prev, ...data]); // 기존 데이터에 새로운 데이터 추가
setLoading(true); // 로딩 완료 상태로 변경
};
// 페이지가 변경될 때마다 fetchPins 실행
useEffect(() => {
fetchPins(page);
}, [page]);
return (
<div>
{/* pins 데이터를 활용한 렌더링 */}
</div>
);
};
새로운 데이터를 가져오기 위해 페이지를 1씩 증가시키는 loadMore 함수를 작성합니다.
코드
const Main = () => {
const [page, setPage] = useState(1);
// 페이지를 1씩 증가시키는 함수
const loadMore = () => {
setPage(prev => prev + 1);
};
return (
<div>
{/* 콘텐츠와 로딩 컴포넌트 */}
</div>
);
};
무한 스크롤에서는 특정 요소가 뷰포트 내에 들어왔을 때 데이터를 가져옵니다. 이때 마지막 요소를 ref로 지정해 관찰 타겟으로 설정합니다.
코드
import { useRef } from "react";
const Main = () => {
const pageEnd = useRef<HTMLDivElement>(null); // 마지막 요소 ref 생성
return (
<div>
{/* 콘텐츠 */}
<div ref={pageEnd} />
</div>
);
};
Intersection Observer를 사용해 마지막 요소가 뷰포트 안에 들어왔을 때 데이터를 추가로 로드합니다.
구현 흐름
IntersectionObserver를 생성하고, 타겟 요소를 감지합니다.
타겟 요소가 화면에 보일 때 loadMore 함수를 호출합니다.
코드
const Main = () => {
const [loading, setLoading] = useState(false);
const pageEnd = useRef<HTMLDivElement>(null);
const loadMore = () => {
setPage(prev => prev + 1); // 페이지 증가
};
useEffect(() => {
if (loading && pageEnd.current) {
// 로딩 중일 때만 옵저버 실행
const observer = new IntersectionObserver(
entries => {
if (entries[0].isIntersecting) {
loadMore(); // 페이지 증가
}
},
{ threshold: 1 } // 100% 보여야 실행
);
// 타겟 요소 관찰
observer.observe(pageEnd.current);
// 컴포넌트 언마운트 시 옵저버 해제
return () => observer.disconnect();
}
}, [loading]);
return (
<div>
{/* 콘텐츠 */}
<div ref={pageEnd} />
</div>
);
};
코드
import React, { useState, useEffect, useRef } from "react";
const Main = () => {
const [pins, setPins] = useState([]);
const [page, setPage] = useState(1);
const [loading, setLoading] = useState(false);
const pageEnd = useRef<HTMLDivElement>(null);
const fetchPins = async (page: number) => {
const API_KEY = "YOUR_API_KEY_HERE";
const res = await fetch(
`https://api.unsplash.com/photos/?client_id=${API_KEY}&page=${page}&per_page=10`
);
const data = await res.json();
setPins(prev => [...prev, ...data]);
setLoading(true);
};
const loadMore = () => {
setPage(prev => prev + 1);
};
useEffect(() => {
fetchPins(page);
}, [page]);
useEffect(() => {
if (loading && pageEnd.current) {
const observer = new IntersectionObserver(
entries => {
if (entries[0].isIntersecting) {
loadMore();
}
},
{ threshold: 1 }
);
observer.observe(pageEnd.current);
return () => observer.disconnect();
}
}, [loading]);
return (
<div>
{pins.map((pin, index) => (
<div key={index}>{pin.description || "No Description"}</div>
))}
<div ref={pageEnd}>Loading...</div>
</div>
);
};
export default Main;
피드백은 환영입니다!