
react-router-dom 은 리액트 앱 내에서 부드러운 페이지 이동을 구현하게 해주는 라이브러리이다. 이를 응용해서 Pagination 기능을 만들어보았다.
Pagination은 많은 목록을 여러 페이지로 나누어서 정렬하는 기법이다.

- 한 페이지에 최대 5개의 목록을 표시한다.
 - 숫자를 누르면 해당하는 목록을 표시한다. (숫자는 한 페이지 최대 5개)
 - Start - 1 페이지로 이동
 - Prev - 한 페이지 전으로 이동
 - Next - 한 페이지 뒤로 이동
 - End - 끝 페이지로 이동
 - 숫자 1이 목록에 있으면 Start 버튼을 비활성화한다.
 - 마지막 페이지에 해당하는 숫자가 목록에 있으면 End 버튼을 비활성화한다.
 - 이전 페이지가 없으면 Prev 버튼을 비활성화한다.
 - 다음 페이지가 없으면 Next 버튼을 비활성화한다.
 
Pagination 기능을 구현하기 위해서 makePagination() 함수를 만들었다.
| Name | Value | 
|---|---|
| listCnt | 총 게시물의 개수 (완성본 -> 31) | 
| pageRange | 숫자 목록에 나타낼 숫자의 범위 (완성본 -> 5) | 
| thisPage | 현재 페이지 | 
| Name | Value | 
|---|---|
| startRange | 현재 페이지 범위의 시작 번호 (숫자 목록에 표시된 처음 번호) | 
| endRange | 현재 페이지 범위의 끝 번호 (숫자 목록에 표시된 끝 번호) | 
| lastPage | 마지막 페이지의 번호 (완성본 -> 7) | 
| startIdx | 현재 페이지의 게시물 첫번째 인덱스 | 
| lastIdx | 현재 페이지의 게시물 마지막 인덱스 | 
| showStartBtn | Start 버튼을 활성화하는지 | 
| showEndBtn | End 버튼을 활성화하는지 | 
| pageNotFound | 현재 페이지가 게시물의 범위를 벗어나는지 | 
use-pagination.js

다른 페이지에서도 로직을 재사용할 수 있게 커스텀 hook인 usePagination() 함수를 만들었다. 위에서 만든 makePagination() 함수의 반환값에 페이지 상태 함수와 페이지 이동 함수를 더해서 리턴해준다.
| 함수명 | 기능 | 
|---|---|
| goStart() | 처음 페이지(1)로 이동 (Start 버튼) | 
| goPrev() | 이전 페이지로 이동 (Prev 버튼) | 
| goNext() | 다음 페이지로 이동 (Next 버튼) | 
| goEnd() | 마지막 페이지로 이동 (End 버튼) | 

QuoteList.js 에서는 다음과 같은 역할을 수행한다.
(prop 으로 목록(quote) 전체를 받아온다.)
- url에 표시된 페이지 정보를 받아온다.
 
- prop으로 받아온 목록의 길이와 현재 페이지 정보를 usePagination() 함수에 매개변수로 넘겨서 리턴값들을 받아온다.
 
- 사용자가 페이지를 이동하면 그에 따라서 실제 페이지를 이동시킨다. (useEffect() 활용)
 
