API 를 통해서 많은 양의 데이터를 가져와 화면에 렌더링해야 하는 경우 자칫하면 성능 문제를 야기할 수 있습니다.
이 때 성능을 최적화하기 위한 다양한 방법 중 전통적인 방법으로 페이지네이션이 있습니다.
페이지네이션은 데이터의 갯수를 미리 알고 있고 데이터의 추가나 삭제가 자주 일어나지 않을 때 유용합니다.
React Project 생성
npx create-react-app ./ --template typescript
npm install @emotion/styed @emotion/react react-icons
import React from 'react'
interface Props {
count: number // 페이지 전체 개수
page: number // 현재 페이지
onPageChange: (page: number) => void
disabled?: boolean
siblingCount?: number //현재페이지 전 후에 표시되는 숫자
boundaryCount?: number //시작과 끝에서 항상 표시되는 숫자
}
export default function usePagination({
count,
page,
onPageChange,
disabled,
siblingCount = 1,
boundaryCount = 2,
}: Props) {
// 시작과 끝을 지정했을 시 그 사이에 숫자를 배열로 반환
// ex range(1,5) = [1,2,3,4,5] 반환
const range = (start: number, end: number) => {
const length = end - start + 1
return Array.from({ length }).map((_, index) => index + start)
}
const startPage = 1 // 시작페이지
const endPage = count // 전체 페이지 = 끝 페이지
// 시작 페이지
// 시작 페이지(1) ~ (바운더리카운트, 전체페이지 중 작은 수) 의 배열
const startPages = range(startPage, Math.min(boundaryCount, count))
// 끝 페이지
// (전체페이지 - 바운더리카운트 + 1), (바운더리카운트 + 1) 중 높은 숫자 ~ 전체페이지
const endPages = range(
Math.max(count - boundaryCount + 1, boundaryCount + 1),
count
)
// 현재페이지 앞
// (현재페이지 + 1 - 현재 앞뒤 설정개수,) (전체페이지 - 맨앞맨끝 설정개수 - 현재 앞뒤 설정개수 * 2 - 1 )중 작은 수
const siblingStart = Math.max(
Math.min(
page + 1 - siblingCount,
count - boundaryCount - siblingCount * 2 - 1
),
boundaryCount + 2
)
// 현제페이지 뒤
const siblingEnd = Math.min(
Math.max(page + 1 + siblingCount, boundaryCount + siblingCount * 2 + 2),
endPages.length > 0 ? endPages[0] - 2 : endPage - 1
)
const itemList = [
'prev',
...startPages,
...(siblingStart > boundaryCount + 2
? ['start-ellipsis']
: boundaryCount + 1 < count - boundaryCount
? [boundaryCount + 1]
: []),
...range(siblingStart, siblingEnd),
...(siblingEnd < count - boundaryCount - 1
? ['end-ellipsis']
: count - boundaryCount > boundaryCount
? [count - boundaryCount]
: []),
...endPages,
'next',
]
const items = itemList.map((item, index) =>
typeof item === 'number'
? {
key: index,
onClick: () => onPageChange(item - 1),
disabled,
selected: item - 1 === page,
item,
}
: {
key: index,
onClick: () => onPageChange(item === 'next' ? page + 1 : page - 1),
disabled:
disabled ||
item.indexOf('ellipsis') > -1 ||
(item === 'next' ? page >= count - 1 : page < 1),
selected: false,
item,
}
)
return { items }
}
import React from 'react'
import styled from '@emotion/styled/macro'
import { GrFormPrevious, GrFormNext } from 'react-icons/gr'
import { AiOutlineEllipsis } from 'react-icons/ai'
import usePagination from '../hooks/usePagination'
interface Props {
count: number
page: number
onPageChange: (page: number) => void
disabled?: boolean
siblingCount?: number //현재페이지 전 후에 표시되는 숫자
boundaryCount?: number //시작과 끝에서 항상 표시되는 숫자
}
const Navigation = styled.nav``
const Button = styled.button<{ selected?: boolean }>`
color: ${({ selected }) => (selected ? '#fff' : '#000')};
border: 0;
margin: 0;
padding: 8px 12px;
font-size: 16px;
font-weight: normal;
background-color: ${({ selected }) => (selected ? '#3d6afe' : '#fff')};
cursor: pointer;
border-radius: 100%;
width: 48px;
height: 48px;
&:hover {
background-color:#ccc;
color:#fff;
}
&:active {
opacity: 0.8;
}
`
const PaginationItem = styled.li``
const PaginationItemList = styled.ul`
margin: 0;
padding: 0;
display: flex;
list-style: none;
${PaginationItem} + ${PaginationItem} {
margin-left: 8px;
}
`
export default function Pagination({
count,
page,
onPageChange,
disabled,
siblingCount,
boundaryCount,
}: Props) {
const getLabel = (item: number | string) => {
if (typeof item === 'number') return item
else if (item.indexOf('ellipsis') > -1) return <AiOutlineEllipsis />
else if (item.indexOf('prev') > -1) return <GrFormPrevious />
else if (item.indexOf('next') > -1) return <GrFormNext />
else return item
}
const { items } = usePagination({
count,
page,
onPageChange,
disabled,
siblingCount,
boundaryCount,
})
return (
<Navigation>
<PaginationItemList>
{items.map(({ key, disabled, selected, onClick, item }) => (
<PaginationItem key={key}>
<Button disabled={disabled} selected={selected} onClick={onClick}>
{getLabel(item)}
</Button>
</PaginationItem>
))}
</PaginationItemList>
</Navigation>
)
}