[ React ] 무한스크롤 적용하기 - useInfiniteQuery & InterSectionObserver

CJY00N·2023년 7월 17일
0

react

목록 보기
8/10
post-thumbnail

기존에는 product 갯수가 20개였지만, 60개로 늘려서 product.json 파일을 수정한다.

60개의 상품을 4개의 페이지로 나눈다고 생각하면 id 1~15번인 15개의 상품을 먼저 보여준 후 커서가 끝까지 갔을 때 id 16~30번인 15개의 상품을 보여주고 ... 하는 식으로 무한 스크롤로 페이지를 나눌 수 있다.

⚡️ 무한스크롤 적용하기

커서 Pagination

  • 서버에서 cursor를 argument로 받아주어야 한다.
  • 커서를 아이디 값으로 하면 그 값에 따라 상품이 변경되어 보이게 할 수 있다.

▼ server/src/schema/products.ts

  extend type Query {
    products(cursor: ID): [Product!]
    product(id: ID!): Product!
  }
  • 페이지 번호를 인덱스라고 생각했을 때 해당 인덱스부터 15개씩 상품을 보여주도록 한다.

▼ server/src/resolvers/products.ts

  products: (parent, { cursor = "" }, { db }) => {
    const fromIndex =
      db.products.findIndex((product) => product.id === cursor) + 1;
    return db.products.slice(fromIndex, fromIndex + 15) || [];
  },

서버의 설정은 끝났다.

⚡️ useInfiniteQuery

https://tanstack.com/query/v4/docs/react/reference/useInfiniteQuery
- useInfiniteQuery는 일반적인 useQuery와 매우 유사하지만, 페이지네이션, 무한 스크롤, 커서 기반 페이지네이션 등과 같은 사용 사례를 지원하는 데 특화되어 있다.

useInfiniteQuery 사용하기

▼ client/src/pages/products.tsx

  const { data, isSuccess, fetchNextPage, hasNextPage, isFetchingNextPage } =
    useInfiniteQuery<Products>(
      QueryKeys.PRODUCTS,
      ({ pageParam = "" }) =>
        graphqlFetcher<Products>(GET_PRODUCTS, { cursor: pageParam }),
      {
        getNextPageParam: (lastpage, allpages) => {
          return lastpage.products.at(-1)?.id;
        },
      }
    );
  • at(-1)을 하면 가장 마지막 요소를 반환한다.
  • lastpage와 allpages, data를 콘솔에 출력해보면 lastpage.products에 원하는 데이터들이 존재하고, allpages.0.products 안에 데이터들이 존재하고, data.pages.0.products에 원하는 데이터들이 존재하는 것을 확인할 수 있다.

ProductsList 불러오기 수정

상품목록 화면에서 ProductItem 컴포넌트를 호출하면서 data.pages를 props로 넘겨줄 것이므로, productList에서 props로 list를 전달받으면, list에서 이중 map을 돌려야 원하는 data에 접근할 수 있다.
▼ client/src/components/list.tsx

const ProductList = ({ list }: { list: { products: Product[] }[] }) => {
  return (
    <ul className="products">
      {list.map((page) =>
        page.products.map((product) => (
          <ProductItem {...product} key={product.id} />
        ))
      )}
    </ul>
  );
};

⚡️ InterSectionObserver

사용자가 화면에 끝 지점에 도달했는지 확인하는 방법 두가지

  • 전통적인 방법
    : scrollTop = window.height 등을 이용해서 정말 도달했는지 계속 감지하는 방법
    - eventHandler로 (scroll) 감시를 계속해주어야 하며, throttle, debounce처리까지 필요할 수 있다.
    → 스레드 메모리를 사용하게 되며, 성능에도 좋지 않다.

  • interSectionObserver 이용하는 방법
    : 이벤트 등록 x, 브라우저에서 제공하는 별개의 감지자
    - 싱글스레드인 자바스크립트와 별개로 동작하므로 성능 문제 발생 x

