
전체 게시물 수를 페이지 당 표시할 게시물 수로 나눈 뒤 올림하면 총 몇 개의 페이지가 필요한지 구할 수 있다.전체 페이지 개수 구하기
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;
참고 블로그