현재 진행하고 있는 프로젝트에 하객들이 실시간으로 📸사진과 💬축하 메시지를 남길 수 있는 포토톡 기능이 있다.
포토톡 데이터를 보여줄 때React Query
와Intersection Observer API
를 사용하여 무한스크롤을 구현했는데, 왜 사용했는지 그리고 어떻게 구현했는지 기록하고자 한다.
사용자에게 많은 양의 정보를 보여줄 수 있는 방법에는 크게 2가지가 있다. 각 기법을 어떤 상황에 사용해야 하는지부터 알아보자.
특정 기준으로 정렬된 콘텐츠를 여러 페이지로 나눠서 페이지별로 제공하는 UX다.
사용자가 스크롤을 내릴때 다음 데이터를 자동으로 불러오는 UX다.
포토톡은 (1)이미지 기반이고, (2)하객들이 부담 없이 스크롤하며 보는 UI가 목적이므로 무한스크롤을 적용하기로 했다.
먼저 무한스크롤이 동작하는 원리를 살펴보고, 직접 구현한 예제로 설명하려고 한다.
무한스크롤은 다음과 같이 3가지 과정을 반복한다.
(1) 스크롤 위치 감지
(2) 조건을 만족하면, 다음 페이지 요청
(3) 기존 리스트에 추가 렌더링
여기서 조건이란, 한 페이지에 보여주는 콘텐츠 갯수를 말한다.
이를 위해, React에서는 보통 IntersectionObserver
와 React Query
를 함께 사용한다.
브라우저에서 제공하는 API로, 어떤 요소가 뷰포트에 보이는 순간을 감지할 수 있다.
React Application에서 서버의 상태를 불러오고, 캐싱하며, 지속적으로 동기화하고 업데이트 하는 작업을 도와주는 라이브러리이다.
React Query는 다양한 UI에 유연하게 적용할 수 있도록 useQueries
, useInfiniteQuery
와 같은 Hook들을 제공하는데, useInfiniteQuery
는 무한스크롤을 구현할 때 사용할 수 있다.
const {
fetchNextPage,
fetchPreviousPage,
hasNextPage,
hasPreviousPage,
isFetchingNextPage,
isFetchingPreviousPage,
...result
} = useInfiniteQuery({
queryKey,
queryFn: ({ pageParam = 1 }) => fetchPage(pageParam),
...options,
getNextPageParam: (lastPage, allPages) => lastPage.nextCursor,
getPreviousPageParam: (firstPage, allPages) => firstPage.prevCursor,
})
export const useInfinitePhototalkByQuery = <T, ItemType>({
queryFn, // 서버에서 데이터를 가져오는 함수
extractItems, // 가져온 데이터에서 실제 항목만 뽑는 함수
getHasMore, // 다음 페이지가 있는지 판단하는 함수
initialPage = 1,
enabled = true,
}: useInfinitePhototalkByQueryProps<T, ItemType>) => {
...
};
page
: 지금 몇 번째 페이지인지items
: 지금까지 불러온 전체 데이터 목록hasMore
: 더 가져올 데이터가 있는지 여부observeRef
: 스크롤 감지용 div를 가리키는 변수다음 페이지 데이터를 불러오고 기존 리스트에 추가하는 함수다.
hasMore
값을 갱신해서 더 불러올 게 있는지 판단page
값을 증가isFetching
상태를 사용const loadMore = async () => {
if (!hasMore) return;
const res = await queryFn(page);
const newItems = extractItems(res);
setItems([...items, ...newItems]);
setPage(page + 1);
};
처음부터 데이터를 다시 불러올 때 사용된다.
const refetch = async () => {
const res = await queryFn(1);
setItems(extractItems(res));
setPage(2);
};
첫 로딩 시 화면이 너무 짧아 빈 공간이 생기는 걸 방지한다.
refetch()
로 초기화한 후loadMore()
를 호출하며 데이터를 더 불러온다const fetchUntilFull = async () => {
await refetch();
while (hasMore) {
const addedLength = await loadMore();
if (addedLength === 0) break;
}
};
useEffect(() => {
const target = observeRef.current;
const observer = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) loadMore();
}, { threshold: 0.2 });
if (target) observer.observe(target);
return () => observer.unobserve(target);
}, []);
(참고)
카카오페이 프론트엔드 개발자들이 React Query를 선택한 이유
React-Query(tanstack query v5)로 만들어보는 무한스크롤
📲 React-query 사용하는 이유, useInfiniteQuery 로 무한 스크롤 구현하기 (+ react-infinite-scroller)