[React Query] useInfiniteQuery로 무한스크롤 구현하기 feat. react-intersection-observer

apro_xo·2022년 8월 16일
0
post-thumbnail

react-query를 사용하면서 무한스크롤을 구현하기 위해서는 보통 useInfiniteQuery를 사용한다.

나는 여태 axios + redux-thunk + intersection observer API 의 조합으로 무한스크롤 구현을 해왔는데, 이번에 react-query로 프로젝트를 진행하며 무한스크롤에 대한 고민이 생겼다.

axios + useInfiniteQuery + react-intersection-observer 의 조합으로 무한스크롤을 구현하는 방법이 있길래 사용해보고 공부해서 여기에 기록한다. 우선 react-intersection-observer의 사용법부터 간단하게 알아보자.

1. react-intersection-observer

1-1. 설치

npm install react-intersection-observer

1-2. 개념 및 코드

1-2-1. 개념

개념 자체는 web API인 intersection observer API와 조금 다르다.

intersection observer API는 root element와 target element가 겹쳐지는 순간 콜백함수를 실행하는 개념이라면 react-intersection-observer는 조금 다르다.

우선 react-intersection-observer는 useInView라는 훅을 제공한다. 이 훅을 통해 ref와 inView라는 boolean값을 받아올 수 있는데, ref는 어떤 element를 가리키는 역할을 하고, inView는 ref가 가리키는 element가 화면에 보이는지 아닌지에 따라 boolean 값을 반환한다.

1-2-2. 예시 코드

import React, { useEffect } from 'react';
import { useInView } from 'react-intersection-observer';

const Home = () => {
  const { observerRef, inView } = useInView();
  
  useEffect(()=> {
    console.log(inView);
  }, [inView]);
  
  return(
    (..생략)
    <div ref={observerRef}></div>
  )
}

2. useInfiniteQuery

useInfiniteQuery는 react-query에서 제공하는 훅으로, 파라미터 값만 변경하면서 동일한 useQuery를 무한히 반복하고 싶을 때 사용한다. 이것을 이용하여 무한스크롤을 구현할 수 있다.

2-1. 반환값

useInfiniteQuery가 반환하는 많은 것들이 있지만, 그 중에서 fetchNextPage, isFetchingNextPage, hasNextPage 정도만 알아보자.

2-1-1. hasNextPage

다음 페이지가 있는지 없는지 판단할 수 있는 boolean 값으로, hasNextPage 값이 참이어야 fetchNextPage 메서드를 사용할 수 있다.
useInfiniteQuery의 옵션 중 getNextPageParam 옵션이 있는데, 이 옵션에서 이 값을 반환한다.

2-1-2. fetchNextPage

다음 API를 요청할 때 사용되는 메서드이다. 따라서 무한스크롤을 구현할 때 ref로 가리키는 element가 화면에 보이면 fetchNextPage()를 하여 다음 페이지를 가져올 수 있다.

2-1-3. isFetchingNextPage

이름 그대로 다음 페이지를 불러오는 API 요청을 하는 중인지 아닌지 판별할 수 있는 boolean 값이다. 스크롤이 되어 다음 페이지를 불러올 때 로딩화면을 나타낼지 말지 결정할 수 있겠다.

2-2. 옵션

2-2-1. getNextPageParam

hasNextPage의 값을 결정한다. 그리고 두 개의 파라미터를 가질 수 있다.

🔥 lastPage
useInfiniteQuery를 이용해 호출된 가장 마지막에 있는 페이지 데이터를 의미

🔥 allPage
useInfiniteQuery를 이용해 호출된 모든 페이지 데이터를 의미

📌 사용법

// api.js
export const postingsAPI = {
    fetchPostingsListWithScroll: async (pageParams, category) => {
        const res = await instance.get(`/api/main/${category}?page=${pageParams}&size=5`);
        const { content } = res.data;
        const { last } = res.data;
        return { posts:content, nextPage: pageParams + 1, isLast:last}
    },
}
//Component.jsx
(..생략)
const paramCategory = useParams().category;

const { data, fetchNextPage, isFetchingNextPage, refetch } = useInfiniteQuery(
      ['cardList', paramCategory],
      ({ pageParam = 0 }) => postingsAPI.fetchPostingsListWithScroll(pageParam, paramCategory),
      {
        getNextPageParam: (lastPage) =>
          !lastPage.isLast ? lastPage.nextPage : undefined,

        onSuccess:(data) => {
          console.log(data);
        },
        retry:false
      },
    );

(..생략)

app.js에서 API 통신으로 받은 response에서 last라는 값이 있는데, 이는 백엔드에서 지금 현재 넘겨주는 페이지가 마지막 페이지인지 아닌지를 boolean값으로 나타내 주는 값이다.

fetchPostingsListWithScroll에서는 useInfiniteQuery에서 쓰일 정보들을 객체로 만들어 반환한다.
따라서 getNextPageParam에서 lastPage의 isLast값이 false라면 다음 페이지가 있다는 것이므로 다음 페이지 숫자를 리턴하고, hasNextPage가 true가 되어 다음 페이지를 불러올 수 있게 된다.

3. 무한스크롤 구현하기

import React, { useEffect, useState } from 'react';
import { useQueryClient, useInfiniteQuery } from '@tanstack/react-query';
import { useParams } from 'react-router-dom';
import { postingsAPI } from '../shared/api';
import styled from 'styled-components';
import { useInView } from 'react-intersection-observer';
import PostingCard from '../components/card/PostingCard';
import Header from '../components/header/Header';




const Home = () => {
    const paramCategory = useParams().category;
    const queryClient = useQueryClient();
    const { ref, inView} = useInView();

    const { data, fetchNextPage, isFetchingNextPage, refetch } = useInfiniteQuery(
      ['cardList', paramCategory],
      ({ pageParam = 0 }) => postingsAPI.fetchPostingsListWithScroll(pageParam, paramCategory),
      {
        getNextPageParam: (lastPage) =>
          !lastPage.isLast ? lastPage.nextPage : undefined,

        onSuccess:(data) => {
          console.log(data);
        },
        retry:false
      },
    );

    useEffect(()=> {
      if(inView){
        fetchNextPage();
      } 
    }, [inView])    
  
    return (
      <HomeWrapper>
        <Header/>
            {data.pages?.map((page, index) => (
              <Page key={index} >
                  {page.posts.map((post) => (
                    paramCategory==='postings'?<PostingCard key={post.posting_id} post={post}></PostingCard>:null
                  ))}
              </Page>
            ))}
        {isFetchingNextPage ? <div>로딩중입니다1!!!!</div>: <div ref={ref} style={{height:"100px"}}></div>}
        </HomeWrapper>
    );
}

(..생략)
profile
유능한 프론트엔드 개발자가 되고픈 사람😀

0개의 댓글