IntersectionObserver와 useRef를 활용한 무한 스크롤 구현하기

SteadySlower·2024년 12월 23일
0

React JS

목록 보기
12/13
post-thumbnail

예전에 SwiftUI를 사용해서 무한스크롤을 구현하는 법을 작성한 포스팅 작성했었다. (벌써 2년반이나 전이다.) 이번에는 React에서 무한스크롤을 구현하는 법을 배워보자.

저 포스팅에서 사용했던 방법 중에 2번 방법 (스크롤 중에 맨 아래에 ProgressView가 나타나면 데이터를 불러오는 방식을 사용할 것이다.)


useRef란?

useRef는 React에서 DOM 요소나 값을 저장할 수 있는 훅이다. 리렌더링 없이 값을 유지할 수 있다는 점이 특징이다. (useState는 값을 유지할 수 있지만 리랜더링을 유발한다.) 주로 아래와 같은 상황에서 사용된다:

  1. DOM 요소에 직접 접근해야 할 때
  2. 리렌더링 없이 값을 기억하고 싶을 때
import { useRef } from "react";

function Example() {
  const inputRef = useRef<HTMLInputElement>(null);

  const focusInput = () => {
    if (inputRef.current) {
      inputRef.current.focus(); // input 요소에 포커스를 설정
    }
  };

  return (
    <div>
      <input ref={inputRef} type="text" />
      <button onClick={focusInput}>Focus Input</button>
    </div>
  );
}

IntersectionObserver란?

IntersectionObserver는 DOM 요소가 뷰포트(Viewport) 안에서 얼마나 보이는지(교차 상태)를 관찰하는 브라우저 API이다. 이를 통해 사용자는 특정 요소가 화면에 나타났는지 여부를 알 수 있다. (SwiftUI의 onAppear라고 보면 된다.)

처음에 객체를 만들 때 전달하는 콜백은 IntersectionObserverEntry[]와 IntersectionObserver 타입의 두개의 param을 받는데 이는 각각 관찰하는 대상과 관찰하는 옵저버를 의미한다.

IntersectionObserver는 무한 스크롤, 레이지 로딩 이미지, 애니메이션 트리거 등 다양한 곳에서 활용된다.

무한 스크롤 구현하기

아래 코드는 React에서 IntersectionObserveruseRef를 활용해 무한 스크롤을 구현한 예시이다. useRef를 통해 div의 참조를 얻어 IntersectionObserver 의 옵저빙 대상으로 등록하여 화면에 나타나면 추가적인 데이터를 불러오는 함수를 실행하는 방식이다.

아래 사항에 주의해서 코드를 살펴보자!

  1. 마지막 페이지인 경우를 체크해서 더 이상 데이터가 없다면 로드하지 않도록 하자! (trigger 역할을 하는 div를 제거)
  2. 콜백 안에서 unobserve를 실행하고 데이터를 불러오자! (콜백이 추가적으로 실행되어 한번에 데이터를 여러번 불러오는 것을 방지)
  3. 클리너 함수 (메모리 누수 방지)를 리턴하자.
  4. page 값을 디팬던시로 하자!
    1. 추가 데이터를 다 불러오면 옵저버를 다시 등록해야 함
    2. 마지막 페이지라면 page 값이 증가하지 않으므로 자동적으로 옵저빙이 중단됨!
"use client";

import { useEffect, useRef, useState } from "react";
import { getMoreWords } from "./dbClient.ts"; // 데이터를 가져오는 함수

interface WordListProps {
  initialWords: string[];
}

export default function WordList({ initialWords }: WordListProps) {
  const [words, setWords] = useState(initialWords);
  const [isLoading, setIsLoading] = useState(false);
  const [page, setPage] = useState(0);
  
  // 마지막 페이지라면 trigger를 숨기기 위해 필요!
  const [isLastPage, setIsLastPage] = useState(false);

  const trigger = useRef<HTMLSpanElement>(null);

  useEffect(() => {
    const observer = new IntersectionObserver(
      async (entries, observer) => {
        const element = entries[0]; //👉 옵저빙 대상 (= trigger)
        if (element.isIntersecting && trigger.current) {
          observer.unobserve(trigger.current); //❗️ 중복 실행 안되게 unobserve하기

					// 로딩을 세팅하고 추가적인 단어 불러오기
          setIsLoading(true);
          const newWords = await getMoreWords(page + 1);

					// 마지막 페이지면 마지막 페이지를 체크
          if (newWords.length !== 0) {
            setPage((prev) => prev + 1);
            setWords((prev) => [...prev, ...newWords]);
          } else {
            setIsLastPage(true);
          }

          setIsLoading(false);
        }
      },
      { threshold: 1.0 } //👉 100% 보였을 때 콜백을 실행한다. 0.5라면 50% 이상 보이면 실행
    );

		// trigger의 참조를 observer에 등록한다.
    if (trigger.current) {
      observer.observe(trigger.current);
    }

		// 클리너 함수! (이 페이지를 벗어나면 옵저버 해제!)
    return () => observer.disconnect();
  }, [page]); //❗️ dependency를 page로! 페이지가 +1 되면 다시 옵저버 등록한다!

  return (
    <div className="p-5 flex flex-col gap-5">
      {words.map((word, index) => (
        <div key={index} className="p-3 border rounded">
          {word}
        </div>
      ))}
      {!isLastPage && <div ref={trigger} className="loader">Loading...</div>}
    </div>
  );
}

왜 IntersectionObserver를 사용할까?

기존의 무한 스크롤 구현 방식은 스크롤 이벤트를 감지해 계산을 반복적으로 수행하는 방식이었다. 그러나 이는 성능 문제가 발생할 수 있다. (스크롤 할 때 마다 콜백을 실행해야 하기 때문) 반면, IntersectionObserver는 브라우저 레벨에서 최적화된 감지 기능을 제공하므로 더 효율적이고 정확하다.

profile
백과사전 보다 항해일지(혹은 표류일지)를 지향합니다.

0개의 댓글