본문 바로가기
Next.js

[Next.js] 페이지 기반 페이지네이션 구현하기 (react-query)

by 상똥프 2024. 7. 4.

목표
- 리액트 쿼리를 사용해 재사용성이 높은 페이지네이션 훅을 만든다
- 한 페이지에 보여줄 데이터 개수를 정할 수 있다
- 페이지 번호를 클릭하면 해당 페이지로 이동한다
- '이전', '이후' 버튼을 사용해 다른 데이터를 렌더링할 수 있다
- 마지막 페이지일 경우, 이후페이지를 클릭할 수 없고 첫 페이지일 경우 이전 페이지를 클릭할 수 없다

 
목차
1. 초기 설정

2. 데이터 불러오기 (렌더링하기)
3. 페이지 버튼 구현하기 - 훅 구현
4. 데이터 가져오기 및 이전페이지, 이후페이지 구현하기 - 훅 구현
5. 전체 코드


[1. 초기 설정]

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

// src/types/comment.type.ts

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

 
3. api연결
(1) getComment
- 페이지값(page)과 한 페이지에 보여줄 댓글 개수(limit)를 통해 댓글을 조건에 맞게 가져온다
- 반환 타입은 앞서 설정한 DComment의 배열 형태이다
(2) getTotalComments
- 전체 댓글의 개수를 가져온다
- 페이지 번호 버튼을 구현하는데 사용된다

// src/api/comment.api.ts

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

  return commentList.data;
};

export const getTotalComments = async (): Promise<number> => {
  const totalComments = await server.get("comment/count");
  return totalComments.data;
};

[2. 데이터 가져오기]

1. 데이터 가져오기
- 현재 페이지(currentPage)와 페이지당 데이터 개수(limit) 그리고 배열 pages를 인자로 전달받는다
- useQuery를 사용해 서버에서 데이터를 가져온다

// src/hooks/comment/useQuery.getComments.tsx

import { getComment } from "@/api/comment/comment.api";
import { useQuery } from "@tanstack/react-query";

export default function useQueryGetCommentsPerPage(
  currentPage: number,
  limit: number
) {
  const { data } = useQuery({
    queryKey: ["data", currentPage],
    queryFn: () => getComment(currentPage, limit),
  });

  return {
    data
  };
}

 

2. 데이터 렌더링하기

- map을 사용해 데이터를 하나씩 보여준다

// src/app/page.tsx

import useQueryGetCommentsPerPage from "@/hooks/comment/useQuery.getComments";

  
  ...
  const { data } = useQueryGetCommentsPerPage(
    currentPage,
    LIMIT_PER_PAGE,
  );
  
  return (
    <div>
      <div>Comments</div>
      <div>
        <strong>번호</strong>
        <strong>닉네임</strong>
        <strong>내용</strong>
      </div>
      {data?.map((comment: DComment, index: number) => (
        <div key={index}>
          <p>{comment.id}</p>
          <p>{comment.nickname}</p>
          <p>{comment.content}</p>
        </div>
      ))}
        ...
    </div>
  );
}

 

- 결과는 아래와 같다


[3. 페이지 버튼 구현하기 - 훅 구현]

1. 전체 데이터의 개수 가져오기
- 한 페이지에 렌더링할 데이터 개수(limit)에 따라 페이지 버튼의 개수가 달라진다
- 버튼 개수를 page로 설정한다
- 전체 데이터 개수를 limit으로 나눴을 때 나머지가 생긴다면, 몫에 1을 더해준다
- page 크기의 배열 pages를 만들어주고, 값을 1부터 page까지 매겨준다

import { getTotalComments } from "@/api/comment/comment.api";
import { useQuery } from "@tanstack/react-query";

export default function useQueryGetTotalComments(limit: number) {
  const { data } = useQuery({
    queryKey: ["data"],
    queryFn: () => getTotalComments(),
  });

  const page =
    data &&
    (data % limit > 0
      ? Math.floor(data / limit) + 1
      : Math.floor(data / limit));

  const pages = [page];
  if (page) {
    for (let i = 0; i < page; i++) {
      pages[i] = i + 1;
    }
  }

  return {
    pages,
  };
}

 
2. 화면에 페이지 버튼 구현
(1) LIMIT_PER_PAGE를 한 페이지에 렌더링할 데이터의 개수로 설정한다.
(2) currentPage 관리
- useState로 현재 페이지를 관리한다
- 초기값은 1로 설정한다
(3) onHandlePage : 페이지 설정 함수
- 페이지 버튼을 클릭하면 currentPage를 해당 페이지로 설정하는 함수를 구현한다

(4) 배열 pages를 불러오고 화면에 map으로 하나씩 구현

// src/app/page.tsx

"use client";
import { useState } from "react";

const LIMIT_PER_PAGE = 10;

export default function Home() {
  const [currentPage, setCurrentPage] = useState(1);

  const onHandlePage = (page: number) => {
    setCurrentPage(page);
  };
  
  return (
    <div>
    ...
      <div>
        {pages?.map((page: number | undefined, index: number) => (
          <button
            key={index}
            title="page number"
            type="button"
            onClick={() => onHandlePage(page || 1)}
          >
            {page}
          </button>
        ))}
      </div>
    </div>
  );
}

