NEXTJS_최종프로젝트_DREAMCARD_후기페이지_무한스크롤 구현

정소현·2024년 10월 23일
0

팀프로젝트

목록 보기
26/50

🌟 구현 계획

useInfiniteQuery을 이용해 구현

☘️ 선정이유

useInfiniteQuery는 캐싱, 동기화, refetch 등의 복잡한 네트워크 로직을 처리하며, 페이지 파라미터를 자동으로 처리하고, 'fetchNextPage' 함수를 호출해 다음 페이지 데이터를 가져올 수 있다.

☘️ useInfiniteQuery 훅 사용 인터페이스

useInfiniteQuery(['쿼리명'], ({ pageParam = defaultUrl}) => 데이터함수(pageParam))

☘️ useInfiniteQuery가 동작하기 위해서 넘겨받을 api에 필요한 값 3가지

  • 현재 페이지
  • 다음페이지 판단여부
  • 데이터

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;

🔥 Trouble Shooting

🥵 문제점
: 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

0개의 댓글