[react-query] useInfiniteQuery

이춘구·2022년 6월 17일
18

translation

목록 보기
1/11

Tanstack Query 공식문서의 Infinite Queries를 번역한 글입니다. 피드백은 댓글로 부탁드립니다.


Infinite Queries

기존 데이터에 더해 데이터를 "더 불러와서" 렌더링 하는 것이나 "무한 스크롤"은 매우 흔한 UI 패턴입니다. Tanstack Query는 이런 리스트형 데이터를 요청하기 위해, useQuery의 유용한 버전인 useInfiniteQuery를 지원합니다.

useInfiniteQuery를 사용할 때, 몇 가지가 다르다는 걸 알아차릴 것입니다.

  • data는 infinite query 데이터가 담겨있는 객체입니다.
  • data.pages은 fetch한 페이지들이 담겨있는 배열입니다.
  • data.pageParams 은 페이지들을 fetch 하는 데에 필요한 page params가 담겨있는 배열입니다.
  • fetchNextPagegetPreviousPageParam를 사용할 수 있습니다.
  • getNextPageParamgetPreviousPageParam 옵션은 불러올 데이터가 더 있는지 여부와 fetch할 정보를 결정할 때 사용할 수 있습니다. 이 정보는 query 함수에 추가 매개 변수로 제공됩니다.
    (query 함수는 fetchNextPage 또는 fetchPreviPage 함수를 호출할 때, 선택적으로 오버라이드 될 수 있음)
  • getNextPageParam 함수가 undefined가 아닌 다른 값을 반환하면 hasNextPagetrue입니다.
  • getPreviousPageParam 함수가 undefined가 아닌 다른 값을 반환하면 hasPreviousPagetrue입니다.
  • isFetchingNextPageisFetchPreviousPage로 백그라운드 새로고침 상태인지 추가 로딩 중인 상태인지 구별할 수 있습니다.

주의: query에서 initialDataselect 같은 옵션을 사용할 경우, 데이터를 재구성할 때 data.pagesdata.pageParams 속성이 계속 담겨있도록 해야 합니다. 그렇게 하지 않으면 query가 반환될 때 변경사항을 덮어쓰게 됩니다!

Example

한 번의 요청에 projects 3개와 다음 projects 3개를 불러올 때 사용할 수 있는 cursor를 불러오는 API가 있다고 가정하겠습니다.

fetch('/api/projects?cursor=0')
// { data: [...], nextCursor: 3}

fetch('/api/projects?cursor=3')
// { data: [...], nextCursor: 6}

fetch('/api/projects?cursor=6')
// { data: [...], nextCursor: 9}

fetch('/api/projects?cursor=9')
// { data: [...] }

이 정보를 가지고 우리는 "더 불러오기" UI를 이렇게 만들 수 있습니다.

  • useInfiniteQuery의 default 동작인 첫번째 데이터 그룹 요청을 기다리기
  • getNextParam으로 다음 요청을 위한 정보 반환하기
  • fetchNextPage 함수 호출하기

주의: getNextParam 함수가 반환한 pageParam을 덮어쓰고 싶은 게 아니라면, fetchNextPage에 인자를 넘겨서 호출하지 말아야 합니다.
예시) <button onClick={fetchNextPage} /> <= onClick 이벤트를 fetchNextPage 함수에 인자로 넘기게 되니까 이렇게 하지 마세요.

import { useInfiniteQuery } from '@tanstack/react-query'

function Projects() {
  const fetchProjects = async ({ pageParam = 0 }) => {
    const res = await fetch('/api/projects?cursor=' + pageParam);
    return res.json();
  }

  const {
    data,
    error,
    fetchNextPage,
    hasNextPage,
    isFetching,
    isFetchingNextPage,
    status,
  } = useInfiniteQuery({
    queryKey: ['projects'],
    queryFn: fetchProjects,
    getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
  })

  return status === 'loading' ? (
    <p>Loading...</p>
  ) : status === 'error' ? (
    <p>Error: {error.message}</p>
  ) : (
    <>
      {data.pages.map((group, i) => (
        <React.Fragment key={i}>
          {group.projects.map(project => (
            <p key={project.id}>{project.name}</p>
          ))}
        </React.Fragment>
      ))}
      <div>
        <button
          onClick={() => fetchNextPage()}
          disabled={!hasNextPage || isFetchingNextPage}
        >
          {isFetchingNextPage
            ? 'Loading more...'
            : hasNextPage
            ? 'Load More'
            : 'Nothing more to load'}
        </button>
      </div>
      <div>{isFetching && !isFetchingNextPage ? 'Fetching...' : null}</div>
    </>
  )
}

