NextJS - 페이지네이션 구현하기

yoon Y·2022년 10월 10일
0

요구사항

  • 한 페이지 당 보여지는 상품의 개수는 10개입니다.
  • 한 번에 보이는 페이지의 갯수는 5개 여야합니다.
  • 페이지 클릭 시 url path가 변경되어야 합니다.
  • 이전 범위 버튼 클릭 시 이전 범위의 마지막 페이지를 보여줘야 합니다.
  • 다음 범위 버튼 클릭 시 다음 범위의 첫 번째 페이지를 보여줘야 합니다.
  • 이전/디음 페이지가 없다면 이전/다음 범위 버튼을 비활성화해야합니다.

필요한 값들

  • 전체 페이지 갯수 (totalPageCount) - (전체 컨텐츠 / 한 페이지에 보여줄 컨텐츠 갯수)내림 한 수
  • 보여지는 페이지 갯수 (limitPageCount)
  • 현재 속한 페이지 숫자 (currentPage) - 현재 머물고 있는 페이지의 url path
  • 핸들러 함수(onChange) - 페이지 클릭 시 해당 페이지로 url이동
			  <Pagination
                totalPageCount={Math.round(allProducts.length / PRODUCTS_LENGTH)}
                limitPageCount={5}
                currentPage={currentPage} 
                onChange={handleChangePage}
              />

전체적인 동작 과정

  1. 페이지 아이템을 클릭하면 해당 페이지 번호를 path로 가진 url로 변경합니다.
  2. 변경된 url의 path를 가져와 Pagination컴포넌트에 전달해(props) 리렌더링되게 합니다.
  • url변경과 관련된 로직은 Pagination컴포넌트와는 관심사가 다르다고 생각해 사용측인 부모 컴포넌트로 위임했습니다.
  • url변경과 변경 감지는 부모 컴포넌트가 하고, 현재의 url path를 Pagination컴포넌트에 props으로 전달해 url이 변경될 때마다 리렌더링되도록 합니다.
// PaginationPage.tsx
// 필요한 부분만 간략화했습니다.

const PaginationPage: NextPage = () => {
  const router = useRouter();
  const { page } = router.query;
  const [products, setProducts] = useState<Product[]>([]);
  const [currentPage, setCurrentPage] = useState(1);
  
  // 컨텐츠 데이터를 불러오는 함수
  const fetchProducts = async (page: number) => {
    ...
  };

 //  페이지 버튼 클릭 시 url 변경
  const handleChangePage = (page: number) => {
    router.push(`pagination?page=${page}`, undefined, { shallow: true, scroll: true });
  };

  useEffect(() => {
    // 페이지 변경 시 
    if (!page) return;
    setCurrentPage(Number(page)); // 현재 페이지 상태 변경 -> Pagination리렌더링
    fetchProducts(Number(page));  // 컨텐츠 데이터 새롭게 불러와 상태 변경 -> ProductList리렌더링
  }, [page]);

  return (
     <Container>
         <ProductList products={products} />
         <Pagination
                totalPageCount={Math.round(allProducts.length / PRODUCTS_LENGTH)}
                limitPageCount={5}
                currentPage={currentPage}
                onChange={handleChangePage}
              />
     </Container>
  );
};

export default PaginationPage;
}

usePagination 훅 구현하기

1. 초기 데이터 생성 및 적용

인자로 받아온 데이터와 range, createPagesGroupList, getCurrentGroupIndex함수를 이용해 초기값을 생성합니다.

  • 모든 페이지를 5개(limitCount)씩 그룹지은 배열을 생성합니다.([1,2,3,4,5], [6,7,8,9,10],[11])
  • 현재 페이지가 속한 그룹의 index를 구합니다. (각 페이지를 직접 접근했을 때의 초기화를 위함)
  • 두 값을 pagesGroupList, currentGroupIndex ref변수에 초기값으로 할당해줍니다. (렌더링과 관련 없으므로 ref사용)
  • 두 ref값들을 이용해 현재 페이지가 속한 그룹 변수를 pages상태에 초기값으로 넣어주어 페이지네이션을 첫 렌더링합니다.
// usePagination.tsx

const range = (size: number, start: number) => {
  return Array(size)
    .fill(start)
    .map((x, y) => x + y);
};

// 모든 페이지를 5개(limitCount)씩 그룹지은 배열 생성하기 위한 함수
const createPagesGroupList = (totalPageCount: number, limitPageCount: number) => {
  const totalPagesGroupList = range(totalPageCount, 1);
  const pagesGroupList = [];
  for (let i = 0; i < totalPagesGroupList.length; i += limitPageCount) {
    pagesGroupList.push(totalPagesGroupList.slice(i, i + limitPageCount));
  }
  return pagesGroupList;
};

// 현재 페이지가 속한 그룹의 index를 구하기 위한 함수
const getCurrentGroupIndex = (currentPage: number, limitPageCount: number) => {
  return Math.ceil(currentPage / limitPageCount) - 1;
};

const usePagination = ({
  totalPageCount,
  limitPageCount,
  currentPage,
  onChange,
}: UsePaginationArgs) => {
  const pagesGroupList = useRef<number[][]>(createPagesGroupList(totalPageCount, limitPageCount));
  const currentGroupIndex = useRef<number>(getCurrentGroupIndex(currentPage, limitPageCount));
  const [pages, setPages] = useState<number[]>(pagesGroupList.current[currentGroupIndex.current]);
  
  const isFirstGroup = currentGroupIndex.current === 0;
  const isLastGroup = currentGroupIndex.current === pagesGroupList.current.length - 1;

2. 핸들러 함수 선언

페이지 클릭 시

  • 페이지 버튼의 textContent로 입력된 숫자를 가져옵니다.
  • 해당 숫자를 path로 설정해 url을 변경합니다. (숫자만 넘기고 부모 컴포넌트에서 수행)

이전 범위 버튼 클릭 시

  • currentGroupIndex가 1감소되고 이 값을 이용해 이전 범위 그룹으로 리렌더링합니다.
  • 현재 그룹의 가장 마지막 요소의 숫자를 path로 설정해 url을 변경합니다.

다음 범위 버튼 클릭 시

  • currentGroupIndex가 1증가되고 다음 범위 그룹으로 리렌더링합니다.
  • 현재 그룹의 가장 첫번째 요소의 숫자를 path로 설정해 url을 변경합니다.
// usePagination.tsx

 const handleClickPage = (event: any) => {
    const { textContent } = event.target;
    const selectedPage = Number(textContent);
    onChange(selectedPage); // 클릭한 페이지로 url변경
  };

  const handleClickLeft = () => {
    if (isFirstGroup) return;
    currentGroupIndex.current -= 1;
    setPages(pagesGroupList.current[currentGroupIndex.current]); // 이전 그룹으로 ui변경
    onChange(pagesGroupList.current[currentGroupIndex.current][limitPageCount - 1]); //현재 속한 그룹의 가장 마지막 페이지로 url변경
  };

  const handleClickRight = () => {
    if (isLastGroup) return;
    currentGroupIndex.current += 1;
    setPages(pagesGroupList.current[currentGroupIndex.current]); // 다음 그룹으로 ui변경
    onChange(pagesGroupList.current[currentGroupIndex.current][0]); //현재 속한 그룹의 가장 첫번째 페이지로 url변경
  };

Pagination컴포넌트 구현

  • 현재 페이지에 해당하는 페이지는 스타일을 다르게 주어 표시하고 비활성화합니다.
// Pagination.tsx
const Pagination = ({
  currentPage = 1,
  totalPageCount,
  limitPageCount,
  onChange,
}: PaginationProps) => {
  const { pages, isFirstGroup, isLastGroup, handleClickPage, handleClickLeft, handleClickRight } =
    usePagination({ totalPageCount, limitPageCount, currentPage, onChange });
  return (
    <Container>
      <Button onClick={handleClickLeft} disabled={isFirstGroup}>
        <VscChevronLeft />
      </Button>
      <PageWrapper>
        {pages.map((page) => (
          <Page
            key={page}
            selected={page === currentPage}
            disabled={page === currentPage}
            onClick={handleClickPage}
          >
            {page}
          </Page>
        ))}
      </PageWrapper>
      <Button onClick={handleClickRight} disabled={isLastGroup}>
        <VscChevronRight />
      </Button>
    </Container>
  );
};

...

const Page = styled.button<PageType>`
  padding: 4px 6px;
  background-color: ${({ selected }) => (selected ? '#000' : 'transparent')};
  color: ${({ selected }) => (selected ? '#fff' : '#000')};
  font-size: 20px;
  & + & {
    margin-left: 4px;
  }
  &:disabled {
    cursor: default;
  }
`;
profile
#프론트엔드

0개의 댓글