Space
article thumbnail
반응형

Before you learn

🔗 [NextJS] pagination


[NextJS] Infinite Scroll (무한 스크롤)

영상

반응형

Why Use?

무한스크롤과 페이지네이션의 사용 이유는 간단하다.

 

페이지를 들어갈 때마다 서버에서 해당 페이지에 대한 모든 데이터 리스트들을 받아서 보여준다면,

데이터가 많으면 많을수록 서버가 한 번에 처리해야 되는 데이터 양이 많이 지므로 서버가 더욱 좋아야 할 것이며,

화면을 보여주는데 시간도 오래 걸리고, 그러면 그만큼 사용자 경험도 좋지 않으며,

사용자 경험이 좋지 않으면 유저 이탈수가 증가할 것이고, 이탈수가 증가하면 그만큼 회사의 손해이기 때문이다.

 

흡사 캐시를 사용하는 이유도 이러하다. 캐시도 기회가 되면 한 번 알아두면 좋겠다. 


동작 방식

페이지네이션을 이해하고 있으면 더욱 쉽게 무한 스크롤에 대해 이해할 수 있을 것이다!

 

페이지네이션 : 페이지 이동 버튼을 눌렸을 때 동작하여 데이터를 받아온다.

 

무한스크롤 : 해당 버튼이나 아이템이 화면에 보였을 때, 또는 특정 위치에 화면이 도달했을 때 동작하여 데이터를 받아온다.


Code

이번 무한 스크롤은 observer를 사용하여 구현해 보겠다.

 

화면에 접속했을 때 처음으로 상품 리스트들을 보여준다.

(필자는 Prisma를 사용하였다.)

// add/(tabs)/life/page.tsx

async function getInitialProductList() {
  // 처음으로 product 페이지 띄울 때
  const initialProductList = await db.product.findMany({
    select: {
      title: true,
      price: true,
      created_at: true,
      photo: true,
      id: true,
    },
    take: 10, // take에 적힌 수 만큼 상품 리스트 가져옴
    orderBy: {
      created_at: "desc", // 최신 순으로 정렬함
    },
  });
  return initialProductList;
}

// getInitialProducts의 타입 입력 (interface로 해도 됌)
export type InitialProductList = Prisma.PromiseReturnType<
  typeof getInitialProductList
>;

export default async function Products() {
  const initialProductList = await initialProductList();
  return (
    <div>
      <ProductList initialProductList={initialProductList} />
      ...
    </div>
  );
}

상품 리스트들 맨 아래에 버튼을 만들고, 해당 버튼이 100% 다 보이면 새로운 데이터를 가져오도록 한다.

<ProductSimpleInfo> 컴포넌트는 무한 스크롤로 인해 새로 받아온 데이터들을 펼쳐서 보여주기에 굳이 코드를 작성하진 않겠다.

// components/home-page/product-list.tsx

"use client";

import { InitialProductList } from "@/app/(tabs)/home/page";
import ProductSimpleInfo from "./product-simple-info";
import { useEffect, useRef, useState } from "react";
import { getMoreProductList } from "@/app/(tabs)/home/actions";

interface ProductListProps {
  initialProductList: InitialProductList;
}

export default function ProductList({ initialProductList }: ProductListProps) {
  const [productList, setProductList] = useState(initialProductList);
  const [isLoading, setIsLoading] = useState(false);
  const [page, setPage] = useState(0);
  const [isLastPage, setIsLastPage] = useState(false);
  const trigger = useRef<HTMLSpanElement>(null);

  useEffect(() => {
    const observer = new IntersectionObserver(
      async (
        entries: IntersectionObserverEntry[],
        observer: IntersectionObserver
      ) => {
        const element = entries[0];
        // 화면에 버튼이 보이면
        if (element.isIntersecting && trigger.current) {
          // 트리거를 관찰하지 않고,
          observer.unobserve(trigger.current);
          setIsLoading(true);
          // 새로운 상품 데이터를 가져온다.
          const newProductList = await getMoreProductList(page + 1);
          // 마지막 상품 데이터가 아니라면 (= 더 가져올 상품 데이터가 있다면), 데이터를 추가시킨다.
          if (newProductList.length !== 0) {
            setProductList([...productList, ...newProductList]);
            // page의 값이 변경되므로, useEffect의 dependency로 인해 useEffect가 다시 실행된다.
            // 그로 인해 다시 해당 트리거를 관찰(observe)한다.
            setPage(page + 1);
          }

          // 마지막 상품 데이터라면 (= 더 가져올 상품 데이터가 없다면),
          else {
            setIsLastPage(true);
          }
          setIsLoading(false);
        }
      },
      {
        threshold: 1.0, // 해당 버튼이 전체(100%) 다보여야 동작한다. (0.5 === 50%)
      }
    );
    // trigger가 null(초기값)이 아니면, 관찰한다.
    if (trigger.current) {
      observer.observe(trigger.current);
    }
    // 유저가 해당 페이지를 벗어난다면, 트리거를 더이상 관찰하지 않는다. 메모리 누수 방지
    return () => {
      observer.disconnect();
    };
  }, [page]);

  return (
    <div className="p-5 flex flex-col gap-5 mb-20">
      {productList.map((product) => (
        <ProductSimpleInfo key={product.id} {...product} />
      ))}
      {!isLastPage ? (
        <span
          ref={trigger}
          className="text-sm font-semibold bg-orange-500 w-fit mx-auto px-3 py-2 rounded-md hover:opacity-90 active:scale-95"
        >
          {isLoading ? "Loading..." : "Load more"}
        </span>
      ) : null}
    </div>
  );
}
반응형
profile

Space

@Space_zero

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!