본문 바로가기

TanStack Query

[TanStack Query / React Query] useInfiniteQuery와 무한 스크롤

 

 

useInfiniteQuery는 페이지네이션이나 무한 스크롤과 같은 데이터 로딩을 구현할 때 사용됩니다.

 

 

 

 

1. useQuery 와 useInfiniteQuery의 차이

 

 

● useQuery


단일 데이터 객체 반환
한 번의 요청에 대한 응답만 저장

 


  useInfiniteQuery


pages 배열: 모든 페이지의 데이터를 순서대로 저장
pageParams 배열: 각 페이지를 가져올 때 사용된 매개변수 저장
여러 페이지의 데이터를 누적하여 저장

 

좀 더 이해하기 쉽게 data에 로그를 찍어보면 

useQuery 와 uesInfiniteQuery 의 반환되는 data 의 구조는 다음과 같은 차이가 있습니다

 

 

 

1-1. useQuery 

const { data } = useQuery({
  queryKey: ['items'],
  queryFn: fetchItems
});

console.log(data) 

// 출력:
{
  results: [...],
  count: 100,
  next: "url",
  previous: null
}

 

 

 

1-2. uesInfiniteQuery 

const { data } = useInfiniteQuery({
  queryKey: ['items'],
  queryFn: fetchItems
});

// data는 pages와 pageParams를 포함하는 객체
console.log(data)

// 출력:
{
  pages: [
    // 첫 번째 페이지 데이터
    {
      results: [...],
      count: 100,
      next: "url",
      previous: null
    },
    // 두 번째 페이지 데이터
    {
      results: [...],
      count: 100,
      next: "url",
      previous: "url"
    },
    // ... 추가 페이지들
  ],
  pageParams: [undefined, "url", "url"] // 각 페이지를 가져올 때 쿼리함수(queryFn)에 전달되는 매개변수
}

 

useQuery 와는 달리 pages 란 배열에 담겨진 각 페이지에 보여줄 데이터들이 객체로 나눠져 담겨집니다.

 

 

따라서 useInfiniteQuery에서 데이터에 접근할 때는 보통 다음과 같이 합니다

data?.pages.map((page) => {
  return page.results.map((item) => (
    <Item key={item.id} {...item} />
  ));
});

 

 

2. uesInfiniteQuery  특징

 

  • 페이지별 데이터 관리: 각 페이지의 데이터를 자동으로 관리
  • 다음 페이지 정보 추적: queryFn의 pageParam을 통해 다음 페이지 정보를 추적
  • 캐싱: 이미 불러온 데이터를 캐시에 저장

 

 

3. 기본 구조

const {
  data,                // 모든 페이지 데이터를 포함하는 객체
  fetchNextPage,       // 다음 페이지 로드 함수
  hasNextPage,         // 다음 페이지 존재 여부,  boolean
  isFetchingNextPage,  // 다음 페이지 로딩 상태  boolean
} = useInfiniteQuery({
  queryKey: ['items'],
  queryFn: ({ pageParam = 초기값 }) => fetchUrl(pageParam),
  getNextPageParam: (lastPage, allPages) => {
    const nextPage = allPages.length + 1;
    return nextPage <= lastPage.totalPages ? nextPage : undefined;
  }
});

 

3- 1. getNextPageParam 추가 설명

 

- 이 쿼리에 대한 새 데이터가 수신되면 이 함수는 무한 데이터 목록의 마지막 페이지와 캐쉬에 저장된 모든 페이지의 전체 배열을 모두 수신합니다. 

 

- 쿼리 함수에 마지막 선택적 매개변수로 전달할 단일 변수를 반환해야 합니다. 

 

- getNextPageParam의 반환값이 다음 queryFn 호출 시의 pageParam 값이 됩니다.

더보기
// 첫 번째 페이지 로드
queryFn({ pageParam: 'https://api.example.com/page1' })
↓
getNextPageParam(lastPage) // returns 'https://api.example.com/page2'
↓
// fetchNextPage() 호출 시
queryFn({ pageParam: 'https://api.example.com/page2' })
↓
getNextPageParam(lastPage) // returns 'https://api.example.com/page3'
↓
// 계속...

 

- 다음 페이지가 없음을 나타내려면 undefined 를 반환합니다.

 

 

3-1-1. getNextPageParam  의 파라미터 allPages:

 

- 현재까지 로드된 모든 페이지 데이터를 포함하는 배열
- data.pages와 동일한 배열
- 각 요소는 queryFn이 반환한 페이지 데이터

 

 

3-2. hasNextPage

 

- getNextPageParam의 반환값을 기반으로 결정됨
- true: 다음 페이지가 있음 (getNextPageParam이 undefined가 아닌 값 반환)
- false: 마지막 페이지에 도달 (getNextPageParam이 undefined 반환)

 

 

3-3. isFetchingNextPage

 

- 다음 페이지 데이터를 가져오는 중인지 상태
- true: fetchNextPage() 호출로 새 페이지를 로딩 중
- false: 로딩이 완료되었거나 아직 시작하지 않음

 

 

 

 

4. 적용법

 

const fetchUrl = async (url) => {
  const response = await fetch(url);
  return response.json();
};

export function InfinitePeople() {
 const {data, fetchNextPage, hasNextPage, isFetching, isLoading } =  useInfiniteQuery({
    queryKey: ['people'],
    queryFn: ({pageParam = initialUrl}) => fetchUrl(pageParam),
    getNextPageParam: (lastPage) => {
      return lastPage.next || undefined
    }
  })
  
  if (isLoading) return <div>로딩중...</div> 

  return (
  <InfiniteScroll 
    loadMore={() => {
    if(!isFetching)  fetchNextPage()} }
    hasMore={hasNextPage}
    initialLoad={false} // 추가 설명 아래에 있습니다
    >
      {data.pages.map(pageData => {
        return pageData.results.map(person => 
         <Person 
           key={person.name} 
           name={person.name} 
        />
        )
      })}
    </InfiniteScroll>
  )
}

 

여기서 

  if (isLoading) return <div>로딩중...</div>

이 없다면 처음 페이지를 렌더링할 때, data 는 undefined 이므로 data.pages 가 undefined 라는 오류가 발생하게된다

 

위처럼 로딩 안내를 하거나 data?.pages? 처럼 옵셔널 체이닝을 하면 오류가 해결되지만

사용자 경험측면에서 로딩상태 안내가 더 낫다고 판단되어 별도의 태그를 사용했다

 

initialLoad={false} react-infinite-scroller 의 작동방식 때문에 추가로 설정하게 됬습니다.

추가로 정리하겠습니다.

https://daunje0.tistory.com/208

 

[React] react query와 react infinite scroller 사용 시 두 번째 페이지 중복 호출 이슈

react infinite scroller 라이브러리에는 initialLoad 속성이 true 로 기본 설정이 되어있습니다. react query 와 함께 사용할 경우, 1. 첫 페이지 데이터를  react query 가 가져오게 되고2. InfiniteScroll 컴포넌트

daunje0.tistory.com

 

 

 

무한스크롤 구현 시, 아래의 라이브러리를 사용 했습니다