본문 바로가기
Next.js

[Next.js] 페이지네이션 구현하기, 무한스크롤 (react-query)

by 상똥프 2024. 7. 4.

목표

- 리액트 쿼리를 사용해 재사용성이 높은 페이지네이션 훅을 만든다
- 한 페이지에 보여줄 데이터 개수를 정할 수 있다

 

목차

1. 초기 설정

2. 데이터 불러오기 (렌더링하기)

3. 무한스크롤 구현하기

4. 전체 코드

 


[1. 초기설정]

0. 예시로 댓글을 렌더링하는 코드를 구현한다
 
1. 서버 코드
- 여기에서 확인할 수 있다
 
2. 데이터 타입

// src/types/comment.type.ts

export type DComment = {
  id: number;
  nickname: string;
  content: string;
};

 
3. api연결
- getComment
- 페이지값(page)과 한 페이지에 보여줄 댓글 개수(limit)를 통해 댓글을 조건에 맞게 가져온다
- 반환 타입은 앞서 설정한 DComment의 배열 형태이다

// src/api/comment/comment.api.ts

import { DComment } from "@/types/comment.type";
import { server } from "..";

export const getComment = async (
  page: number,
  limit: number
): Promise<DComment[]> => {
  const commentList = await server.get("comment", {
    params: { page, limit },
  });

  return commentList.data;
};

 


[2. 데이터 불러오기 (렌더링하기)]

1. useQueryDetDataPerPAge

- 인자로 데이터를 가져오는 함수(fetchData)와 한 페이지에 보여줄 데이터의 수(limit)을 받는다

- useInfiniteQuery를 사용한다

- 현재 페이지(currentPage)에 포함된 데이터의 개수가 limit보다 작으면 마지막 페이지인 것이므로 undefined를 리턴

- 초기 페이지값은 1로 설정

 

// src/hooks/pagination/useQuery.getData.tsx

import { useInfiniteQuery } from "@tanstack/react-query";

interface PaginationProps {
  fetchData: ({ pageParam }: { pageParam?: number }) => Promise<any>;
  limit: number;
}

export default function useQueryGetDataPerPage({
  fetchData,
  limit,
}: PaginationProps) {
  return useInfiniteQuery({
    queryKey: ["data"],
    queryFn: fetchData,
    getNextPageParam: (currentPage, allPages) => {
      if (currentPage.length < limit) return undefined;
      return allPages.length + 1;
    },
    initialPageParam: 1,
  });
}

 

2. 데이터 렌더링하기

 

(1) fetchData 함수

- pageParam을 매개변수로 받아 LIMIT_PER_PAGE 값을 사용하여 getComment 함수 호출

- getComment 함수는 주어진 페이지 번호와 페이지당 항목 수를 기반으로 댓글 데이터를 반환

(2) useQueryGetDataPerPage 훅:

- fetchData 함수와 LIMIT_PER_PAGE를 인자로 전달하여 댓글 데이터를 페이지 단위로 가져옴

- data, fetchNextPage, hasNextPage, isFetchingNextPage 등의 값을 반환합니다.

 

- data : 렌더링하고자 하는 데이터

- fetchNextPage : 다음 페이지(limit개의 데이터) 가져오기

- hasNextPage : 남은 데이터가 있는지 확인

- isFetchingNextPage : 다음 데이터 불러오는 중

(3) 데이터 렌더링

- map과 page를 사용하여 하나씩 렌더링한다

"use client";

import { getComment } from "@/api/comment/comment.api";
import useQueryGetDataPerPage from "@/hooks/comment/useQuery.getComments";
import { DComment } from "@/types/comment.type";

const LIMIT_PER_PAGE = 7;

export default function Home() {
  const fetchData = async ({ pageParam = 1 }) => {
    return await getComment(pageParam, LIMIT_PER_PAGE);
  };

  const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
    useQueryGetDataPerPage({ fetchData, limit: LIMIT_PER_PAGE });

  return (
    <div>
      <div>Comments</div>
      {data?.pages.flatMap((page) =>
        page.map((comment: DComment, index: number) => (
          <div key={index}>
            <p>{comment.id}</p>
            <p>닉네임 : {comment.nickname}</p>
            <p>내용 : {comment.content}</p>
          </div>
        ))
      )}
    </div>
  );
}

