230911 useInfiniteQuery로 무한스크롤 구현하기

나윤빈·2023년 9월 11일
0

TIL

목록 보기
52/55

1. useInfiniteQuery란?

📌 useInfiniteQuery란? Data Fetching이 일어날 때 마다 기존 리스트 데이터에 Fetched Data 를 추가하고자 할 때 유용하게 사용할 수 있는 React-query hook이다. 더보기 UI 또는 무한스크롤 UI 에 사용하기에 적합하다.

2. useInfiniteQuery의 실행순서

1. queryFn 실행: queryFn의 매개변수로 QueryFunctionContext가 전달된다. 이때 QueryFunctionContext에는 queryKey, pageParam, meta, signal을 가지고 있는 객체이다. 이 중에 pageParam을 queryFn에 넣어줘야 한다. 보통은 pageParam은 1로 지정해준다.

2. 캐시 데이터 등록: 1 page에 대한 데이터를 불러오면 이 response를 캐시 Context pages 배열의 첫번째 요소로 추가를 해준다. pageParams 배열의 첫번째 요소는 hook mermory에서 뽑아서 준다. hook mermory의 pageParam가 undefined였기 때문에 첫번째 요소는 null로 넣어준다.

3. getNextPageParam 실행: 다음 페이지의 유무를 미리 파악하는 작업을 한다. lastPage(: 캐시 Context의 최근 캐시 데이터)라는 매개변수를 받아 다음 페이지가 있는지 여부를 알 수 있는 프로퍼티가 있는 경우 이를 이용하여 nextPage를 계산할 수 있다. nextPage가 있는 경우 이 값을 hook memory 안에 pageParam 값으로 재할당한다.

4. hasNextPage 상태변경: nextPage가 undefined가 아닌 다른 값으로 리턴이 되면 hasNextPage의 상태가 true가 된다.

5. fetchNextPage 호출: 미리 할당해준 nextPage의 pageParam 값이 queryFn의 pageParam으로 넘어가고 nextPage에 대한 데이터를 불러와 pages 배열의 두번째 요소로 들어간다. pageParam의 값에 따라 pageParams 배열의 두번째 요소도 정해진다.

📌 pages와 pageParams를 갖는 캐시 데이터

  • useQuery에서는 QueryFn의 반환값이 캐시데이터로 등록된다.
  • useInfiniteQuery에서는 QueryFn의 반환값은 pages 배열의 요소로 추가되고, 매개변수로 받았던 pageParam은 pageParams 배열의 요소로 추가된다.

3. useInfiniteQuery로 무한스크롤 구현하기

const queryKey = pathname === '/review' ? 'reviews' : 'mates';

  const {
    data: posts,
    isLoading,
    isError,
    hasNextPage,
    fetchNextPage,
    isFetchingNextPage
  } = useInfiniteQuery<FetchPost>({
    queryKey: [`${queryKey}`, storeId, pathname],
    queryFn: ({ pageParam }) => getStorePosts(pageParam, storeId, pathname),
    getNextPageParam: (lastPage) => {
      // 전체 페이지 개수보다 작을 때
      if (lastPage.page < lastPage.totalPages) {
        // 다음 페이지로 pageParam을 저장
        return lastPage.page + 1;
      }
      return null; // 마지막 페이지인 경우
    }
  });

4. Intersection Observer

📌 Intersection Observer란? Intersection Observer은 기본적으로 브라우저 뷰포트(Viewport)와 설정한 요소(Element)의 교차점을 관찰하며, 요소가 뷰포트에 포함되는지 포함되지 포함되지 않는지, 즉 사용자의 화면에 보이는 요소인지 아닌지를 구별하는 기능을 제공한다.

  1. Intersection Observer로 감시할 대상을 ref로 지정해준다.
  2. Intersection Observer의 옵션 중 threshold를 지정한다.
    ➡️ 이때 threshold란? 옵저버가 실행되기 위해 타겟의 가시성이 얼마나 필요한 지에 대한 설정이다.
    ➡️ threshold가 1인 경우? 관찰 대상의 맨 밑 부분이 화면의 맨 밑 부분과 교차 되었을 때를 말한다.

5. react-intersection-observer 이용하기

  1. react-intersection-observer 설치

    yarn add react-intersection-observer
  2. useInView hook import 하기

    import { useInView } from 'react-intersection-observer';
  3. useInView에서 ref라는 값을 받아와서 관찰대상 및 threshold 설정하기

  4. threshold가 1일 때 onChange 함수를 통해 fetchNextPage를 호출
    ➡️ 이때 onChange 함수의 매개변수로 들어오는 inView 값은 true 또는 false이다. 즉, 관찰대상이 교차가 되었는가에 대한 여부를 가리킨다.

      // 언제 다음 페이지를 가져올 것
      const { ref } = useInView({
        threshold: 1, // 맨 아래에 교차될 때
        onChange: (inView: any) => {
          if (!inView || !hasNextPage || isFetchingNextPage) return;
          fetchNextPage();
        }
      });
    <St.Trigger ref={ref} />

6. pages의 results를 단일 배열로 만들어주기

select: (data) => {
  return data.pages
    .map((pageData) => {
      return pageData.results;
    })
    .flat();
};
  • .map() 을 통해 pages 배열의 각 요소(pageData)에서 results 배열만 추출한다.
  • .flat() 를 통해 중첩된 배열을 하나의 배열로 만든다.

7. useMemo로 memoization 적용하기

📌 useInfiniteQuery 사용 시 주의사항

  • 훅 내부적인 동작원리로 인해 예상보다 잦은 리렌더링이 발생할 수 있다.
  • 연산량이 많은 코드가 있는 경우 useMemo와 같은 memoization 적용을 특히 고려해야 한다.
  • 리렌더링이 발생한다고 해서 실제 브라우저 렌더링이 발생하는 것은 아니다.
    ➡️ Virtual DOM에 의하면 이전 상태의 UI와 state 변경 이후 UI가 동일한 경우 브라우저 랜더링이 일어나지 않는다.
  const selectPosts = useMemo(() => {
    return posts?.pages
      .map((data) => {
        return data.posts;
      })
      .flat();
  }, [posts]);
  • useMemo 훅을 사용하여 memoized된 값을 생성한다.
  • 의존성 배열에 posts를 담아, posts의 값이 변경되었을 때만 해당 함수를 실행한다.
  • posts 배열이 변경되지 않는 경우 useMemo는 이전에 계산한 값을 반환하며 다시 계산하지 않는다.

8. 최종 코드

  const queryKey = pathname === '/review' ? 'reviews' : 'mates';
  const {
    data: posts,
    isLoading,
    isError,
    hasNextPage,
    fetchNextPage,
    isFetchingNextPage
  } = useInfiniteQuery<FetchPost>({
    queryKey: [`search${queryKey}`, keyword, ctg, pathname],
    queryFn: ({ pageParam }) => getSearchPosts(pageParam, keyword, ctg, pathname),
    getNextPageParam: (lastPage) => {
      if (lastPage.page < lastPage.totalPages) {
        return lastPage.page + 1;
      }
      return null;
    }
  });

  const selectPosts = useMemo(() => {
    return posts?.pages
      .map((data) => {
        return data.posts;
      })
      .flat();
  }, [posts]);

  const { ref } = useInView({
    threshold: 1,
    onChange: (inView: any) => {
      if (!inView || !hasNextPage || isFetchingNextPage) return;
      fetchNextPage();
    }
  });
profile
프론트엔드 개발자를 꿈꾸는

1개의 댓글

comment-user-thumbnail
2023년 9월 12일

무한스크롤 강의 열어주세요

답글 달기