[React] 무한스크롤 구현 (Intersection Observer)
무한스크롤(Infinite Scroll)이란?
무한스크롤이란 사용자가 화면의 스크롤을 일정 수준까지 이동하면 새로운 데이터를 추가로 페칭(fetching)하여 화면에 나타냄으로써 말 그대로 스크롤을 무한히 이동하며 새로운 데이터를 볼 수 있는 것을 의미한다. 이를 구현하기 위한 방법엔 라이브러리 사용, 스크롤 위치 판단, intersection observer API 활용 등 다양한 방법이 있는데, 이 글에선 intersection observer API를 활용한 방법을 서술하려 한다.
Intersection Observer API
Intersection Observer API에 대한 글에서 설명했듯이, 이 API는 target 요소가 root 요소와 교차가 일어나는지를 판단하여 콜백 함수를 실행할 수 있다. 따라서 target 요소를 적절한 위치에 배치를 한다면 사용자가 스크롤을 이동하는 것에 따라 데이터 페칭을 구현할 수 있다.
구현 예시
-
target생성const [target, setTarget] = useState(null); const targetStyle = { width: "100%", height: "200px" }; return ( <div> <div ref={setTarget} style={targetStyle}> This is Target. </div> </div> );HTML이 생성되고 그 요소들 중 하나가 교차 상태를 판별할 대상인
target이 돼야 하므로ref와useState를 활용하여target을 지정해 줬다. 또한 요소에 크기를 정해줌으로써 교차 상태를 판별하기 쉽게 해줬다. -
observer생성useEffect(() => { let observer; if (target) { observer = new IntersectionObserver(); observer.observe(target); } }, [target]);컴포넌트가 렌더가 완료됨에 따라
observer가 생성되어야 하므로useEffect를 활용해야 한다. 또한target이 생성되기 전에observe를 시작할 수 없으므로 조건문을 넣어줬다. -
콜백함수 생성
const onIntersect = async ([entry], observer) => { if (entry.isIntersecting) { observer.unobserve(entry.target); await /* 데이터 페칭 함수*/ observer.observe(entry.target); } };교차 상태가 변화했을 때, 교차된
target인[entry]의 속성 중 하나인isIntersecting을 활용하여 교차 상태가true일 때 데이터 페칭이 이루어지도록 함수를 만들었다. 또한 사용자가 데이터 페칭이 완료되기 전에 교차 상태를 여러 번 변화시키는 상황이 발생하지 않도록unobserve를 사용하여 관찰을 중단했다가 데이터 페칭이 완료되면 다시observe를 하도록 했다. -
데이터 페칭 함수 생성
const page = 1; const fetchData = async () => { const response = await fetch(`/api/db/${page}`); const data = await response.json(); setItems((prev) => prev.concat(data.results)); page++; };async&await을 사용하여 api로부터 데이터를 페칭해오는 함수이다. fetch가 이루어질 때마다 page가 1씩 더해져 다음 fetch 때 다음 페이지의 데이터가 페칭되고, 그 데이터를 이전
moviesstate에 concat으로 합쳐줌으로써movies배열이 업데이트되고, 그에 따라 렌더가 다시 이루어져 화면에 나타나게 된다. -
최종 코드
const [items, setItems] = useState([]); // 추가된 부분 const [target, setTarget] = useState(null); const page = 1; const fetchData = async () => { const response = await fetch(`/api/db/${page}`); const data = await response.json(); setItems((prev) => prev.concat(data.results)); page++; }; // 추가된 부분 useEffect(() => { fetchData(); }, []); useEffect(() => { let observer; if (target) { const onIntersect = async ([entry], observer) => { if (entry.isIntersecting) { observer.unobserve(entry.target); await fetchData(); observer.observe(entry.target); } }; observer = new IntersectionObserver(onIntersect, { threshold: 1 }); // 추가된 부분 observer.observe(target); } return () => observer && observer.disconnect(); }, [target]); return ( <div> {items.map((item, idx) => { <div> <img src=`/item/${item.image}` alt="item.img" /> <div>{item.title}</div> </div>; })} <div ref={setTarget} style={targetStyle}> This is Target. </div> </div> );데이터를 담아줄 state를 생성하고, 초기값은 빈 배열로 설정해줬다. 또한 첫 렌더 때 첫 페칭이 이루어져야 하므로
useEffect로 첫 데이터를 호출해왔다.IntersectionObserver()에onIntersect를 콜백함수로 전달해주고,options로는{threshold: 1}을 전달해줌으로써 관찰하고 있는 대상이 화면에 완전히 보여야onIntersect가 실행되도록 했다. 또한useEffect의return에disconnect()를 넣어줬는데, 그 이유는 데이터 페칭이 완료되고 나면 업데이트 로직이 끝나기 때문에 clean-up을 통해 observer의 관찰을 일시정지하고 버그를 방지하기 위함이다.
발생할 수 있는 버그
- 첫 렌더 때 데이터 페칭이 2번 발생하는 현상
이는 리액트 생명주기 때문에 발생하는 버그이다. 생명주기 흐름 상 HTML이 먼저 렌더되고 이후 자바스크립트를 통해 hydration이 이루어지면서 데이터가 화면에 나타나게 되는데, 데이터가 화면에 나타나기 전에 observer가 관찰 중인 대상이 화면에 먼저 렌더되어 교차가 발생할 경우useEffect와onIntersect가 둘 다 실행되어 발생하는 버그이다. 이를 해결하기 위해선 조건부 렌더링을 활용하거나, Next.js와 같이 SSR 혹은 Static Site Generation이 가능한 프레임워크를 사용하여 초기화면을 구현해주면 해결할 수 있다.
참고: 문가네 개발 블로그 - Intersection Observer API란?
참고: MDN - Intersection Observer API