문제 원인


렌더링 데이터의 마지막 인덱스 위치에 IO 관찰 요소를 표시하는 방식은 무한 렌더링 문제가 발생할 수 있다.

데이터가 1~2개 밖에 없어서 관찰 요소가 항상 화면에 들어온다면 ➊fetchNextPage 실행(React Query 훅이 반환하는 함수) → ➋기존 데이터 반환 → ➌렌더 → ➍IO 화면에 들어옴 → ➊fetchNextPage 실행 → 반복…

위 과정을 계속 반복하면서 무한 렌더링하는 문제가 발생한다. Query Key가 바뀌지 않아서 데이터 fetch 없이 캐싱한 데이터를 사용하지만 무한 렌더링 때문에 어플리케이션이 정상적으로 작동하지 않는다. 데이터가 아예 없다면 IO 관찰 대상 요소도 렌더링 하지 않으므로 문제가 없는것처럼 보이기 때문에 주의해야될 버그.

// queryKey 및 queryFn 생략
const { data, fetchNextPage } = useInfiniteQuery(queryKey, queryFn);

const { setObservationTarget } = useIntersectionObserver({
  onIntersect: ([{ isIntersecting }]) => isIntersecting && fetchNextPage(),
});

const renderData = data?.pages.flatMap(({ data }) => data.orders);

return (
	<div>
		{renderData?.map((order, i, { length }) => (
		  <Fragment key={order.order_id}>
		    <OrderListItem data={order} />
		    {i === length - 1 && <div ref={setObservationTarget} />} {/* 문제의 코드 */}
		  </Fragment>
		))}
	</div>
);

해결 방법


위 문제는 IO 관찰 요소 표시 조건을 지정할 때 useInfiniteQuery 훅이 반환하는 hasNextPage를 이용하면 해결할 수 있다. hasNextPage 값은 getNextPageParam(다음 요청시 사용할 pageParam을 지정하는 함수)이 반환하는 값에 따라 달라진다. getNextPageParam 함수가 truthy 값을 반환하면 hasNextPagetrue가 되고 undefined를 반환하면 false가 된다.

const { data, fetchNextPage, isFetching, hasNextPage } = useInfiniteQuery(
  queryKey, // 생략
  queryFn, // 생략
  { getNextPageParam: (lastPage) => lastPage.data.cursor ?? undefined }
	// cursor는 다음 요청시 pageParam으로 사용됨. 값은 'ISO format string' | null
);

// ...

return (
	<div>
		{renderData?.map((order) => (
		  <Fragment key={order.order_id}>
		    <OrderListItem data={order} />
		    {hasNextPage && <div ref={setObservationTarget} />} {/* 수정한 코드 */}
		  </Fragment>
		))}
	</div>
);