- 현재 페이지의 정보를 활용해서 해당 목록들을 화면에 렌더링한다. (페이지가 목록의 범위를 벗어나면 PageNotFound 컴포넌트를 리턴한다.
 
- usePagination() 에서 받아온 값들을 Pagination 컴포넌트에 prop 으로 넘긴다. (페이지 리모컨은 Pagination 컴포넌트에서 렌더링한다.)
 
page 쿼리의 값은 useLocation() 함수로 손쉽게 받아올 수 있다.
import { useLocation } from 'react-router-dom';
const location = useLocation();
const queryParams = new URLSearchParams(location.search);
이제 queryParams.get('page')  함수로 현재 페이지를 받아올 수 있다.
prop 으로 받아온 목록의 길이와 페이지 정보를 usePagination() 함수의 매개변수로 넘긴다.
import usePagination from '../../hooks/use-pagination';
 const {
    thisPage,
    setThisPage,
    goStart,
    goPrev,
    goNext,
    goEnd,
    startRange,
    endRange,
    lastPage,
    startIdx,
    lastIdx,
    showStartBtn,
    showEndBtn,
    pageNotFound,
  } = usePagination(
    props.quotes.length, // 목록의 길이
    5, 					 // 숫자 목록에 나타낼 숫자의 범위 
    Number(queryParams.get('page')) || 1 // 현재 페이지 (없으면 1)
  );
사용자가 페이지를 이동하면 usePagination() 의 thisPage state 가 변경된다.
useEffect() 함수로 thisPage 가 변경되는 것을 감지하면 useNavigate() 를 사용해서 실제로 페이지를 이동시킨다.
import { useEffect } from 'react';
import { createSearchParams, useNavigate } from 'react-router-dom';
const navigate = useNavigate();
// useLocation() 에서 현재 url을 받아옴 (쿼리 값 제외)
const { pathname } = location;  
// thisPage 가 변경되면 useEffect() 함수를 실행 (나머지는 종속 변수)
useEffect(() => {
    navigate({
      pathname: pathname,
      search: `?${createSearchParams({
        page: thisPage,
      })}`,
    });
  }, [navigate, thisPage, isSortingAscending, pathname]);
만약 현재 페이지가 목록의 범위를 벗어날 경우 PageNotFound 컴포넌트를 리턴한다. 이를 위해 목록을 렌더링하기 전 if 문으로 한번 확인해준다.
(범위가 벗어난 지 확인하는 값은 usePagination() 함수에서 받아왔다.)

이제 usePagination() 에서 받아온 startIdx, lastIdx 값을 slice() 함수의 인자로 넣어준다.
(간단한 기능이므로 다른 건 생략하겠다.)

페이지 리모컨 기능은 Pagination 컴포넌트에서 구현할 것이므로 필요한 기능들을 prop 으로 넘겨준다.
사용자가 버튼을 누르지 않고 숫자를 직접 눌러서 페이지를 이동할 수 있게 onChangePage() 함수를 추가로 생성한다.


Pagination.js 에서는 페이지 리모컨을 생성한다.
필요한 모든 정보와 기능을 prop 으로 받아왔기 때문에 이제 간단히 구현할  수 있다.
Start Prev Next End 버튼을 표시하는 코드이다.
(실제로는 숫자 양옆에 표시한다.)

비활성 여부를 결정하기 위해 disabled prop 에 간단한 로직을 추가했다.
사용자가 숫자를 눌러서 페이지를 이동할 수 있게 해주는 숫자 목록이다.

prop 으로 받아온 startRange 와 endRange 를 사용해서 리스트를 만들어준다.
만약 현재 페이지가 3페이지면 pages 변수에는 [1,2,3,4,5] 의 값이 들어간다.
이제 생성한 리스트와 map() 메소드를 이용해서 숫자를 렌더링하면 끝이다.
( 현재 페이지에 해당하는 숫자의 색을 다르게 하기 위해서 className prop 에 로직을 추가했다. )


예전에 django 동영상 강의를 보면서 Pagination 기능을 만들어본 적이 있었지만 혼자서 구현해보니 아예 다른 느낌이었다. 어떤 식으로 코드를 짜야할지 처음 설계할 때 부터 많은 고민을 했고 결국에는 커스텀 훅으로 기능을 구현했지만 잘 한건지 아직도 모르겠다.
하지만 이번 프로젝트를 진행하며 확실히 느낀 점은 기능을 구현하는 것보다 useMemo() / useCallback() 으로 앱을 최적화하는 것이 더 어렵다는 점이다. (중간에 시도했다가 결국 다 없애버렸다.)
코드 최적화에 대해 더 공부해야겠다. ^^
끝.