기존에는 product 갯수가 20개였지만, 60개로 늘려서 product.json 파일을 수정한다.
60개의 상품을 4개의 페이지로 나눈다고 생각하면 id 1~15번인 15개의 상품을 먼저 보여준 후 커서가 끝까지 갔을 때 id 16~30번인 15개의 상품을 보여주고 ... 하는 식으로 무한 스크롤로 페이지를 나눌 수 있다.
▼ server/src/schema/products.ts
extend type Query {
products(cursor: ID): [Product!]
product(id: ID!): Product!
}
▼ server/src/resolvers/products.ts
products: (parent, { cursor = "" }, { db }) => {
const fromIndex =
db.products.findIndex((product) => product.id === cursor) + 1;
return db.products.slice(fromIndex, fromIndex + 15) || [];
},
서버의 설정은 끝났다.
https://tanstack.com/query/v4/docs/react/reference/useInfiniteQuery
- useInfiniteQuery는 일반적인 useQuery와 매우 유사하지만, 페이지네이션, 무한 스크롤, 커서 기반 페이지네이션 등과 같은 사용 사례를 지원하는 데 특화되어 있다.
▼ client/src/pages/products.tsx
const { data, isSuccess, fetchNextPage, hasNextPage, isFetchingNextPage } =
useInfiniteQuery<Products>(
QueryKeys.PRODUCTS,
({ pageParam = "" }) =>
graphqlFetcher<Products>(GET_PRODUCTS, { cursor: pageParam }),
{
getNextPageParam: (lastpage, allpages) => {
return lastpage.products.at(-1)?.id;
},
}
);
상품목록 화면에서 ProductItem 컴포넌트를 호출하면서 data.pages를 props로 넘겨줄 것이므로, productList에서 props로 list를 전달받으면, list에서 이중 map을 돌려야 원하는 data에 접근할 수 있다.
▼ client/src/components/list.tsx
const ProductList = ({ list }: { list: { products: Product[] }[] }) => {
return (
<ul className="products">
{list.map((page) =>
page.products.map((product) => (
<ProductItem {...product} key={product.id} />
))
)}
</ul>
);
};
전통적인 방법
: scrollTop = window.height 등을 이용해서 정말 도달했는지 계속 감지하는 방법
- eventHandler로 (scroll) 감시를 계속해주어야 하며, throttle, debounce처리까지 필요할 수 있다.
→ 스레드 메모리를 사용하게 되며, 성능에도 좋지 않다.
interSectionObserver 이용하는 방법
: 이벤트 등록 x, 브라우저에서 제공하는 별개의 감지자
- 싱글스레드인 자바스크립트와 별개로 동작하므로 성능 문제 발생 x
:Intersection Observer는 웹 API로, 뷰포트와 요소 간의 교차 영역을 감지하는 기능을 제공한다. 이를 통해 요소가 뷰포트에 들어오거나 나갈 때를 감지하고, 이벤트를 처리할 수 있다.
보통 ref를 사용한다.
const observerRef = useRef<IntersectionObserver>();
entries는 ntersectionObserverEntry 인스턴스의 배열인데, 어떤 값이 들어가는 지 확인하기 위해 console에 출력해볼 것이다.
▼ client/src/pages/products.tsx
const getObserver = useCallback(() => {
if (!observerRef.current) {
observerRef.current = new IntersectionObserver((entries) => {
console.log("entries", entries);
});
}
return observerRef.current;
}, [observerRef.current]);
const fetchMoreRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (fetchMoreRef.current) {
getObserver().observe(fetchMoreRef.current);
}
}, [fetchMoreRef.current]);
const [intersecting, setIntersecting] = useState(false);
setIntersecting(entries[0]?.isIntersecting);
useEffect(() => {
if (!intersecting || !isSuccess || !hasNextPage || isFetchingNextPage)
return;
fetchNextPage();
}, [intersecting]);
스크롤 할 때마다 데이터가 추가되어 보여지고 있는 것을 확인할 수 있다.
observer를 쓰는 부분의 코드가 길고 재사용할 가능성이 있으므로 따로 hooks로 저장해둘 것이다.
▼ client/src/hooks/useIntersection.ts
const useInfiniteScroll = (targetRef: RefObject<HTMLElement>) => {
const observerRef = useRef<IntersectionObserver>();
const [intersecting, setIntersecting] = useState(true);
const getObserver = useCallback(() => {
if (!observerRef.current) {
observerRef.current = new IntersectionObserver((entries) => {
setIntersecting(entries.some((entry) => entry.isIntersecting));
});
}
return observerRef.current;
}, [observerRef.current]);
useEffect(() => {
if (targetRef.current) {
getObserver().observe(targetRef.current);
}
}, [targetRef.current]);
return intersecting;
};
잘 봤습니다. 좋은 글 감사합니다.