- 아래와 같이 나타난다 (아직 무한스크롤 기능을 넣지 않아 limit개의 데이터만 가져옴)


[3. 무한스크롤 구현하기]

 

1. Intersection Observer

- useEffect 훅 내에서 IntersectionObserver를 설정하여 loadMoreRef가 화면에 나타날 때마다 fetchNextPage 함수를 호출

- threshold 값을 1.0으로 설정하여 요소가 100% 화면에 나타날 때 트리거

 

2. 댓글 목록 렌더링

- data?.pages.flatMap을 사용하여 페이지 데이터를 평탄화(flatten), 각 댓글을 렌더링

- DComment 타입을 사용하여 각 댓글의 id, nickname, content를 표시

 

3. 무한 스크롤 로딩 표시

- loadMoreRef가 참조하는 div 요소에 isFetchingNextPage가 true일 때 "Loading..." 텍스트를 표시

 

// src/app/page.tsx

import { useEffect, useRef } from "react";

  ...
  const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
    useQueryGetDataPerPage({ fetchData, limit: LIMIT_PER_PAGE });

  const loadMoreRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const observer = new IntersectionObserver(
      (entries) => {
        if (entries[0].isIntersecting && hasNextPage) {
          fetchNextPage();
        }
      },
      { threshold: 1.0 }
    );

    if (loadMoreRef.current) {
      observer.observe(loadMoreRef.current);
    }

    return () => {
      observer.disconnect();
    };
  }, [hasNextPage, fetchNextPage]);

  return (
    <div>
    ...
      <div ref={loadMoreRef}>{isFetchingNextPage ? "Loading..." : ""}</div>
    </div>
  );
}

 

- 아래와 같이 구현된다


[4. 전체 코드]

1. useQuery.getData.tsx

import { useInfiniteQuery } from "@tanstack/react-query";

interface PaginationProps {
  fetchData: ({ pageParam }: { pageParam?: number }) => Promise<any>;
  limit: number;
}

export default function useQueryGetDataPerPage({
  fetchData,
  limit,
}: PaginationProps) {
  return useInfiniteQuery({
    queryKey: ["data"],
    queryFn: fetchData,
    getNextPageParam: (currentPage, allPages) => {
      if (currentPage.length < limit) return undefined;
      return allPages.length + 1;
    },
    initialPageParam: 1,
  });
}

 

2. page.tsx

"use client";

import { getComment } from "@/api/comment/comment.api";
import useQueryGetDataPerPage from "@/hooks/comment/useQuery.getComments";
import { DComment } from "@/types/comment.type";
import { useEffect, useRef } from "react";

const LIMIT_PER_PAGE = 7;

export default function Home() {
  const fetchData = async ({ pageParam = 1 }) => {
    return await getComment(pageParam, LIMIT_PER_PAGE);
  };

  const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
    useQueryGetDataPerPage({ fetchData, limit: LIMIT_PER_PAGE });

  const loadMoreRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const observer = new IntersectionObserver(
      (entries) => {
        if (entries[0].isIntersecting && hasNextPage) {
          fetchNextPage();
        }
      },
      { threshold: 1.0 }
    );

    if (loadMoreRef.current) {
      observer.observe(loadMoreRef.current);
    }

    return () => {
      observer.disconnect();
    };
  }, [hasNextPage, fetchNextPage]);

  return (
    <div>
      <div>Comments</div>
      {data?.pages.flatMap((page) =>
        page.map((comment: DComment, index: number) => (
          <div key={index}>
            <p>{comment.id}</p>
            <p>닉네임 : {comment.nickname}</p>
            <p>내용 : {comment.content}</p>
          </div>
        ))
      )}
      <div ref={loadMoreRef}>{isFetchingNextPage ? "Loading..." : ""}</div>
    </div>
  );
}