-
custom Infinite Scrolling hooks (with tanstack Query in Next.js)FrontEnd/React.js 2024. 9. 3. 14:59
오랜만에 무한 스크롤링 구현하면서 기록을 남겨봄
두가지 커스텀 훅을 만들 예정
1. 리액트 쿼리에서 제공하는 useInfiniteQuery를 활용한 커스텀 훅 ->이하 useQuery( 아래는 반환 값 ) -> useInfiniteQuery
{data, fetchNextPage, hasNextPage, isFetchingNextPage, status}
2. 바닥 ref가 InView인지 확인하여 조건 만족시 fetchNextPage 발동 할 useInfiniteScroll
1. useQuery 매개변수로는 아래 요인들 ( 카테고리에서 날짜,지역,태그에 따라 보여지는 게시물들이 다름)
{category,searchDate,searchRegions, searchTags}
일단 react-query 에서 제공 받을 값들부터 알아보자
- data:
- 서버로부터 받은 응답 데이터를 포함하는 객체
- *** 무한 쿼리의 경우, 이 객체는 pages 배열을 포함
- *** 각 page는 개별 요청에 대한 응답 데이터
- 예: data.pages[0]은 첫 번째 페이지의 데이터
- fetchNextPage:
- 다음 페이지의 데이터를 가져오는 ***함수
- 사용자가 "더 보기" 버튼을 클릭하거나 스크롤의 끝에 도달했을 때 호출
- 이 함수를 호출하면 새로운 페이지의 데이터가 ****(기존 data.pages 배열에 추가)
- hasNextPage:
- 불리언 값으로, ***더 가져올 데이터가 있는지를 나타냄.
- true면 더 많은 페이지가 있다는 뜻이고, false면 모든 데이터를 가져왔다는 뜻
- 이 값은 주로 getNextPageParam 옵션의 반환값에 따라 결정
- isFetchingNextPage:
- 불리언 값으로, 현재 다음 페이지를 ***가져오고 있는 중인지를 판단
- true일 때 로딩 인디케이터를 표시하는 데 유용
- status:
- 쿼리의 현재 상태를 나타내는 문자열
- 'loading': 초기 데이터를 가져오는 중 -> 맨 처음 데이터 불러올 때라는 뜻
- 'pending'
- Tanstack Query v5에서 도입된 새로운 상태
- loading을 대체
- 쿼리가 아직 데이터를 가져오지 않았거나 가져오는 중임을 나타냄
- 캐시된 데이터가 있더라도, 백그라운드에서 refetch가 진행 중이면 'pending' 상태가 될 수 있음
- 'error': 데이터 가져오기 실패
- isFetchingNextPage VS status.pending
- isFetchingNextPage: 초기 데이터 **이후 추가 페이지를 가져올 때 사용
- status pending: 초기 데이터를 가져올 때 또는 전체 쿼리가 다시 실행될 때 사용
아래는 인피니티쿼리훅에 넘겨줄 인자들
useInfiniteQuery({ queryKey: [category, searchDate, searchRegions, searchTags], queryFn: ({ pageParam }) => getCategoryList(pageParam), initialPageParam: 0, getNextPageParam: (lastPage) => lastPage?.nextCursor, });
- queryKey:
- 쿼리를 식별하는 고유한 키
- 배열 형태로 지정
- 이 키를 기반으로 쿼리 결과가 캐시되고 관리됨. (쿼리 메모리에서 끄집어온다능)
- queryFn:
- 데이터를 가져올 패칭 함수
- 이 함수는 pageParam을 인자로 받아 페이지네이션을 구현함 -> 위에서 언급한 data.pages[num]
- initialPageParam:
- 첫 번째 페이지를 가져올 때 사용할 초기 페이지 파라미터 값
- 이 값은 queryFn의 첫 번째 호출 시 pageParam으로 전달
- default undefined
- getNextPageParam:
- 다음 페이지의 파라미터를 결정하는 함수
- 현재 페이지 데이터와 모든 페이지 데이터를 인자로 받음
- 이 함수의 반환값이 다음 queryFn 호출의 pageParam으로 사용됨
- undefined를 반환하면 더 이상 페이지가 없다고 간주
===>
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, status } = useInfiniteQuery({ queryKey: [category, searchDate, searchRegions, searchTags], queryFn: ({ pageParam }) => getCategoryList(pageParam), initialPageParam: 0, getNextPageParam: (lastPage) => lastPage?.nextCursor, }); return { data, fetchNextPage, hasNextPage, isFetchingNextPage, status, };
data => 화면에 보여줌
fetchNextPage => 무한 스크롤을 통해 바닥 찍으면 발동 (단, hasNextPage가 true일 시 )
isFetchingNextPage => 바닥을 찍고 추가로 데이터를 가져오는 상황이라면 loader 애니메이션을 보여줄 거임
status => error / pending을 통해 에러 또는 초기 로딩시 스켈레톤 보여줄 용도
2. 바닥 ref가 InView인지 확인하여 조건 만족시 fetchNextPage 발동 할 useInfiniteScroll
1. bottomRef 를 따로 선언하여 쿼리 데이터중 가장 마지막 요소에게 할당
2. 옵저버를 통해 inview 이면 fetchNextPage() 실행
*** 트러블 슈팅:
- 이상하게 클라이언트가 동작(이벤트 발생 같은)이후에야 해당 inView가 관찰됨
- page.tsx에서 'use client'를 선언하는 것이 클라이언트 렌더링 버그가 생기나 해서 단위 컴포넌트로 쪼갠 후 그 안에서 클라이언트 렌더링을 명시해봄 -> 이전과 똑같음
- next.js만의 무슨 버그인건가? -> 임시 페이지 생성 후 관찰 했을 떄는 잘됨
- 그렇다면 ref 할당 시점이 늦어지는건가 싶어서 하이드레이션 지연 시켜질만한 요소들 찾아봄 -> useQuery 훅 동작을 제외했더니 관찰 잘 됨 -> 근데 이 훅 사용자체가 렌더링에 문제를 만들어 ref 할당이 늦어지고 inView 관찰이 안되는건 말이 안됨 =====> status에 따라 아래와 같이 코드를 짰을 때 관찰이 안됐던 거였음 (왜냐면 아직 서버에 직접 연결안 한 상태라 껄껄)
{status === "pending" ? ( <CategoryList.skeleton /> ) : ( <CategoryList bottomRef={bottomRef} categoryType={(category as "popup") || "exhibition"} /> )}
- 마지막 카드가 시야에 들어오면 fetchNextPage를 실행시키게 짤려는데 관찰되는 마지막 카드가 처음에 할당된 ref로 고정박혀짐
- 화면에 표출되는 카드에 useRef로 할당된 ref값을 넘겨준 뒤 이것이 시야에 들어오면 data 패칭이 일어나서 카드들은 증가할 것. bottomRef를 length-1 === idx인 카드에 할당해서 리렌더링시 자동으로 bottomRef가 추가로 할당된 마지막 카드를 가르킬 것이라 생각함. --> useRef 특성이 리렌더링이 일어나더라도 값이 변하지 않는다는(ReadOnly니까..) 특성을 잊은 잘못
- 이것에 대책으로 카드 리스트 바닥에 heigth 1인 투명한 박스에 bottomRef를 줘서 해당 박스가 시야에 들어오면 fetchNextPage가 일어나도록 변경
import { useCallback, useEffect, useRef, useState } from "react"; type IUseInfiniteScroll = { loadMoreFunC: () => void; // useInfiniteQuery의 fetchNextPage shouldMore: boolean; // useInfiniteQuery의 hasNextPage }; const useInfiniteScroll = ({ shouldMore, loadMoreFunC, }: IUseInfiniteScroll) => { const bottomRef = useRef<HTMLInputElement>(null); const [inView, setInView] = useState(false); const handleObserver = useCallback((entries: IntersectionObserverEntry[]) => { const target = entries[0]; setInView(target.isIntersecting); }, []); useEffect(() => { const ob = new IntersectionObserver(handleObserver, { threshold: 1 }); if (bottomRef.current) ob.observe(bottomRef.current); return () => { if (bottomRef.current) ob.unobserve(bottomRef.current); }; // obRef.current. }, [bottomRef]); useEffect(() => { if (inView && shouldMore) { loadMoreFunC(); } }, [inView, shouldMore]); return { inView, bottomRef }; }; export default useInfiniteScroll;
'FrontEnd > React.js' 카테고리의 다른 글
[React] 카카오 api를 통해 주소를 좌표로 변환해보기 (undefinded Geocorder) (1) 2024.09.06 react-day-picker 에서 한글화 시키기 (feat. date-fns) (1) 2024.09.04 webpack과 babel (feat. CRA) (1) 2024.01.11 [React] 기술 면접 대비 리액트 용어 모음 (0) 2023.11.06 아임포트 결제 로직 (0) 2023.08.23 - data: