Intersection Observer API를 통한 무한 스크롤 구현 ( feat. React + typescript )

SOPLAY·2023년 5월 16일
8

Intersection Observer API 란 ?

Intersection Observer API는 타겟 요소와 상위 요소또는 최상위 document의 viewport 사이의 intersection 내의 변화를 비동기적으로 관한하는 방법입니다. - MDN -

보통 viewport상에 요소가 올라왔는지에 따라 실행할 콜백을 전달하는 방식으로 사용을합니다.

기본 적인 사용방법은 다음과 같습니다.

const options = {
	root : "viewport요소",
	rootMargin : "margin 값",
	threshold : "0.5같은 가시성 퍼센티지"
}
const callback = (entries, observer) => {
	entries.forEach(entry => {
		//화면상에 보이지 않을때
		if(!entry.isIntersecting) return;
		//화면상에 보일때 실행 시키고 싶은 코드 작성

		//entry가 가진 속성들
    	// Each entry describes an intersection change for one observed
	    // target element:
	    //   entry.boundingClientRect
	    //   entry.intersectionRatio
	    //   entry.intersectionRect
	    //   entry.isIntersecting
	    //   entry.rootBounds
	    //   entry.target
	    //   entry.time
	});
}
const observer = new IntersectionObserver(callback,options)

Intersection observer options

Intersection observer() 생성자에 전달되는 optionsroot, rootMargin, threshold로 구성되어 있습니다.

  • root : target의 가시성을 판단하는 기준인 viewport 요소 잆니다. null이거나 지정되지 않으면 document의 viewport를 기본값으로 가지게 됩니다.
  • rootMargin : root가 가지게 되는 여백입니다. ( css의 margin 속성과 유사합니다.)
  • threshold : observer의 콜백이 실행될 대상 요소의 가시성 퍼센티지를 나타내는 단일 숫자 혹은 숙자 배열입니다. (ex - 50%가 보였을때 콜백이 실행시키고 싶다면 0.5로 설정하면 됩니다.)

Intersection observer callback

감시 대상의 교차(intersection) 정보를 받아 처리합니다.

이 교차 정보는 IntersectionObserverEntry 객체로 표현이 되어 있으며, 다음과 같은 속성을 가집니다.

  • isIntersecting: 요소가 뷰포트 내에 보이는지 여부를 나타냅니다.
  • intersectionRatio: 요소가 뷰포트와 교차하는 영역의 비율을 나타냅니다.
  • boundingClientRect: 요소의 크기와 위치를 나타내는 DOMRect 객체입니다.
  • rootBounds: 뷰포트의 크기와 위치를 나타내는 DOMRect 객체입니다.
  • target: 감시 대상 요소입니다.

Intersection Observer API를 활용한 무한 스크롤 구현

비동기 데이터를 기반으로 React를 활용하여 간단하게 무한스크롤을 구현해보겠습니다!

1. fakeAPI 구현

비동기로 페이지네이션 기반의 api를 흉내내서 값을 한번 받아올 수 있는 fakeAPI 함수를 구현해보겠습니다.

이 함수가 받아야 하는 매개변수로는
1. currentPage : 현재 페이지
2. pageSize : 한 페이지당 게시물의 크기
3. delay : 실제 네트워크상으로 받아올 경우 받아오는 시간이 있으므로 이부분을 통해 임의의 값을 설정할 수 있습니다. ( 값이 없을경우 0으로 처리 됩니다.)

이 값들을 받아 Promise 배열형태로 [1,2,3,4,5]같은 값을 return 합니다.

이를 코드로 작성하면 다음처럼 작성할 수 있습니다.

const fakeAPI = async (currentPage: number, pageSize: number, delay = 0) => 
	await new Promise((resolve) => setTimeout(resolve, delay)).then(() =>
		Array.from(
			{ length: pageSize },
			(_, i) => pageSize * (currentPage - 1) + i + 1
		)
	);

export default fakeAPI;

2. useIntersectionObserver hooks 작성

IntersectionObserver를 hooks 패턴을 사용해서 생성해보겠습니다.

이 hooks 가 받아야하는 매개변수로는
1. callback : 콜백 함수를 받습니다.

