[React] 무한스크롤 구현

slppills·2024년 9월 2일

TIL

목록 보기
49/69

뉴스피드 팀프로젝트를 진행하던 도중 무한스크롤을 구현하게 되었다. 내가 생각한 계획은 처음에 화면에 한줄에 4개씩 총 2줄로 8개의 게시물을 띄우고 화면 스크롤을 맨 아래로 내릴 때마다 그 다음 8개 게시물을 불러오도록 구현하려고 했다. 하지만 막상 이렇게 구현하고 보니 문제가 있었다.

문제상황

뷰포트 높이에 상관없이 처음에 불러오는 게시물 개수를 8개로 지정해놓아서 뷰포트 높이가 일정 크기보다 크다면 scrollHeight와 clientHeight가 같아져버릴 수 있다. 나는 스크롤 이벤트로 setState를 해서 데이터를 불러오는 코드를 썼기 때문에 scrollHeight와 clientHeight가 같아져 스크롤이 비활성화 되는 것을 막아야 했다.

로직 수정

그래서 맨 처음 게시물을 불러올 때 뷰포트 높이 이상만큼 게시물을 불러오고 스크롤을 내리면 이전과 같이 게시물 8개씩 불러오기로 수정하였다.

parseInt((document.documentElement.scrollHeight - 450) / 385)

scrollHeight에서 header와 footer의 height 값을 뺀 값을 게시물 컴포넌트의 height 값으로 나눈걸 변수로 저장했다. 그리고 불러올 게시물 개수를 지정하는 state를

const [postLimit, setPostLimit] = useState(countPost * 4 + 4);

이렇게 지정해서 한줄에 4개씩이니까 앞에서 지정한 변수에 *4를 한 값에 한줄 더 추가하는 값인 4를 더해서 무조건 scrollHeight이 clientHeight보다 크게 해 스크롤이 가능하게끔 만들었다.

전체 코드

const countPost = parseInt((document.documentElement.scrollHeight - 450) / 385);
  const [postList, setPostList] = useState([]);
  const [postLimit, setPostLimit] = useState(countPost * 4 + 4);
  const [loadingVisibility, setLoadingVisibility] = useState(false);
  const [allPostLength, setAllPostLength] = useState(0);

  useEffect(() => {
    keyword && setPostLimit(countPost * 4 + 4);
    const fetchData = async (limit) => {
      console.log("postLimit", postLimit);
      console.log("allPostLength", allPostLength);
      keyword || postLimit - 8 > allPostLength ? setLoadingVisibility(false) : setLoadingVisibility(true);
      try {
        let response;
        if (keyword) {
          console.log(keyword);
          response = await supabase.from("recipe_info").select("*").order("created_at", { ascending: true });

          const filteredData = response.data.filter((post) =>
            post.RECIPE_TITLE.replace(/\s/gi, "").includes(keyword.replace(/\s/gi, ""))
          );
          setPostList(filteredData);
        } else {
          response = await supabase
            .from("recipe_info")
            .select("*")
            .order("created_at", { ascending: true })
            .limit(limit);
          setPostList(response.data);
        }
        setAllPostLength(response.data.length);
      } catch (error) {
        console.log(error);
      } finally {
        setLoadingVisibility(false);
      }
    };

    const handleScroll = throttle(() => {
      const scrollHeight = document.documentElement.scrollHeight;
      const scrollTop = document.documentElement.scrollTop;
      const clientHeight = document.documentElement.clientHeight;
      if (scrollTop + clientHeight >= scrollHeight - 1) {
        setPostLimit((prev) => prev + 8);
      }
    }, 300);

    fetchData(postLimit);

    window.addEventListener("scroll", handleScroll);

    return () => window.removeEventListener("scroll", handleScroll);
  }, [postLimit, keyword, allPostLength]);

하지만 이렇게 scroll이벤트를 addEventListener로 주면 단시간에 수백번 호출되기도 하고 여러가지로 성능 문제가 발생한다. 이런 비효율적이고 근본없는 로직보다 훨씬 간단하게 구현하는 방법이 있었다.
Intersection Observer API 를 사용하면 무한스크롤을 쉽게 구현할 수 있었다. intersection observer api는 루트 요소와 타겟 요소의 교차점을 관찰한다. 그리고 타겟 요소가 루트 요소와 교차하는지 아닌지를 구별하는 기능을 제공한다. scroll 이벤트와 다르게 교차 시 비동기적으로 실행되며 가시성 구분 시 reflow (reflow는 생성된 DOM 노드의 레이아웃 수치(위치, 높이, 너비 등)가 변경되었을 때, 발생하는 재배치) 를 발생시키지 않는다.

기본 사용법

Intersection Observer는 new IntersectionObserver() 생성자를 통해 인스턴스 (io)를 만든다. 그리고 해당 인스턴스로 관찰자 (Observser)를 초기화하고 관찰할 대상을 지정한다.
이때, new IntersectionObserver() 생성자는 2개의 인수 (callback, options)를 갖는다.

나의 코드

const lastPostElementRef = useCallback(
    (node) => {
      if (loadingVisibility) return;
      if (observerRef.current) observerRef.current.disconnect();
      observerRef.current = new IntersectionObserver(
        (entries) => {
          if (entries[0].isIntersecting) {
            setPage((prevPage) => prevPage + 1);
          }
        },
        { threshold: 1.0 }
      );
      if (node) observerRef.current.observe(node);
    },
    [loadingVisibility]
  );

여기서 threshold는 new IntersectionObserver() 생성자의 인수의option 중 하나이다. 콜백이 실행될 타겟 요소의 가시성 퍼센티지를 나타내는 단일 숫자 및 숫자 배열이 들어갈 수 있다. 즉, 요소의 top, bottom 이 노출된 순간만 콜백을 실행할 수 있는 것이 아니라 어느정도 타겟 요소가 보여졌는 지에 따라서도 콜백을 호출할 수 있다.

1개의 댓글

comment-user-thumbnail
2024년 9월 3일

혹시 이렇게 한 이유가 있나요?
IntersectionObserver 로 마지막 아이템의 보여지는 정도로 다음페이지를 불러오는 방식을 할 수도 있는데
이건 페이지 불러오는걸 스크롤 위치의 의존해서 유지보수에 문제가 있지 않을까요?

답글 달기