티스토리 뷰

TIL

무한스크롤 쉽게 구현하기

윤미주 2024. 6. 25. 14:00

무한스크롤을 아주 쉽게 라이브러리 2개로 구현할 수 있다..!!

 

사용된 라이브러리
1. useInfiniteQuery
//설치명령어
yarn add @tanstack/react-query​


2. react-intersection-observer
//설치명령어
yarn add @react-intersection-observer/hooks​

 

react-intersection-observer를 사용한 이유

 

useInfiniteQuery로만 작성해보려고 하였으나

초보자 이지만 코드 분리하는거 좋아하는 편..

axios코드, 훅, 전부 분리해 사용하고 싶은 오기 발동.

export const useReadReHomeHomeList = (params: ScrollParamsType) => {
  return useInfiniteQuery<ReHomeGetListType, Error>({
    queryKey: ["reHomeHomeList", params],
    queryFn: ({ pageParam = 1 }) =>
      getReHomeHomeList({ ...params, page: pageParam as number }),
    getNextPageParam: (lastPage, allPages) => {
      if (lastPage.data.result.length === params.limit) {
        return allPages.length + 1;
      }
      return undefined;
    },
    initialPageParam: 1,
  });
};

위에 보이는 것 처럼 queryhook과 useInfiniteQuery를 한코드에 사용할 때, 초기에 useRef가 null로만 잡혔다. 

 

 

useRef 초기 렌더링 시 값을 읽기 위해 시도한 방법

 

1. useEffect로 감싸 처리하기. 

  const ref = useRef<HTMLDivElement | null>(null)
  const pageRef = useIntersectionObserver(ref, {})
  const isPageEnd = !!pageRef?.isIntersecting

  useEffect(() => {
    let timerId: NodeJS.Timeout | undefined

    if (ref && isPageEnd && hasNextPage) {
      timerId = setTimeout(() => {
        fetchNextPage()
      }, 500)
    }
  }, [fetchNextPage, hasNextPage, isPageEnd, ref])

 

2. null 또는 undifinded 일 경우 반환

import { RefObject, useState, useEffect } from 'react'

function useIntersectionObserver(
  elementRef: RefObject<Element>,
  { threshold = 0.1, root = null, rootMargin = '0%', enableObserver = true },
) {
  const [entry, setEntry] = useState<IntersectionObserverEntry>()

  const updateEntry = ([entry]: IntersectionObserverEntry[]): void => {
    setEntry(entry)
  }

  useEffect(() => {
    console.log('elementRef.current 감지했음', elementRef?.current)
    const node = elementRef?.current
    const hasIOSupport = !!window.IntersectionObserver

    if (!node || !hasIOSupport) return

    const observerParams = { threshold, root, rootMargin }
    const observer = new IntersectionObserver(updateEntry, observerParams)

    observer.observe(node)

    return () => observer.disconnect()
  }, [
    elementRef?.current,
    root,
    rootMargin,
    JSON.stringify(threshold),
    enableObserver,
  ])

  return entry
}

export default useIntersectionObserver

 

위와 같은 방법으로 시도해 보았으나,

☠️ ref 값이 여전히 null이거나 만들어둔 useIntersectionObserver훅에서 ` useRef `가 변경됨을 감지하지 못했다.

☠️ useState로 상태를 관리하여 초기 렌더링 시 useRef를 읽게 만들었을 경우에는 useState로 관리를 하다보니 무한스크롤 트리거에 닿은 경우 무조건 새로고침이 일어났다.. 🫠🫠🫠🫠🫠

 

그래서 queryhook과 useInfiniteQuery를 결국 따로 작성해야하나.. 오기부리다가 몇일을 날리는건가..

고민하다가 만난 react-intersection-observer💛🩷

 

❓ useView

`useView` 훅이란 react-intersection-observer 라이브러리의 일부로 웹페이지에서 무한스크롤 트리거가 사용자의 뷰포트에 들어오는 순간을 감지하는 ` Intersection Observer API `를 React 컴포넌트에서 쉽게 사용할 수 있도록 만든 래퍼이다.

 

내가 구현한 useIntersectionObserver는 useEffect 훅이 ref의 변화를 제대로 감지하지 못했다.

useEffect의 의존성 배열이 ref를 제대로 감지하지 못하는 부분은 공식문서에서 보았다. 하여 elementRef.current를 넣었으나 어떤 방법을 써도 감지하지 못했.. 

 

하지만 useView 훅은 위와 같은 문제를 해결하기 위해 요소의 참조가 실제로 변경될때 만 observer를 다시 연결하는 등의 내부 로직을 가지고 있다고 한다.

또 요소가 뷰포트에 들어오거나 나갈때마다 상태를 업데이트 해 react 컴포넌트가 적절한 렌더링을 할 수 있도록 해준다고 한다.!!.. 알랍쏘마치

 

하여 전체코드는 아주 깔꼼

차근차근 설명할 것도 없다.. 하핫 그래도 굳이 굳이 설명해 보겠다.

 

  const { ref, inView } = useInView();

사랑스러운 useInView 훅에 ref 와 inView를 객체구조분해할당 하여 가져와준다.

 

ref

ref는 React의 ` useRef `와 유사한 역할을 하며 DOM 요소에 대한 참조를 제공하는 것 인데,

쉽게 말해 ref는 관찰을 하고자 하는 요소를 IntersectionObserver에 연결하는데 필요한 링크 역할을 한다고 생각하면 쉽다.

해서 트리거 하고자 하는 요소에 ref를 부착해두면 트리거 요소가 화면에 나타날 때 추가 데이터를 불러오는 역할을 할 수있게 된다.

 

inView

inView는 해당 요소가 사용자의 뷰포트에 들어왔는지 여부를 boolean 값으로 준다.

해서 조건부 렌더링을 할때 ref의 트리거 요소가 화면에 보일 때만 특정 장업을 수행하게 하거나 렌더링 할 수 있다.

 

  const {
    data: homeLists,
    isFetching,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
    isError,
    isLoading,
  } = useReadReHomeHomeList({ limit: 12, page: 1 });

나의 경우 api 요청 시 limit=${limit}&page=${page}의 경로로 값을 보내야 하기 때문에 인자 그대로 받길 원해 위와 같이 작성해 사용해 주었다. 

 

  useEffect(() => {
    if (inView && hasNextPage) {
      fetchNextPage();
    }
  }, [inView, hasNextPage, fetchNextPage]);

 

useEffect를 이용해 inView(트리거 요소 treu) 와 hasNextPage(다음 페이지가 있을때) 가 있을때

fetchNextPage를 호출!! 그럼 useRedReHomeHomeList에 인자로 전달하는 page에 다음 번 페이지를 요청해준다. 

예를들어 현재 page가 1 일때 inView가 true이고 hasNextPage가 있으면 page에 2를 전달해 api 요청을 함.

 

      {(isFetchingNextPage || isFetching || hasNextPage) && (
        <Loader className="my-10" />
      )}

데이터를 아직 받지 못한 경우에는 만들어둔 Loader를 보여준다. 

 

<div className="w-full touch-none h-15" ref={ref} />

div 요소에 ref를 주어 트리거를 만들어 두었다. 

즉 화면 가장 아래로 내려갔을 때 트리거가 발동 된다!

 

 

queryhook 궁금해 하실 분들을 위해 참고해 주세요..!! 

export const useReadReHomeHomeList = (params: ScrollParamsType) => {
  return useInfiniteQuery<ReHomeGetListType, Error>({
    queryKey: ["reHomeHomeList", params],
    queryFn: ({ pageParam = 1 }) =>
      getReHomeHomeList({ ...params, page: pageParam as number }),
    getNextPageParam: (lastPage, allPages) => {
      if (lastPage.data.result.length === params.limit) {
        return allPages.length + 1;
      }
      return undefined;
    },
    initialPageParam: 1,
  });
};

 

 

무한스크롤 사용한 전체코드 

import React, { useEffect } from "react";
import HomeItem from "./_component/HomeItem";
import { useReadReHomeHomeList } from "@/hooks/list/useReHomeList";
import Loader from "@/components/common/Loader";
import { useInView } from "react-intersection-observer";

export default function Home() {
  const { ref, inView } = useInView();

  const {
    data: homeLists,
    isFetching,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
    isError,
    isLoading,
  } = useReadReHomeHomeList({ limit: 12, page: 1 });

  useEffect(() => {
    if (inView && hasNextPage) {
      fetchNextPage();
    }
  }, [inView, hasNextPage, fetchNextPage]);

  if (isLoading) {
    return <Loader />;
  }

  if (isError) {
    return <div>Error fetching homeLists.</div>;
  }

  return (
    <div>
      <ul className="grid gap-5 grid-cols-2 sm:grid-cols-3 xl:grid-cols-4 mb-10">
        {homeLists ? (
          homeLists.pages.map((page, pageIndex) => (
            <React.Fragment key={pageIndex}>
              {page?.data?.result.map((item) => (
                <HomeItem key={`${item.id}-${pageIndex}`} item={item} />
              ))}
            </React.Fragment>
          ))
        ) : (
          <Loader />
        )}
      </ul>
      {(isFetchingNextPage || isFetching || hasNextPage) && (
        <Loader className="my-10" />
      )}
      <div className="w-full touch-none h-15 bg-red-500" ref={ref} />
    </div>
  );
}

 

 

'TIL' 카테고리의 다른 글

토큰 만료처리하기  (0) 2024.07.06
react-hook-form 나홀로 정리하기  (0) 2024.07.02
Zustand 이용해 컨펌창 만들기  (2) 2024.06.24
Axios에서 url 요청보낼 때 주의할 점  (0) 2024.06.07
Next.js Image tag 사용하기  (0) 2024.06.07