이 hooks의 return값으로는
1. observe : observer.current.observer()의 동작을 하면서 이를 해지 하는 함수를 받을 예정입니다.

이를 코드로 보면 다음과 같습니다.

import { useRef } from 'react';
  
const useIntersectionObserver = (cb: () => void) => {
	const observer = useRef(
		new IntersectionObserver(
			(entries, observer) => {
				entries.forEach((entry) => {
					if (!entry.isIntersecting) return;
					cb();
				});
			},
			{ threshold: 0.4 } //40%가 보일때를 기본 값으로 설정 했습니다.
		)
	);
	  
	/**
	*@returns {()=>void} unobserve:()=>viod 를 반환한다.
	*/
	const observe = (element: HTMLElement) => {
		observer.current.observe(element);
		return () => observer.current.unobserve(element);
	};
	return { observe };
};
  
export default useIntersectionObserver;

여기서 useRef로 IntersectionObserver 생성자를 감싼이유는 react의 생명주기동안 값이 살아있어야 하지만 이 값이 변경되었을때 리렌더링이 필요하지 않아서 useRef를 활용해서 처리했습니다.

컴포넌트 작성

이제 앞서 작성한 fakeAPI와 useIntersectionObserver hooks를 활용해서 무한스크롤을 구현해보겠습니다.

요구 사항을 간단하게 보자면
1. page네이션 기반 fakeAPI 활용
2. observe하는 HTMLElements에 도달할 경우 useIntersectionObserver hooks에 전달한 콜백함수 실행
정도가 있습니다.

이를 기반으로 코드로 구현하면 다음과 같습니다.

import * as React from 'react';
import fakeAPI from './api/fakeAPI';
import useIntersectionObserver from './hooks/useIntersectionObserver';
import './style.css';
  
export default function App() {
	const pageSize = 10;
	const [page, setPage] = React.useState(1);
	const [data, setDate] = React.useState([]);
	
	const scrollRef = React.useRef(null);
	  
	const observer = useIntersectionObserver(() => {
		setPage((page) => page + 1);
	});
	  
	// useEffect를 활용해서 scrollRef가 null이 아니라면 observe 실행
	  
	React.useEffect(() => {
		const unobserve = observer.observe(scrollRef.current);
	  
		return () => observer && unobserve();
	}, [scrollRef]);
	  
	React.useEffect(() => {
		const getData = async () => {
			setDate([
			...data,
			...(await fakeAPI(page, pageSize, page === 1 ? 0 : 100)),
			]);
		};
		getData();
	}, [page]);
	  
	return (
		<div className="container">
			<header className="page-info">
				loadPage = {page}, pageSize={pageSize}
			</header>
			<div className="data-container">
				{data.map((v, i) => (
					<div className="data-item" key={i}>
						{v}
					</div>
				))}
				<div className="scroll-observer" ref={scrollRef}>
					로딩...
				</div>
			</div>
		</div>
	);
}

코드 분석

useIntersectionObserver 파트

const observer = useIntersectionObserver(() => {
	setPage((page) => page + 1);
});
  • callback 함수로 useState로 생성한 page를 +1 해주는 콜백을 전달합니다.

observer.observe 파트

React.useEffect(() => {
	const unobserve = scrollRef && observer.observe(scrollRef.current);
	return () => observer && unobserve();
}, [scrollRef]);

scrollRef 가 존재할 경우 scrollRef.current를 observe 하게 동작합니다.

이 observe 매서드의 경우 이전에서 봤던 코드와 같이 ()=>unobserve()를 반환하는데 이를 useEffect의 return 에서 실행해서 라이프 사이클을 조절합니다.

비동기 fackAPI 파트

React.useEffect(() => {
	const getData = async () => {
		setDate([
		...data,
		...(await fakeAPI(page, pageSize, page === 1 ? 0 : 100)),
		]);
	};
	getData();
}, [page]);

useEffect를 활용해서 page의 변화가 생겼을 때만 실행하게끔 값을 지정한 후
useEffect내부에서 비동기 처리를 합니다.


완성!!

완성본 소스코드

참고


profile
꾸준히 발전하는 개발자

0개의 댓글