푸디로그에서 식당 리뷰를 보여주는 피드 목록을 무한 스크롤로 어떻게 구현하였는지 분석해보았다.
무한스크롤
Tanstack Query를 사용해서 데이터 페칭하고, react-infinite-scroller를 사용해서 페이지를 스크롤할 때 추가 데이터를 불러온다.
다음 코드는 푸디로그 피드 목록을 불러오는 쿼리를 useFeedListQuery 커스텀 훅으로 래핑한 코드.
import { RestaurantCategory } from "@/src/types/restaurant";
import { APIFeedResponse } from "@@types/apiTypes";
import { getFeedList, getFeedListByUserId } from "@services/feed";
import { useInfiniteQuery } from "@tanstack/react-query";
interface useFeedListQueryProps {
userId?: number;
singleFeedId?: number;
category?: RestaurantCategory;
}
const useFeedListQuery = ({ userId, singleFeedId, category }: useFeedListQueryProps) => {
return useInfiniteQuery(
["feedList", userId, category],
async ({ pageParam = 0 }) => {
let response;
if (userId) {
response = await getFeedListByUserId(userId, pageParam);
} else {
response = await getFeedList(pageParam, category);
}
const apiResponse = response.data;
return apiResponse;
},
{
getNextPageParam: (lastPage: APIFeedResponse) => {
const lastFeed = lastPage.response.content.at(-1);
if (lastPage?.response?.content?.length <= 15) return undefined;
return lastFeed?.feed.feedId;
},
enabled: !singleFeedId || !userId,
}
);
};
export default useFeedListQuery;
queryKey
기본적으로 쿼리 키에 따라 쿼리 캐싱을 관리한다.
쿼리 키에 종속 변수를 추가하면 쿼리가 독립적으로 캐시되고 변수가 변경될 때마다 쿼리가 자동으로 다시 페치된다.
Query Functions: (context: QueryFunctionContext) => Promise
프로미스를 반환하는 함수. 데이터를 반환하거나 오류 반환한다.
getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) => TPageParam | undefined | null
다음 페이지를 가져올 파라미터를 반환해서 계속 다음 페이지를 불러올 수 있도록 한다.
위 예시 코드에선 lastPage 길이가 15를 초과하면 lastFeed의 FeedId를 반환하고, 길이 15이하면 불러올 다음 페이지가 없기 때문에 undefind 반환한다.
반환되는 값에 따라 hasNextPage 값이 boolean으로 반환된다.
다음 코드에서 useFeedListQuery를 호출하여 데이터를 페칭하고, InfiniteScroll 컴포넌트를 사용해 무한 스크롤을 구현한다. data.pages를 순회하며 피드 데이터를 렌더링한다.
"use client";
import { Fragment, useEffect, useRef, useState } from "react";
import { getSingleFeed } from "@services/feed";
import InfiniteScroll from "react-infinite-scroller";
import Feed from "@components/Feed/Feed";
import { Content } from "@@types/feed";
import useFeedListQuery from "@hooks/queries/useFeedListQuery";
interface FeedsProps {
id?: number;
startingFeedId?: number;
singleFeedId?: number;
}
const Feeds = ({ id, startingFeedId, singleFeedId }: FeedsProps) => {
const [singleFeedData, setSingleFeedData] = useState<Content | null>(null);
const feedRef = useRef<{ [key: number]: HTMLDivElement | null }>({});
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useFeedListQuery({ userId: id, singleFeedId });
return (
<div className="flex flex-col pt-5 max-w-[640px] w-full mx-auto">
<InfiniteScroll pageStart={0} loadMore={loadMore} hasMore={hasNextPage && !isFetchingNextPage}>
{(data?.pages || []).map((page, index) => {
console;
if (!Array.isArray(page.response?.content)) {
return null;
}
return (
<Fragment key={index}>
{page?.response.content.map((feedData: Content, index) => {
const { feed } = feedData;
const hasFeedId = feed?.feedId !== undefined;
const Key = hasFeedId ? feed.feedId : index;
return (
<div
key={Key}
ref={(el) => {
if (hasFeedId) {
feedRef.current[feed.feedId] = el;
}
}}
>
<Feed key={Key} feedData={feedData} userId={id} />
</div>
);
})}
</Fragment>
);
})}
</InfiniteScroll>
)}
</div>
};
export default Feeds;
react-infinite-scroller 라이브러리의 InfiniteScroll 컴포넌트는 스크롤 이벤트를 감지하여 스크롤이 뷰포트 끝에 도달하면 loadMore 함수를 호출하여 추가 데이터를 로드하는 기능을 제공한다.
<InfiniteScroll pageStart={0} loadMore={loadMore} hasMore={hasNextPage && !isFetchingNextPage}>
hasMore={hasNextPage && !isFetchingNextPage}