본문 바로가기
Web/React

무한스크롤 : IntersectionObserver와 useRef

by 가나닩 2024. 4. 14.

검색결과 등과 같이 방대한 양의 데이터를 화면에 표시하고 싶은 경우가 있다. API요청등을 통해 수많은 결과를 한번에 가져오고 화면에 표시하면 프로그래밍 난이도는 낮아지겠지만 여러가지 문제점이 발생한다.

  1. 과도한 DOM 노드 수 증가에 따른 성능 저하
  2. 메모리 사용량 증가 (성능 저하)
  3. 네트워크 리소스 과부하
  4. 사용자 경험을 고려하지 않은 비효율적인 데이터 처리

이러한 문제를 해결하기 위해서는 페이지를 나누는 방법도 있지만 사용자경험 향상을 위해서 무한스크롤을 사용해볼 수 있다.

 

IntersectionObserver

무한스크롤 기능을 구현할때 IntersectionObserver를 사용하면 성능 저하를 최소화하여 간단하게 구현해 볼 수 있다.

const [movies, setMovies] = useState([]);
const [searchPage, setSearchPage] = useState(1);
const loader = useRef(null);
const [nowPage, setNowPage] = useState(0);

// API 요청
useEffect(() => {
        if (getItem) {
            fetch(/* API 요청 코드 */)
                .then((response) => response.json())
                .then((data) => {
                    if (movies.length > 0) {
                        setMovies((prevData) => [...prevData, ...data.results]);
                        setNowPage((prev) => prev + 1);
                        //api요청 완료시마다 증가시켜 무한스크롤 감지를 활성화
                    } else {
                        setMovies(data.results);
                        setNowPage(1);
                        //최초 api요청 완료후 loader 감지 시작을 위해 1로 변경
                    }
                });
        }
    }, [searchPage]);
    // Observer의 콜백함수가 실행되어 searchPage값이 증가하면 API요청을 실행

useEffect(() => {
// 무한 스크롤을 위한 요소 감지
    const observer = new IntersectionObserver(
        (entries) => {
            if (entries[0].isIntersecting && nowPage != 0) {
            //최초 로딩전 loader 감지 방지를 위한 조건식
                setSearchPage(searchPage + 1);
                //searchPage 값을 증가시켜 다음페이지의 api요청이 가능하도록 함
            }
        },
        { threshold: 1 }
        //loader 요소가 viewport에 얼마나 보이는가 (0~1)
    );
    if (loader.current) observer.observe(loader.current);
    return () => observer.disconnect();
}, [nowPage]);
//API 요청이 완료되고 데이터 저장을 마친뒤 nowPage의 값이 증가하면 다시 무한스크롤 요소를 감지


return (
    <>
        <div>
            <div ref={loader}></div>
        </div>
    </>
);

 

IntersectionObserver는 특정 DOM 요소가 viewport에 얼마나 보이는가를 감지하는 기능이다.

기본적으로는 useEffect를 통해 페이지 렌더링 처음에 활성화하여 사용할 수 있으며 여러가지 조건을 붙일수도 있다.

 

IntersectionObserver 내부를 자세히 살펴보면

const observer = new IntersectionObserver(
    (entries) => {
        if (entries[0].isIntersecting && nowPage != 0) {
            setSearchPage(searchPage + 1);
        }
    },
	{ threshold: 1 }
);

 

하나의 콜백함수와 옵션객체를 인자로 받는다.

  • 콜백함수 : 관찰되는 요소가 특정 조건을 만족할때 실행되는 코드이다. entries[0]는 관찰 첫번째 대상을 의미하고 isIntersecting은 관찰대상이 화면에 보이는 상태인지를 판별한다. 관찰대상이 조건을 만족하여 실행되면 searchPage의 상태값을 1 증가시킨다.
  • 옵션객체 : threshold값은 0~1의 값을 가질 수 있다. 1은 viewport에 모두 보이는 상태이고 0은 아예 보이지 않는 상태이다. 0.2 0.5 0.8등의 값을 이용해 화면에 얼마나 보이냐에 따른 기능 설정을 할 수 있다.
  • nowPage != 0 : api요청이 완료되지 않아 화면에 데이터가 없으면 데이터를 불러오는동안 관찰요소가 계속해서 화면에 보이게 된다. 페이지 로딩 초기에 searchPage 상태값을 증가시키게 되고 의도치않은 형태의 페이지 로딩이 이루어질 수 있다. 처음 nowPage의 상태값은 0으로 두고 api요청 완료시에 nowPage값을 증가시키게 하여 api 요청이 완료 된 상태에서만 관찰요소를 관찰하여 기능을 실행할 수 있게하기위한 조건이다.
if (loader.current) observer.observe(loader.current);
return () => observer.disconnect();

 

  • loader라는 ref가 현재 DOM요소를 참조하고 있는지 확인한다. 렌더링이 완료되기전에는 해당 DOM요소가 존재하지 않으므로 조건문을 통해 정상적인 참조상태를 확인하는것이다. 참조하고 있을 경우 해당 요소에 대한 관찰을 시작하도록 한다. 여기서 loader.current는 관찰 대상을 의미한다.
  • 메모리 누수, 필요하지 않은 상황에서의 반복된 참조 등을 통한 성능저하와 버그를 막기 위해 클린업 함수가 필요하다.

 

useRef

관찰 대상에는 useRef를 사용하고 있다. IntersectionObserver를 사용할때 useRef를 사용해야 하는 이유는

  • DOM 요소에 접근 : viewport에 관찰 대상인 DOM 요소가 보이는지 확인을 하는것이 IntersectionObserver의 역할이다. 해당 요소를 모니터링하기 위해선 useRef를 사용해야 한다.

 

 

결론

IntersectionObserver는 이벤트 기반으로 작동하므로 일반적인 스크롤 이벤트 리스너 보다 효율적일 수 있다. 스크롤 이벤트 리스너의 경우 페이지가 스크롤 될때마다 모니터링을 하므로 성능저하를 가져올 수 있기 때문이다. 또한 여러 DOM 요소를 두고 조건에 따라 동시에 관찰할 수 있으므로 사용자경험 향상과 동적인 웹사이트를 구현하는데 많은 도움을 받을 수 있다.

하지만 viewport를 활용하는 만큼 사용자가 화면을 어떻게 다루냐에 따라 예상보다 많은 콜백함수의 실행이 이루어질 수 있고 예상치 못한 동작을 보여줄 수 도 있으므로 기능 구현시 조건에 대한 주의가 필요하다.