프로젝트를 진행하면서 useInfinityQuery 을 사용하여 무한스크롤을 구현했다. 구현했다곤 하지만 사실상 메인페이지 담당 팀원이 먼저 구현하신 부분을 코드 동일성과 시간적 여유 등을 이유로 거의 가져다 쓴 것에 가까워서 내 것으로 만들기 위해 다시 정리한다.
// 공식페이지 예시 코드
const {
fetchNextPage,
fetchPreviousPage,
hasNextPage,
hasPreviousPage,
isFetchingNextPage,
isFetchingPreviousPage,
...result
} = useInfiniteQuery({
queryKey,
queryFn: ({ pageParam = 1 }) => fetchPage(pageParam),
...options,
getNextPageParam: (lastPage, allPages) => lastPage.nextCursor,
getPrevio
// 실제 작성한 코드
import { useInfiniteQuery } from '@tanstack/react-query';
import { GetUserPortfolioPage, PageParam } from '../types';
import { UserPortfolioAPI } from '../api/client';
import { AxiosError } from 'axios';
const { getUserPortfolio } = UserPortfolioAPI;
export const useGetUserPortfolios = (userId: number, size: string, sortOption: string) => {
const {
data: UserPortfolios,
isError: getUserPortfoliosError,
isFetching: getUserPortfoliosFetching,
error: ErrorInfo,
status: userPortfoliosStatus,
fetchNextPage: fetchNextUserPortfolios,
hasNextPage: hasNextUserPortfolios,
isLoading: getUserPortfolioLoading,
} = useInfiniteQuery<GetUserPortfolioPage, AxiosError>({
queryKey: ['userPortfolios', userId, sortOption],
queryFn: ({ pageParam = 1 }: PageParam) =>
getUserPortfolio(userId, sortOption, pageParam, size),
getNextPageParam: (lastPage) => {
if (lastPage.currentPage == lastPage.pageInfo.totalPages) {
return undefined;
} else {
return lastPage.currentPage + 1;
}
},
retry: false,
});
return {
UserPortfolios,
getUserPortfoliosError,
getUserPortfoliosFetching,
ErrorInfo,
userPortfoliosStatus,
fetchNextUserPortfolios,
hasNextUserPortfolios,
getUserPortfolioLoading,
};
};
다음 pageParam을 가져오는 옵션
lastPage를 인자로 받으며, 이 lastPage는 직전 호출한 페이지 조회 함수의 리턴값
다음 페이지가 있는지를 true/false값으로 알려주는 옵션
getNextPageParam에서 리턴한 값이 유효한 값이 아니라면 false를 return
해당 프로젝트에서는 데이터를 요청할 때 totalPages 값을 함께 요청하여 getNextPageParam에서 값을 비교하였다.
화면에 내가 지정한 Target Element가 노출되었는지 여부를 확인할 수 있는 API
- Lazy-loading of images or other content as a page is scrolled.
스크롤에 따라 이미지나 다른 컨텐츠를 lazy-loading할 때- Implementing "infinite scrolling" websites, where more and more content is loaded and rendered as you scroll, so that the user doesn't have to flip through pages.
Infinite scrolling을 통해 점점 더 많은 컨텐츠를 로드하고 렌더할 때- Reporting of visibility of advertisements in order to calculate ad revenues.
광고 수익 계산을 위해 광고의 가시성을 측정할 때- Deciding whether or not to perform tasks or animation processes based on whether or not the user will see the result.
사용자가 화면을 볼 수 있는지 여부에 따라 작업 또는 애니메이션 프로세스를 수행할 때
출처 : MDN
new IntersectionObserver(callback, options)
// callback : 가시성의 변화가 생겼을 때 호출
// options : 코랩ㄱ이 호출되는 상황 정의
타겟 요소의 관찰이 시작되거나 가시성에 변화가 감지되면 실행
let callback = (entries, observer) => {
entries.forEach((entry) => {
// Each entry describes an intersection change for one observed
// target element:
// entry.boundingClientRect
// entry.intersectionRatio
// entry.intersectionRect
// entry.isIntersecting
// entry.rootBounds
// entry.target
// entry.time
});
};
IntersectionObserverEntry 객체 리스트를 배열 형식으로 반환
콜백이 호출되는 IntersectionObserver
let options = {
root: document.querySelector("#scrollArea"),
rootMargin: "0px",
threshold: 1.0,
};
let observer = new IntersectionObserver(callback, options);
어떤 요소를 기준으로 target이 들어오고 나가는 것을 확인할 것인지 지정
기본값 : null
root의 범위를 확장하거나 축소 가능
기본값 : 0px
target이 뷰포트에 얼마나 진입해야(root와 얼마나 교차되어야) callback을 호출할 것인지를 표시
기본값 : 0
(0 : target이 영역에 진입 시작, 1 : target 전체가 진입)
프로젝트에는 이러한 과정을 useHook으로 만들어서 진행
export function useInfiniteScroll({
targetRef,
isError,
isFetching,
hasNext,
fetchNext,
// targetRef와 useInfiniteQuery의 값을 전달
}) {
useEffect(() => {
const options = {
root: null,
rootMargin: '100px',
threshold: 0.3,
};
const handleIntersect: IntersectionObserverCallback = (
entries: IntersectionObserverEntry[],
) => {
entries.forEach((entry) => {
// 에러가 일어나지 않고, 다음 페이지가 있을 경우 => 다음 페이지를 불러오는 api 실행
if (entry.isIntersecting && !isError && hasNext) {
fetchNext();
}
});
};
const observer = new IntersectionObserver(handleIntersect, options);
if (targetRef.current) {
observer.observe(targetRef.current);
}
return () => {
if (targetRef.current) {
observer.unobserve(targetRef.current);
}
};
}, [fetchNext, hasNext, isFetching]);
}