[React] React query를 이용해 Infinite scroll을 구현해보자

fgStudy·2022년 8월 15일
6

프론트엔드 공부

목록 보기
5/6
post-thumbnail

이번 포스팅 때는 React query를 이용해 Infinite scroll, 즉 무한스크롤을 구현하는 방법에 대해 설명하고자 한다. 무한스크롤을 구현하기 위해서는 React query를 이용할지라도, IntersectionObserver api를 이용해 현재 스크롤이 마지막 요소까지 도달했는지를 관찰하는 작업이 필요하다.

따라서 본 포스팅은 React query의 useInfiniteQuery api와 IntersectionObserver 로직을 이용해 무한스크롤을 구현하는 방법에 대해 서술하는 데 목적이 있다.


➰ How. (구현)

무한스크롤을 구현하기 위해서는 아래 차례를 따라가면 된다.

  1. React query의 useInfiniteQuery api를 이용해 무한스크롤 api를 구현한다.
  2. 무한스크롤 api로 fetch한 list를 렌더링한다.
  3. 렌더링한 list 아래 로딩바를 렌더링한다.
  4. IntersectionObserver 로직을 구현한다.
  5. 브라우저가 로딩바를 isIntersecting될 때 다음 page data를 fetch할 수 있게끔 한다.
  6. 콜백 ref를 이용해 IntersectionObserver의 observer target을 로딩바로 설정한다.

각 차례를 코드를 통해 차근차근 설명하도록 하겠다.


1. 무한스크롤 api 구현

const infiniteServiceWorker = axios.create({
  baseURL: "https://jsonplaceholder.typicode.com"
});

export function useDummyJsonQuery() {
  return useInfiniteQuery(
    ["todos"],
    async ({ pageParam = 1 }) => {
      const { data } = await infiniteServiceWorker.get(
        `/todos?_start=${pageParam}&_limit=10`
      );

      if (data.length < 10) return { result: data, nextPage: undefined };

      return {
        result: data,
        nextPage: pageParam + 1
      };
    },
    {
      getNextPageParam: (lastPage, pages) => lastPage.nextPage ?? undefined
    }
  );
}
  • 데이터를 10개씩 받아오기 위해 limit를 10로 설정
  • 현재 pageParam의 data가 10개 이하일 시, 더 이상 fetch할 data가 없으므로 nextPage를 undefined로 설정
    • nextPage가 숫자일 시 그 다음 page가 있으므로 hasNextPage가 true
    • nextPage가 undefined일 시 그 다음 page가 없으므로 hasNextPage가 false
  • hasNextPage의 value에 따라 로딩바 렌더링 여부 결정

2. 무한스크롤 api로 fetch한 list 및 로딩바 렌더링

const {
  data: todoPayload,
  isFetchingNextPage,
  fetchNextPage,
  hasNextPage
} = useDummyJsonQuery();
const todoList = changeInfiniteScrollDataToArray(todoPayload);

...

return (
  <div>
  	<div
      style={{
        display: "grid",
        gap: "10px"
      }}
    >
      {todoList.map((todo, index) => (
        <Card key={uid(index)} todo={todo} />
      ))}
    </div>
    {hasNextPage && <div ref={(elem) => setTarget(elem)}>로딩중...</div>}
  </div>
);
  • 앞서 구현한 무한 스크롤 api를 이용해 fetch한 list를 렌더링
  • next page를 fetch하기 위한 로딩바 구현
    • hasNextPage가 undefined가 아닐 시(= 아직 fetch하지 않은 data가 존재) 렌더링
    • ref 어트리뷰트에 대한 설명은 5번 문항에서 설명할 예정

3. IntersectionObserver 로직을 useIntersectionObserver 훅으로 분리

interface UseIntersectionObserverProps extends IntersectionObserverInit {
  onIntersect: IntersectionObserverCallback;
  options?: {
    root?: Document;
    rootMargin?: string;
    threshold?: number;
  };
}

const useIntersectionObserver = ({
  onIntersect,
  options = { root: null, rootMargin: "0px", threshold: 0 }
}: UseIntersectionObserverProps) => {
  const [target, setTarget] = React.useState<HTMLElement>(null);
  React.useEffect(() => {
    if (!target) return;

    const observer = new IntersectionObserver(onIntersect, options);
    observer.observe(target);
    return () => target && observer.disconnect();
  }, [target]);

  return { setTarget };
};

IntersectionObserver 로직은 무한스크롤 뿐만 아니라 Lazy loading 등 여러 곳에서 사용되므로 재사용성을 위해 custom hook으로 분리하였다.

  • hook은 아래 2개의 인자를 받음
    • onIntersect = loaderRef가 가시되었을 때 호출될 callback 함수
    • options = 각종 옵션 (root, rootMargin, threshold)
  • 로딩바에서 콜백 ref를 이용해 로딩바 dom으로 target을 설정
  • useEffect hook을 이용해 클린업 시 observer disconnect

4. next page data를 fetch하는 callback함수 정의 후 useIntersectionObserver 훅의 인자로 전달

const onIntersect = React.useCallback(
  (entries: IntersectionObserverEntry[]) => {
    const [target] = entries;
    if (target.isIntersecting && hasNextPage) {
      fetchNextPage();
    }
  },
  [hasNextPage, fetchNextPage, isFetchingNextPage]
);

const { setTarget } = useIntersectionObserver({
  onIntersect,
  options: {
    rootMargin: "10%",
    threshold: 0.25
  }
});
  • next page data를 fetch하는 callback함수 정의
  • callback함수를 Intersection Observer의 인자로 전달하기 위해 useIntersectionObserver 훅의 인자로 전달
  • setTarget은 콜백 ref로 로딩바 dom을 target으로 설정하는 setter 함수

5. 콜백 ref를 이용해 IntersectionObserver의 observer target을 로딩바로 설정

const { setTarget } = useIntersectionObserver({
  onIntersect,
  options: {
    rootMargin: "10%",
    threshold: 0.25
  }
});

return (
  <div>
    <div
      style={{
        display: "grid",
        gap: "10px"
      }}
    >
      {todoList.map((todo, index) => (
        <Card key={uid(index)} todo={todo} />
      ))}
    </div>
    {hasNextPage && <div ref={(elem) => setTarget(elem)}>로딩중...</div>}
  </div>
);
  • 콜백 ref로 setTarget을 전달함으로써 해당 로딩바가 가시되었을 때 hook의 callback함수가 실행되어 next page를 fetch

👀 Demo

전체 코드는 다음과 같으며 위의 설명을 참고하면서 이해해주길 바란다.


🌻 맺으며

IntersectionObserver 로직을 hook으로 분리하면서 콜백 ref를 처음 알게되었다. 아직 리액트를 잘 모른다는 생각이 들었다.

Infinite scoll을 구현하시는 분들에게 이 글이 조금이라도 도움이 되길 바란다.


(ref)

profile
지식은 누가 origin인지 중요하지 않다.

0개의 댓글