📌 useInfiniteQuery란? Data Fetching이 일어날 때 마다 기존 리스트 데이터에 Fetched Data 를 추가하고자 할 때 유용하게 사용할 수 있는 React-query hook이다. 더보기 UI 또는 무한스크롤 UI 에 사용하기에 적합하다.
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 배열의 요소로 추가된다.
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; // 마지막 페이지인 경우
}
});
📌 Intersection Observer란? Intersection Observer은 기본적으로 브라우저 뷰포트(Viewport)와 설정한 요소(Element)의 교차점을 관찰하며, 요소가 뷰포트에 포함되는지 포함되지 포함되지 않는지, 즉 사용자의 화면에 보이는 요소인지 아닌지를 구별하는 기능을 제공한다.
react-intersection-observer 설치
yarn add react-intersection-observer
useInView hook import 하기
import { useInView } from 'react-intersection-observer';
useInView에서 ref라는 값을 받아와서 관찰대상 및 threshold 설정하기
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} />
select: (data) => {
return data.pages
.map((pageData) => {
return pageData.results;
})
.flat();
};
.map()
을 통해 pages 배열의 각 요소(pageData)에서 results 배열만 추출한다..flat()
를 통해 중첩된 배열을 하나의 배열로 만든다.📌 useInfiniteQuery 사용 시 주의사항
- 훅 내부적인 동작원리로 인해 예상보다 잦은 리렌더링이 발생할 수 있다.
- 연산량이 많은 코드가 있는 경우 useMemo와 같은 memoization 적용을 특히 고려해야 한다.
- 리렌더링이 발생한다고 해서 실제 브라우저 렌더링이 발생하는 것은 아니다.
➡️ Virtual DOM에 의하면 이전 상태의 UI와 state 변경 이후 UI가 동일한 경우 브라우저 랜더링이 일어나지 않는다.
const selectPosts = useMemo(() => {
return posts?.pages
.map((data) => {
return data.posts;
})
.flat();
}, [posts]);
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();
}
});
무한스크롤 강의 열어주세요