전체 게시물 수를 페이지 당 표시할 게시물 수로 나눈 뒤 올림
하면 총 몇 개의 페이지가 필요한지 구할 수 있다.전체 페이지 개수 구하기
Math.ceil(전체 게시물(데이터) 수 / 페이지 당 표시할 게시물(데이터) 수)
const numPages = Math.ceil(todoData.length(37) / limit(10));
두번째로 알아야할 부분은 현재 페이지 번호를 기준으로 표시해줘야할 게시물들의 범위, 즉, 해당 페이지의 첫 게시물의 위치(index)를 알아야한다.
페이지 번호에서 1을 뺀 후에 페이지 당 표시할 게시물의 수를 곱하면
첫 게시물의 위치를 계산할 수 있다. (마지막 게시물의 위치는 첫 게시물의 위치에서 단순히 페이지 당 표시할 게시물의 수만 더해주면 된다.)
첫 게시물 위치 구하기
(페이지 번호 - 1) * 페이지 당 표시할 게시물의 수
예를 들어, 위와 동일한 총 37개의 게시물이 있고, 페이지 당 10개의 게시물을 표시되야 한다면 아래와 같이 구할 수 있다. (index는 0부터 시작하기 때문에 1을 빼준다.)
const offset = (page - 1) * limit(10)
전체 게시물 대신에 현재 페이지에 해당하는 게시물만 보여줄 수 있도록 컴포넌트를 변경한다.
먼저 페이지 당 표시할 게시물 수(limit)
, 현재 페이지 번호(page)
를 상태로 추가하고, 첫 게시물의 위치(offset)
를 계산한다.
// limit 상태 변수는 현재 페이지에 표시할 데이터의 개수를 나타낸다.
const [limit, setLimit] = useState(10); // 기본값: 10개씩 노출
// page 상태 변수는 현재 몇 번째 페이지인지를 나타낸다.
const [page, setPage] = useState(1); // 기본값: 1페이지부터 노출
// `offset`은 각 페이지에서의 첫 번째 데이터의 위치(index)를 나타낸다.
const offset = (page - 1) * limit;
<select>
요소를 사용한다.// 데이터를 10개, 20개, 30개 씩 보여줌
<select type="number" value={limit} onChange={(e) => setLimit(Number(e.target.value))}>
<option value="10">10개씩</option>
<option value="20">20개씩</option>
<option value="30">30개씩</option>
</select>
map
으로 버튼을 만든다.// 데이터를 10개, 20개, 30개 씩 보여줌
const limitList = [10, 20, 30];
{limitList.map((el, idx) => {
return (
<button key={idx} value={el} onClick={(e) => setLimit(Number(e.target.value))}>
{el}
</button>
);
})}
slice()
를 사용하여 첫 게시물부터 선택한 게시물의 수만 보여주도록 변경한다.ex
- 첫 번째 페이지의 경우
offset
이 0이고(page === 1) limit이 10이라면 데이터의 0번째 index부터 9번째 index까지만 데이터를 불러온다.
- 두 번째 페이지의 경우
offset
이 10이고(page === 2) limit이 10이라면 데이터의 10번째 index부터 19번째 index까지만 데이터를 불러온다.
// 기존
{todoData.map((value) =>
<TodoList
list={value}
key={value.id}
getTodoData={getTodoData}
/>
)}
// 변경
{todoData.slice(offset, offset + limit).map((value) =>
<TodoList
list={value}
key={value.id}
getTodoData={getTodoData}
/>
)}
페이지를 이동할 수 있도록 해주는 <Pagination/>
컴포넌트는 이전 페이지나 다음 페이지 또는 특정 페이지로 바로 이동할 수 있는 많은 버튼으로 구성되어야 한다.
<Pagination/>
컴포넌트는 재활용 가능하도록 별도의 컴포넌트로 만들 경우 데이터가 들어가는 컴포넌트로부터 총 게시물 수(total)
와 페이지 당 게시물 수(limit)
그리고 현재 페이지 번호(page, setPage)
를 props
로 받아와야 한다.
필요한 페이지의 개수(numPages)
를 계산한 후 루프를 돌면서 이 페이지의 개수만큼 페이지 번호 버튼을 출력한다.
// allPageLength === todoData.length
const numAllPages = Math.ceil(allPageLength / limit);
페이지 번호 버튼에 클릭 이벤트가 발생하면 props
로 넘어온 setPage()
함수를 호출하여 부모 컴포넌트의 page 상태가 변경되도록 한다. 그러면 부모 컴포넌트는 새로운 페이지 번호에 해당하는 게시물 범위를 계산하여 다시 화면을 렌더링하게 된다.
아래의 예시에서 new Array(numAllPages).fill()
의 의미는 먼저 길이가 numAllPages인 빈 배열을 만들고, 각 원소를 undefined
로 채워준다. .fill()
을 사용하여 undefined
로 채워주는 이유는 empty
인 경우 map
을 사용할 수 없기 때문이다.
그리고 {index + 1}
은 생성된 빈 배열의 undefiend
요소의 개수만큼 버튼을 만든다.
{new Array(numAllPages).fill().map((_, index) => (
<button
className={page === index + 1 ? 'pageTab pageFocused' : 'pageTab'}
key={idx + 1}
onClick={() => setPage(index + 1)}> {/* 클릭한 페이지로 바로 이동하는 버튼 이벤트 핸들러 */}
{index + 1}
</button>
))}
.fill.map()과 Array.from()
- 아래의 2개의 코드 모두 페이지네이션 기능이 정상적으로 동작한다.
- .fill.map()
// fill()이 없으면 배열 생성시 undefined가 아닌 empty로 담김. // 즉, empty면 map을 돌 수 없기 때문에 fill()이 필요함 {new Array(numAllPages).fill().map((_, index) => ( <button key={index + 1} onClick={() => setPage(index + 1)}> {index + 1} </button> ))}
- Array.from()
{Array.from({ length: numAllPages }, (_, index) => ( <button key={index + 1} onClick={() => setPage(index + 1)}> {index + 1} </button> ))}
import TodoList from "./TodoList";
import Pagenation from "./Pagenation";
import axios from "axios";
import { useState, useEffect } from "react";
function Main() {
const [todoData, setTodoData] = useState([]); // 전체 데이터가 담겨 있는 변수
// Get
const getTodoData = async () => {
const res = await axios.get('http://localhost:3001/todos');
setTodoData(res.data);
};
useEffect(() => {
getTodoData();
}, []);
const [limit, setLimit] = useState(10); // 페이지 당 표시할 데이터 수 (기본값: 10개씩 노출)
const [page, setPage] = useState(
() => JSON.parse(window.localStorage.getItem("currentPage")!) || 1
); // 현재 페이지 번호 (기본값: 1페이지부터 노출)
const offset = (page - 1) * limit; // 각 페이지에서 첫 데이터의 위치(index) 계산
useEffect(() => {
window.localStorage.setItem("currentPage", JSON.stringify(page));
}, [page]);
return (
<>
<select className="dropDown" type="number" value={limit} onChange={(e) => setLimit(Number(e.target.value))}>
<option value="5">5개씩</option>
<option value="10">10개씩</option>
<option value="20">20개씩</option>
<option value="30">30개씩</option>
</select>
<ul>
{todoData.slice(offset, offset + limit).map((value) =>
<TodoList
list={value}
key={value.id}
getTodoData={getTodoData}
/>)}
</ul>
<Pagenation
allPageLength={todoData.length}
limit={limit}
page={page}
setPage={setPage}
/>
</>
);
}
export default Main;
로컬스토리지를 이용하여 현재 페이지 번호 저장
- 위 코드에서 현재 페이지의 번호를 const [page, setPage] = useState(1); 이렇게 그냥 1로 지정할 경우 2페이지의 게시글에 들어간 후 뒤로가기를 하면 다시 1페이지로 돌아가버리는 것을 방지하기 위해 로컬스토리지로 현재 페이지 번호를 저장 해둔다.
const [page, setPage] = useState( () => JSON.parse(window.localStorage.getItem("currentPage")!) || 1 ); useEffect(() => { window.localStorage.setItem("currentPage", JSON.stringify(page)); }, [page]);
function Pagenation({ allPageLength, limit, page, setPage }) {
// 필요한 페이지 개수 === 총 데이터 수(allPageLength === todoData.length) / 페이지 당 표시할 데이터 수(limit === 10)
const numAllPages = Math.ceil(allPageLength / limit);
// 현재 페이지의 이전 페이지로 이동하는 버튼 이벤트 핸들러
const prevPageHandler = () => {
setPage(page - 1);
};
// 현재 페이지의 다음 페이지 이동하는 버튼 이벤트 핸들러
const nextPageHandler = () => {
setPage(page + 1);
};
return (
<div>
{/* 왼쪽 버튼 클릭시 현재 페이지에서 1 페이지 이전 페이지로 이동하고, 현재 페이지가 1페이지가 되면 왼쪽 버튼은 비활성화 된다.*/}
<button className="leftHandle" onClick={prevPageHandler} disabled={page === 1}>
<
</button>
{new Array(numAllPages).fill().map((_, index) => (
<button className={page === index + 1 ? 'pageTab pageFocused' : 'pageTab'} key={index + 1} onClick={() => setPage(index + 1)}> {/* 클릭한 페이지로 바로 이동하는 버튼 이벤트 핸들러 */}
{index + 1}
</button>
))}
{/* 오른쪽 버튼 클릭시 현재 페이지에서 1 페이지 이후 페이지로 이동하고, 현재 페이지가 마지막 페이지가 되면 오른쪽 버튼은 비활성화 된다.*/}
<button className="rightHandle" onClick={nextPageHandler} disabled={page === numAllPages}>
>
</button>
</div>
);
}
export default Pagenation;
위의 방법대로 하면 페이지네이션 기능은 정상적으로 동작하지만, 만약 페이지가 100개라면 버튼도 100개가 생성되어 매우 복잡해질 것이다.
그러므로 한 페이지에서는 10개의 페이지 버튼만 보여주고 10페이지에서 다음 페이지 버튼을 누를 시 11부터 20까지의 버튼을 다시 10개씩 보여주는 페이징 기능이 추가로 구현되어야 한다.
Main.js는 위와 동일
Pagenation.js
import styled from 'styled-components';
import { useState } from 'react';
const PageNum = styled.div`
margin-top: 20px;
display: flex;
justify-content: center;
align-items: center;
gap: 5px;
user-select: none;
> .pageTab, .leftHandle, .rightHandle {
width: 20px;
height: 20px;
background-color: transparent;
border: none;
color: ${(props) => props.theme.text};
}
> .pageFocused {
border-radius: 3px;
background-color: #f9f9f9;
border: 1px solid black;
font-weight: 600;
color: black
}
> button:hover {
text-decoration: underline;
}
`;
function Pagenation({ allPageLength, limit, page, setPage }) {
const [blockNum, setBlockNum] = useState(0); // 페이지 당 표시할 페이지네이션 수
const pageLimit = 10; // 페이지 당 표시할 페이지네이션 수 (기본값 : 10개의 페이지네이션 노출)
const blockArea = blockNum * pageLimit; // 각 페이지에서 첫 페이지네이션의 위치 계산
// 필요한 페이지 개수 === 총 데이터 수(allPageLength === todoData.length) / 페이지 당 표시할 데이터 수(limit === 10)
const numAllPages = Math.ceil(allPageLength / limit);
// 새로운 배열 생성 함수
const createArr = (n) => {
const iArr = new Array(n);
for (let i = 0; i < n; i++) {
iArr[i] = i + 1;
}
return iArr;
};
const allArr = createArr(numAllPages); // nArr 함수에 전체 페이지의 개수를 배열로 담음
// 현재 페이지의 이전 페이지로 이동하는 버튼 이벤트 핸들러
const prevPageHandler = () => {
if (page <= 1) {
return;
} else if (page - 1 <= pageLimit * blockNum) {
setBlockNum((n) => n - 1);
}
setPage((n) => n - 1);
}
// 현재 페이지의 다음 페이지 이동하는 버튼 이벤트 핸들러
const nextPageHandler = () => {
if (page >= numAllPages) {
return;
} else if (pageLimit * (blockNum + 1) < page + 1) {
setBlockNum((n) => n + 1);
}
setPage((n) => n + 1);
};
return (
<>
<PageNum>
{/* 왼쪽 버튼 클릭시 현재 페이지에서 1 페이지 이전 페이지로 이동하고, 현재 페이지가 1페이지가 되면 왼쪽 버튼은 비활성화 된다.*/}
<button className="leftHandle" onClick={prevPageHandler} disabled={page === 1}>
<
</button>
{allArr.slice(blockArea, pageLimit + blockArea).map((n) => (
<button className={page === n ? 'pageTab pageFocused' : 'pageTab'} key={n} onClick={() => setPage(n)}> {/* 클릭한 페이지로 바로 이동하는 버튼 이벤트 핸들러 */}
{n}
</button>
))}
{/* 오른쪽 버튼 클릭시 현재 페이지에서 1 페이지 이후 페이지로 이동하고, 현재 페이지가 마지막 페이지가 되면 오른쪽 버튼은 비활성화 된다.*/}
<button className="rightHandle" onClick={nextPageHandler} disabled={page === numAllPages}>
>
</button>
</PageNum>
</>
);
}
export default Pagenation;
참고 블로그