InterSecrionObserver

:Intersection Observer는 웹 API로, 뷰포트와 요소 간의 교차 영역을 감지하는 기능을 제공한다. 이를 통해 요소가 뷰포트에 들어오거나 나갈 때를 감지하고, 이벤트를 처리할 수 있다.

InterSectionObserver in React

  • 보통 ref를 사용한다.
    const observerRef = useRef<IntersectionObserver>();

  • entries는 ntersectionObserverEntry 인스턴스의 배열인데, 어떤 값이 들어가는 지 확인하기 위해 console에 출력해볼 것이다.

▼ client/src/pages/products.tsx

  const getObserver = useCallback(() => {
    if (!observerRef.current) {
      observerRef.current = new IntersectionObserver((entries) => {
        console.log("entries", entries);
      });
    }
    return observerRef.current;
  }, [observerRef.current]);
  • 관찰할 div요소를 지정하기 위해서 useRef인 fetchMoreRef를 새로 정의하고 상품목록 리스트 아래에 div를 새로 생성하여 ref로 지정한다.
const fetchMoreRef = useRef<HTMLDivElement>(null);

  • 그리고 나서 useEffect로 fetchMoreRef.current가 바뀔 때마다 observe를 한다.
  useEffect(() => {
    if (fetchMoreRef.current) {
      getObserver().observe(fetchMoreRef.current);
    }
  }, [fetchMoreRef.current]);
  • 해당 div가 사용자의 화면에 안보일 때에는 entries의 isIntersecting 값이 false였다가, div가 화면에 보이게 되면 isIntersecting true로 바뀌는 것을 확인할 수 있다.

  • intersecting이라는 state를 생성하고, isIntersecting의 값에 따라서 setIntersecting을 적용한다.
  const [intersecting, setIntersecting] = useState(false);
        setIntersecting(entries[0]?.isIntersecting);
  • 위에서 정의한 intersecting에 따라 무한 스크롤을 적용하기 위해 useEffect의 deps에 intersecting에를 넣고 아래와 같이 정의한다.
  • intersecting이 false이거나 (다음div가 보이지 않음),
    isSuccess가 false이거나 (데이터가 로드되지 않음),
    hasNextPage가 false이거나 (더 이상 로드할 다음 페이지가 없음), isFetchingNextPage가 true이면 (다음 페이지를 가져오는 요청이 아직 진행중)
    다음페이지를 로드하지 않는다.
  • 그렇지 않은 경우에는 다음페이지를 로드한다. (fetchNextPage)
  useEffect(() => {
    if (!intersecting || !isSuccess || !hasNextPage || isFetchingNextPage)
      return;
    fetchNextPage();
  }, [intersecting]);

스크롤 할 때마다 데이터가 추가되어 보여지고 있는 것을 확인할 수 있다.

observer를 쓰는 부분의 코드가 길고 재사용할 가능성이 있으므로 따로 hooks로 저장해둘 것이다.
▼ client/src/hooks/useIntersection.ts

const useInfiniteScroll = (targetRef: RefObject<HTMLElement>) => {
  const observerRef = useRef<IntersectionObserver>();
  const [intersecting, setIntersecting] = useState(true);

  const getObserver = useCallback(() => {
    if (!observerRef.current) {
      observerRef.current = new IntersectionObserver((entries) => {
        setIntersecting(entries.some((entry) => entry.isIntersecting));
      });
    }
    return observerRef.current;
  }, [observerRef.current]);

  useEffect(() => {
    if (targetRef.current) {
      getObserver().observe(targetRef.current);
    }
  }, [targetRef.current]);

  return intersecting;
};
profile
COMPUTER SCIENCE ENGINEERING / Web Front End

1개의 댓글

comment-user-thumbnail
2023년 7월 18일

잘 봤습니다. 좋은 글 감사합니다.

답글 달기