본문 바로가기
IT/React

[React-Query] react-query를 활용한 무한 스크롤 구현 에러 처리

by 무녈 2022. 8. 5.

TheMint 개발 에러

useObserver 무한 스크롤 에러

react-query의 useInfiniteQuery와 IntersectionOberser API를 사용하여 무한 스크롤을 구현하고자 하였다.

useInfiniteQuery를 사용하여 데이터 캐싱을 최적화하고, 컴포넌트를 변경 후에도 기존의 데이터에 대한 상태를 관리하여 클라이언트가 새로 데이터를 요청하지 않도록 하기 위함이다.

무한 스크롤을 구현하기 위해 IntersectionObserver API를 활용한 useObserver hooks을 만들었다.

import React, { useEffect } from 'react';

function useObserver({
  target,
  onIntersect,
  root = null,
  rootMargin = '0px',
  threshold = 1.0,
}) {
  useEffect(() => {
    let observer;

    if (target && target.current) {
      observer = new IntersectionObserver(onIntersect, {
        root,
        rootMargin,
        threshold,
      });
      observer.observe(target.current);
    }

    return () => observer && observer.disconnect();
  }, [target, rootMargin, threshold]);
}

export default useObserver;

그리고 무한 스크롤에 대한 데이터를 저장하기 위해 useInfiniteQuery를 활용하여 데이터를 저장하였다.

import React, { useEffect, useRef } from "react";
import { useInfiniteQuery } from "react-query";
import axios from "axios";
import useObserver from "./useObserver";

function Infinite({ Component, pageProps }) {
  const bottom = useRef(null);
  const getPokemonList = ({ pageParam = OFFSET }) =>
    axios
      .get(
        `https://pokeapi.co/api/v2/pokemon?limit=${OFFSET}&offset=${pageParam}`
      )
      .then((res) => {
        return res?.data;
      });

  const OFFSET = 100;
  const { data, error, fetchNextPage, isFetchingNextPage, status } =
    useInfiniteQuery("pokemonList", getPokemonList, {
      getNextPageParam: (lastPage) => {
        const { next } = lastPage;
        if (next) return Number(new URL(next).searchParams.get("offset"));
        return false;
      },
    });

  const onIntersect = ([entry]) => entry.isIntersecting && fetchNextPage();

  useObserver({
    target: bottom,
    onIntersect,
  });

  return (
    <div>
      <div style={{ height: "150vh", backgroundColor: "red" }}></div>
      {status === "loading" && <p>불러오는 중</p>}
      {status === "error" && <p>{error.message}</p>}
      {status === "success" &&
        data.pages.map((group, index) => (
          <div key={index}>
            {group.results.map((pokemon) => (
              <p key={pokemon.name}>{pokemon.name}</p>
            ))}
          </div>
        ))}
      <div ref={bottom} />
      {isFetchingNextPage && <p>계속 불러오는 중</p>}
    </div>
  );
}

export default React.memo(Infinite);

기존의 글을 참고하였을 때 useInfiniteQuery의 핵심은 getNextPageParam으로, 해당 메서드에서 falsy 한 값을 return 할 경우 더 이상 호출을 하지 않도록 한다고 하였다.

하지만 false를 return 하였음에도 불구하고 요청값인 pageParam이 초기화되어 새롭게 데이터를 요청하는 상황이 발생하였다.

문제

이때 intersectionObserver API를 활용한 코드에 문제가 있다고 추측하였다.

우선 페이지를 이동할때 useEffect의 return을 통해 observer에 대한 disconnet 메서드를 실행시켰을 뿐, 데이터가 더 이상 없다는 신호에 대한 disconnect가 존재하지 않는다는 것을 파악했다.

그렇기 때문에 parent component에서 값이 더이상 없을 때 disconnect를 시킬 수 있도록 만들어야 했다.

해결 방법

현재 pokemon API에서 받아오는 데이터 값은 아래의 형태이다.

data에서 next api를 기본적으로 제공해주며, 더 이상 값이 없을 경우

next에서 null을 전달한다. 이 프로퍼티를 통해서 다음 값이 없다는 것을 useObserver의 props에 전달한다.

 useObserver({
    target: bottom,
    onIntersect,
    next: data?.pageParams?.length
      ? Boolean(data?.pageParams[data?.pageParams?.length - 1])
      : true,
  });

처음에 값을 전달하기 위해 data?.pageParams[data?.pageParams?.length - 1]만 전달해서 null일 경우에 false로 처리하려고 했는데 문제는 해당 컴포넌트가 mount 되었을 때에는 data.pageParams가 없기 때문에 시작부터 undefined로 인해 disconnect가 발생하게 되었다.

그래서 3항 연산자를 사용하여 data의 pageParams가 존재하는 경우에는 동일하게 값을 전달하되 Boolean 함수를 사용하여 bool type으로 변환하여 넘겨주었고, mount 되었을 때는 disconnect 시키지 않기 위해 true로 변경하였다.

import React, { useEffect } from "react";

function useObserver({
  target,
  onIntersect,
  next,
  root = null,
  rootMargin = "0px",
  threshold = 1.0,
}) {
  useEffect(() => {
    let observer;

    if (target && target.current) {
      observer = new IntersectionObserver(onIntersect, {
        root,
        rootMargin,
        threshold,
      });
      observer.observe(target.current);
    }
    if (!next) observer && observer.disconnect();
    return () => observer && observer.disconnect();
  }, [next, target, rootMargin, threshold]);
}

export default useObserver;

하지만 next라는 변수로 해당 값을 전달하였지만, 또다시 무한 무한 스크롤이 발생하는 문제가 발생하였다. 이는 next의 값이 변경이 되었을 때 useEffect 내 함수를 update 시켜주지 않았기 때문이었다.

그렇기 때문에 useEffect의 2번째 인자의 리스에 next를 추가하여 next가 변경되었을 경우 해당 메서드를 update 시키는 방법을 통해 에러를 해결할 수 있었다.


참고

https://velog.io/@dev_kdh/42gg-%EB%AC%B4%ED%95%9C%EC%8A%A4%ED%81%AC%EB%A1%A4-%EA%B5%AC%ED%98%84

 

[42gg] 무한스크롤 구현

프로젝트에서 처음 주어진 과제는 랭킹 페이지 구현이었다. 랭크 리스트는 무한 스크롤이나 페이지네이션 등을 통해 나타낼 수 있다. 이번 포스트에서는 무한 스크롤 구현에 대해 설명한다. 무

velog.io

https://tanstack.com/query/v4/?from=reactQueryV3&original=https://react-query-v3.tanstack.com/ 

 

TanStack Query | React Query, Solid Query, Svelte Query, Vue Query

Powerful asynchronous state management, server-state utilities and data fetching for TS/JS, React, Solid, Svelte and Vue

tanstack.com

https://developer.mozilla.org/ko/docs/Web/API/Intersection_Observer_API

 

Intersection Observer API - Web API | MDN

Intersection Observer API는 타겟 요소와 상위 요소 또는 최상위 document 의 viewport 사이의 intersection 내의 변화를 비동기적으로 관찰하는 방법입니다.Intersection Observer API는 타겟 요소와 상위 요소 또는

developer.mozilla.org

 

반응형

댓글