useInfiniteQuery을 이용해 구현
useInfiniteQuery는 캐싱, 동기화, refetch 등의 복잡한 네트워크 로직을 처리하며, 페이지 파라미터를 자동으로 처리하고, 'fetchNextPage' 함수를 호출해 다음 페이지 데이터를 가져올 수 있다.
useInfiniteQuery(['쿼리명'], ({ pageParam = defaultUrl}) => 데이터함수(pageParam))
data는 이제 무한 쿼리 데이터를 포함하는 객체
data.pages: 가져온 페이지를 담은 배열
data.pageParams: 페이지를 가져오는 데 사용된 페이지 매개 변수를 담은 배열
/* useInfiniteQuery 기본 구조 */
const {
data: reviewsData, // 렌더링 할 데이터
fetchNextPage, // 다음페이지 실행 함수
fetchPreviousPage, // 이전 페이지 가져오기
hasNextPage, // 다음페이지 판단 여부
hasPreviousPage, // 이전 페이지가 있는지 확인
isFetchingNextPage, // 다음페이지 로딩중 판단
isFetchingPreviousPage, // 이전 페이지를 가져오는 중인지 확인
isLoading,
isError,
error,
} = useInfiniteQuery({
queryKey,
queryFn: ({ pageParam = 1 }) => fetchPage(pageParam),
...options,
getNextPageParam: (lastPage, allPages) => lastPage.nextCursor,
getPreviousPageParam: (firstPage, allPages) => firstPage.prevCursor,
})
우선 Review를 supabase에서 가져와야하기 때문에 가져오는 로직을 작성해주었다.
import { Review } from '@/types/review.types';
import browserClient from './supabase/client';
type ReviewProps = {
pageParam: number;
row: number;
};
export const getReview = async ({ pageParam = 0, row }: ReviewProps): Promise<Review[]> => {
try {
const { data, error } = await browserClient
.from('reviews')
.select('*')
.range(pageParam, pageParam + row - 1);
if (error) {
console.error(error);
throw new Error('데이터를 가져오는 데 문제가 발생했습니다.');
}
console.log(data);
return data
} catch (error) {
console.error(error);
throw error;
}
};
review/page.tsx에서 클라이언트 사이드에서 실시간으로 스크린의 끝에 닿을 때 정보를 불러와야하기 때문에 "use client"을 작성해주었고
useInfiniteQuery를 활용하여 queryKey와 queryFn을 지정하여 데이터를 우선 불러올 수 있도록 해주었다. initialPageparam과 getNextPageParam을 설정해주어 다음페이지를 불러와야하는지 여부를 설정해주었다.
useEffect()내에서 스크롤에 따라 다음페이지를 불러올 수 있도록 handleScroll함수를 만들어 주었다.
useEffect(() => {
const handleScroll = () => {
const { scrollTop, scrollHeight, clientHeight } = document.documentElement;
if (scrollTop + clientHeight >= scrollHeight - 10 && hasNextPage && !isFetchingNextPage) {
fetchNextPage();
}
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, [hasNextPage, isFetchingNextPage, fetchNextPage]);
pages마다 데이터를 불러오기 때문에 가독성이 좋게 return값을 적어주기 위해 reviews를 선언해주었다.
이번에 flatMap을 처음 사용해보았는데 일반적으로 많이 써왔던 map과의 차이점을 알게되었다.
flatMap은 map과 유사하지만 return 값이 컬렉션이 아니라 평면화 된다.
ex) 깊이가 2인 배열들도 평평하게 꺼내준다라는 느낌
console.log([1,[3],[2]].map(ele => ele)) // [1,[3],[2]]
console.log([1],[3],[2]].flatmap(ele=> ele)) // [1,3,2]
return 값에서 받아온 정보를 바탕으로 레이아웃을 그려주었다.
return (
<div>
{reviews.map((review) => (
<div
key={review.id}
className='flex border border-solid mb-4 p-4'
>
{Array.isArray(review.image_url) && review.image_url.length > 0 ? (
review.image_url.map((imageUrl, index) => (
<Image
key={`${index}+${imageUrl}`}
src={imageUrl}
alt={`후기 이미지 ${index + 1}`}
width={100}
height={100}
style={{ width: 'auto' }}
priority
/>
))
) : (
<Image
src={defaultImg}
alt='기본 이미지'
width={100}
height={100}
style={{ width: 'auto' }}
priority
/>
)}
<div className='flex flex-col ml-4'>
<div className='flex justify-between'>
<h3>{review.user_name}</h3>
<p>{review.created_at}</p>
</div>
<p>{review.content}</p>
</div>
</div>
))}
</div>
);
};
const reviews = reviewsData?.pages.flatMap((page) => page) || [];
'use client';
import { getReview } from '@/utils/getReview';
import { useInfiniteQuery } from '@tanstack/react-query';
import { useEffect } from 'react';
import ReviewImage from '@/components/review/ReviewImage';
import { Review } from '@/types/review.types';
import ReviewCard from '@/components/review/ReviewCard';
const ReviewPage = () => {
const row = 10;
const {
data: reviewsData,
isLoading,
error,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useInfiniteQuery<Review[]>({
queryKey: ['reviews'],
queryFn: async ({ pageParam }) => await getReview({ pageParam: pageParam as number, row }),
initialPageParam: 0,
getNextPageParam: (lastPage, allPages) => {
return lastPage.length === row ? allPages.length * row : undefined;
},
});
useEffect(() => {
const handleScroll = () => {
const { scrollTop, scrollHeight, clientHeight } = document.documentElement;
if (scrollTop + clientHeight >= scrollHeight - 10 && hasNextPage && !isFetchingNextPage) {
fetchNextPage();
}
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, [hasNextPage, isFetchingNextPage, fetchNextPage]);
if (isLoading) return <div>로딩 중...</div>;
if (error) return <div>에러가 발생했습니다: {error.message}. 다시 시도해 주세요.</div>;
const reviews = reviewsData?.pages.flatMap((page) => page) || [];
return (
<div className='flex flex-col w-full'>
<ReviewImage reviews={reviews} />
<h1>후기</h1>
<div>
<ReviewCard reviews={reviews} />
</div>
{isFetchingNextPage && <div>더 불러오는 중...</div>}
</div>
);
};
export default ReviewPage;
🥵 문제점
: supabase에서 리뷰를 가져오는 로직에서 type을 설정해주었는데 반환해주는 부분에서 계속하여 unknown관련한 에러가 발생하였다.
✨ 해결방법
: as 단언을 연쇄적으로 이용하여 unknown관련한 오류가 해결될 수 있도록 설정해주었다. 간단한 오류였으나 연쇄적으로 타입오류가 나 해결하는데 꽤 많은 시간을 들였다.
import { Review } from '@/types/review.types';
import browserClient from './supabase/client';
type ReviewProps = {
pageParam: number;
row: number;
};
export const getReview = async ({ pageParam = 0, row }: ReviewProps): Promise<Review[]> => {
try {
const { data, error } = await browserClient
.from('reviews')
.select('*')
.range(pageParam, pageParam + row - 1);
if (error) {
console.error(error);
throw new Error('데이터를 가져오는 데 문제가 발생했습니다.');
}
console.log(data);
return data as unknown as Review[];
} catch (error) {
console.error(error);
throw error;
}
};
참고 사이트
https://velog.io/@ahn0min/map-flatMap-%EC%9D%98-%EC%B0%A8%EC%9D%B4