- 아래와 같이 나타남 (데이터 개수에 따라 번호는 달라짐)


[4. 데이터 가져오기 및 이전페이지, 이후페이지 구현하기 - 훅 구현]

1. 이전/이후 페이지 구현
- 이전페이지, 이후페이지를 오갈 수 없는 조건을 설정한다
(1) isPrevDisabled
- 이전페이지가 존재하지 않는 경우
- currentPage가 1인 경우
(2) isNextDisabled
- 이후페이지가 존재하지 않는 경우
- 현재 페이지가 pages 배열의 길이와 같은 경우

// src/app/page.tsx

  ...
  
  const isPrevDisabled = currentPage === 1;
  const isNextDisabled = pages && currentPage === pages.length;
  
  ...
}

 
2. 화면에 이전, 이후 페이지 버튼 구현
(1) 이전 페이지 버튼 : onHandlePrevPage
- isPrevDisabled가 아닌 경우에 currentPage-1
(2) 이후 페이지 버튼 : onHandleNextPage
- isNextDisabled가 아닌 경우에 currentPage+1

// src/app/page.tsx
  
  ...
  const onHandlePrevPage = () => {
    if (!isPrevDisabled) setCurrentPage(currentPage - 1);
  };

  const onHandleNextPage = () => {
    if (!isNextDisabled) setCurrentPage(currentPage + 1);
  };

  const onHandlePage = (page: number) => {
    setCurrentPage(page);
  };

  return (
    <div>
    ...
      <div>
        <button
          title="prev"
          type="button"
          onClick={onHandlePrevPage}
        >
          이전
        </button>
        ...
        <button
          title="next"
          type="button"
          onClick={onHandleNextPage}
        >
          이후
        </button>
      </div>
    </div>
  );
}

- 아래와 같이 나타난다

 


[5. 전체 코드]

1. 데이터 개수를 가져오는 코드 : useQuery.getTotalComments.tsx

import { getTotalComments } from "@/api/comment/comment.api";
import { useQuery } from "@tanstack/react-query";

export default function useQueryGetTotalComments(limit: number) {
  const { data } = useQuery({
    queryKey: ["data"],
    queryFn: () => getTotalComments(),
  });

  const page =
    data &&
    (data % limit > 0
      ? Math.floor(data / limit) + 1
      : Math.floor(data / limit));

  const pages = [page];
  if (page) {
    for (let i = 0; i < page; i++) {
      pages[i] = i + 1;
    }
  }

  return {
    pages,
  };
}

 
2. 데이터를 가져오는 코드 : useQuery.getComments.tsx

import { getComment } from "@/api/comment/comment.api";
import { useQuery } from "@tanstack/react-query";

export default function useQueryGetCommentsPerPage(
  currentPage: number,
  limit: number
) {
  const { data } = useQuery({
    queryKey: ["data", currentPage],
    queryFn: () => getComment(currentPage, limit),
  });

  return {
    data,
  };
}

 
3. 홈페이지 코드

"use client";
import useQueryGetCommentsPerPage from "@/hooks/comment/useQuery.getComments";
import useQueryGetTotalComments from "@/hooks/comment/useQuery.getTotalComments";
import { DComment } from "@/types/comment.type";
import { useState } from "react";

const LIMIT_PER_PAGE = 10;

export default function Home() {
  const { pages } = useQueryGetTotalComments(LIMIT_PER_PAGE);
  const [currentPage, setCurrentPage] = useState(1);
  const { data } = useQueryGetCommentsPerPage(currentPage, LIMIT_PER_PAGE);

  const isPrevDisabled = currentPage === 1;
  const isNextDisabled = pages && currentPage === pages.length;

  const onHandlePrevPage = () => {
    if (!isPrevDisabled) setCurrentPage(currentPage - 1);
  };

  const onHandleNextPage = () => {
    if (!isNextDisabled) setCurrentPage(currentPage + 1);
  };

  const onHandlePage = (page: number) => {
    setCurrentPage(page);
  };

  return (
    <div>
      <div>Comments</div>
      <div>
        <p>번호</p>
        <p>닉네임</p>
        <p>내용</p>
      </div>
      {data?.map((comment: DComment, index: number) => (
        <div key={index}>
          <p>{comment.id}</p>
          <p>{comment.nickname}</p>
          <p>{comment.content}</p>
        </div>
      ))}
      <div>
        <button
          title="prev"
          type="button"
          onClick={onHandlePrevPage}>
          이전
        </button>
        {pages?.map((page: number | undefined, index: number) => (
          <button
            key={index}
            title="page number"
            type="button"
            onClick={() => onHandlePage(page || 1)}
          >
            {page}
          </button>
        ))}
        <button
          title="next"
          type="button"
          onClick={onHandleNextPage}
        >
          이후
        </button>
      </div>
    </div>
  );
}