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() 으로 앱을 최적화하는 것이 더 어렵다는 점이다. (중간에 시도했다가 결국 다 없애버렸다.)
코드 최적화에 대해 더 공부해야겠다. ^^
끝.