infinite query가 refetch 되어야 하면 무슨 일이 일어나나요?

infinite query가 stale 상태가 되어서 refetch 되어야 하면, 각 데이터 그룹은 첫번째 그룹부터 순차적으로 fetch 됩니다. 이렇게 함으로써 서버의 데이터가 변형되더라도, 오래된 커서를 사용하지 않는다는 것과 중복 또는 생략된 데이터를 받지 않을 거란 걸 확실히 할 수 있습니다. 만약 infinite query의 결과가 queryCache에서 제거되면, 페이지네이션은 첫번재 데이터 그룹만 요청하는 초기 상태에서 재시작됩니다.

refetchPage

전체 페이지 중 일부만 직접 refetch 하고 싶을 때에는, useInfiniteQuery가 반환하는 refetch 함수에 refetchPage를 넘겨주면 됩니다.

const { refetch } = useInfiniteQuery({
  queryKey: ['projects'],
  queryFn: fetchProjects,
  getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
})
 
// 첫번째 페이지만 refetch 합니다.
refetch({ refetchPage: (page, index) => index === 0 })

이 함수를 queryClient.refetchQueries, queryClient.invalidateQueries, queryClient.resetQueries에 두번째 인자(queryFilters)로 넘길 수도 있습니다.

시그니처

  • refetchPage: (page: TData, index: number, allPages: TData[]) => boolean

이 함수는 각 페이지에 대해 실행되며, 이 함수가 true를 반환하는 페이지만 refetch 됩니다.

query 함수에 사용자 지정 정보를 넘겨야 하면 어떻게 하나요?

getNextPageParam이 반환한 변수들은 query 함수로 넘어가는 게 default지만, 그 변수들을 덮어쓰고 싶다면 아래와 같이 fetchNextPage에 사용자 지정 변수들을 넘기면 됩니다.

function Projects() {
  const fetchProjects = ({ pageParam = 0 }) =>
    fetch('/api/projects?cursor=' + pageParam)

  const {
    status,
    data,
    isFetching,
    isFetchingNextPage,
    fetchNextPage,
    hasNextPage,
  } = useInfiniteQuery({
    queryKey: ['projects'],
    queryFn: fetchProjects,
    getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
  })
 
  // 여러분이 원하는 pageParam을 넘기세요.
  const skipToCursor50 = () => fetchNextPage({ pageParam: 50 })
}

양방향 infinite list를 구현하고 싶으면 어떻게 하나요?

양방향 list는 getPreviousPageParam, fetchPreviousPage, hasPreviousPage, isFetchingPreviousPage를 이용해서 구현할 수 있습니다.

useInfiniteQuery({
  queryKey: ['projects'],
  queryFn: fetchProjects,
  getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
  getPreviousPageParam: (firstPage, pages) => firstPage.prevCursor,
})

페이지를 역순으로 보여주고 싶으면 어떻게 하나요?

페이지들을 역순으로 보여주고 싶으면 select 옵션을 사용하세요.

useInfiniteQuery({
  queryKey: ['projects'],
  queryFn: fetchProjects,
  select: data => ({
    pages: [...data.pages].reverse(),
    pageParams: [...data.pageParams].reverse(),
  }),
})

infinite query를 수동으로 업데이트 하고 싶으면 어떻게 하나요?

  • 첫번째 페이지를 수동으로 제거하기
queryClient.setQueryData(['projects'], data => ({
  pages: data.pages.slice(1),
  pageParams: data.pageParams.slice(1),
}))
  • 개별 페이지에서 수동으로 하나의 값 제거하기
const newPagesArray = oldPagesArray?.pages.map((page) =>
  page.filter((val) => val.id !== updatedId)
) ?? []

queryClient.setQueryData(['projects'], data => ({
  pages: newPagesArray,
  pageParams: data.pageParams,
}))

pagespageParams가 각각 동일한 데이터 구조를 유지하도록 하세요!

profile
프런트엔드 개발자

0개의 댓글