✅ Pagination(페이지네이션)이란?
💡 웹페이지에 표시되는 내용이 많아 한 페이지에 모든 내용을 다 표시하지 않고 여러 페이지로 나누어서 보여주는 방식을 말한다.
Pagination의 필요성
글이 많아지다 보니 한번의 글 조회 요청에 많은 데이터를 불러오게 된다.
이렇게 되면 사용자에게도 너무 많은 스크롤을 요구하게 돼 좋지 않고
서버 입장에서도 한번의 요청에 처리해야하는 데이터가 많아지게 되면 부하가 일어나게 된다.
그렇기 때문에 여러 항목을 보여줘야 하는 기능을 구현할 때는 반드시 Pagination을 사용해야한다.
✅ 게시판에 Pagination 기능 구현 방식
1️⃣ 사용자가 원하는 페이지의 글들을 어떻게 불러올까?
가장 먼저 데이터베이스에서 어떻게 데이터를 불러와야할지 생각을 해야한다.
MySQL은 limit n이라는 키워드를 제공한다. 데이터를 뽑아낸 뒤, 순서대로 n개의 데이터를 반환시켜주는 기능을 갖고 있다.
또한 시작 행을 지정할 수 있는데 limit start_row n은 start_row번째 데이터부터 n개의 데이터를 반환시켜준다.
이를 활용하면 게시판에 페이지네이션을 구현할 수 있다.
[게시판 글 쿼리]
-- 글을 최신순으로 받아옴
SELECT id, title, writer, read_count, created_at from board
order by created_at desc
limit start_row, n;
start_row는 사용자가 원하는 페이지의 첫 번째 글이다.
n은 한 페이지에 보여줄 글의 수로 개발자가 임의로 지정하면 된다. 10으로 했다.
사용자가 원하는 페이지와 함께 글 요청을 보내면 페이지번호를 통해 start_row만 계산하면 끝난다.
어떻게 하면 페이지 번호를 통해 start_row를 계산할 수 있을까?
간단하게 식을 세워보면
1번 페이지는 0번부터 9번 글을 보여줘야 하므로 start_row는 0
2번 페이지는 10번부터 19번 글을 보여줘야 하므로 start_row는 10
3번 페이지는 20번부터 29번 글을 보여줘야 하므로 start_row는 20
이를 일반화하면 n번 페이지의 start_row는 (n-1) * 10이 된다.
2️⃣ 하단 페이지 표시부분은 어떻게?
하지만 아직 남아있는 부분이 있다.
사용자의 편의를 위해 하단에 페이지 번호들을 적어놓는 경우가 많다.
이 또한 모든 페이지 번호를 한 페이지에 표현하기는 어렵기 때문에 특정 단위로 페이지 번호 그룹을 나누어서 보여주는 경우가 많다.
페이지 그룹은 보통 10개 단위로 보여주는 경우가 많다.
우선 페이지의 그룹을 알기 위해선 시작 페이지 번호와 마지막 페이지 번호를 알아야 한다.
이것들은 현재 페이지 번호(사용자가 요청한 페이지 번호)를 통해 구할 수 있다.
시작 페이지 번호: - (현재 페이지 번호 - 1) // 10(몫)
마지막 페이지 번호: - 시작 페이지 번호 + (10 - 1) (한 페이지 그룹이 10개 단위이므로)
예시 - 1~10까지의 페이지그룹은 현재 페이지에서 1을 빼고 10을 나누어서 몫만 취하면 항상 1이 나온다.
여기에 한 페이지 그룹에 10개 이므로 9를 더하면 항상 10이 나온다.
이 뿐만 아니라 몇 가지 더 생각해야 할 부분이 있다.
마지막 페이지 그룹의 경우는 남은 페이지가 없어 이 10개를 다 채우지 못할 수도 있다.
또한 페이지 그룹별로 보여줘야하는 버튼(이전, 다음)도 다르다.
이러한 기능들은 다음과 같이 구현할 수 있다.
- 현재 페이지 그룹의 시작 번호보다 작은 페이지 그룹이 존재하면 이전 버튼을 추가한다. 이 말을 바꿔 말하면 그룹의 시작번호가 1만 아니면 무조건 작은 페이지 그룹이 있는 것이기 때문에 이전 버튼을 추가한다.
- ex - 현재 페이지 그룹이 11~20이면 1~10이 있기 때문에 이전 버튼을 추가해야 한다.
- 현재 페이지 그룹의 마지막 번호가 페이지 전체 갯수 작다면 다음 버튼을 추가한다.
- ex - 현재 페이지가 51~60이고 전체 페이지 갯수가 78이면 다음 버튼을 추가해야 한다.
- 현재 페이지 그룹의 마지막 번호가 페이지 전체 갯수 보다 크다면 해당 그룹의 마지막 번호는 페이지의 전체 갯수가 된다.
- ex - 현재 페이지 그룹이 71~80이고 전체 페이지 갯수가 78이면 80까지 표시하는게 아니라 78까지만 표시하면 된다.
이를 구현하기 위해선 페이지의 전체 갯수가 필요하다.
페이지의 전체 갯수는 전체 데이터의 갯수를 한 페이지에 들어갈 데이터 갯수로 나눈 몫에 1을 더한 값이 된다. 하지만 여기서 주의해야할 것이 나누어 떨어진다면 1을 더하면 안된다.
예를 들어 전체 데이터가 778개이고 한 페이지당 글이 10개씩 들어간다면 전체 페이지 수는 778 / / 10 + 1 = 78이 된다. 하지만 전체데이터가 770개로 나누어 떨어지면 770 / 10 = 77이 된다.
이때 데이터베이스에서 전체 데이터 갯수가 필요하므로 다음의 쿼리가 필요하다.
SELECT COUNT(*) FROM BOARD;
물론, SQL을 통해서 연산을 하고 넘겨줘도 되겠지만, 나는 Java코드를 통해 연산을 했다.
✅ 본격적으로 게시판에 Pagination 구현
1️⃣ 인터페이스 및 SQL 작성하기(MyBatis)
BoardRepository.java
public interface BoardRepository {
int selectCount();
List<Board> selectPageList(@Param("sr") int startRow, @Param("cnt") int count);
}
BoardRepository.xml
<select id="selectPageList" resultType="org.example.board.domain.Board">
SELECT id, title, writer, read_count, created_at from board
order by created_at desc
limit #{sr}, #{cnt}
</select>
<select id="selectCount" resultType="java.lang.Integer">
SELECT COUNT(*) FROM BOARD;
</select>
2️⃣ 서비스 로직 구현
BoardService.java
가독성을 위해 관련없는 다른 메서드들은 생략했다.
@Service
public class BoardService {
private static final int COUNT_PER_PAGE = 10; // 한 페이지당 보여줄 글의 갯수
private final BoardRepository repo;
public BoardService(BoardRepository repo, FileRepository fileRepository) {
this.repo = repo;
this.fileRepository = fileRepository;
}
// 현재 보고자하는 페이지 정보가 들어왔을 때, 실제 해당 페이지에 보여쟈야하는 List<Board>를 포함해서 페이지가 총 몇개 필요하고,
// 하단 페이지 링크는 1-10 or 11~20같은 페이지 구간 계산
public Map<String, Object> getBoards(int page) {
int totalCount = repo.selectCount(); // 총 게시글의 갯수;
int totalPageCount = totalCount % COUNT_PER_PAGE == 0 ? // 예를들어 글이 총 20개라면 2페이지가 필요하지만
totalCount / COUNT_PER_PAGE : totalCount / COUNT_PER_PAGE + 1; // 21~29개라면 3페이지가 필요함(그래서 + 1)
int startPage = (page - 1) / 10 * 10 + 1; // 현재 페이지가 11, 12, 13, ... , 20이었을 때 -1해서 10~19로 바꾸고 /10*10 하면 11, 12, ..., 19 다 동일하게 10으로 동일됨
int endPage = startPage + 9; // 한화면에 보여줄 페이지의 수는 총 10개 (예: 11~20)
if(totalPageCount < endPage) endPage = totalPageCount; // 만약 총 페이지의 갯수가 계산된 마지막 페이지 수보다 많다면 총 페이지의 갯수가 마지막 페이지가 됨
List<Board> boards = repo.selectPageList((page - 1) * COUNT_PER_PAGE, COUNT_PER_PAGE); // 0페이지 0~9번 인덱스 ,1페이지 10~19번 인덱스
Map<String, Object> resultData = new HashMap<>();
resultData.put("page", page);
resultData.put("startPage", startPage);
resultData.put("endPage", endPage);
resultData.put("totalPageCount", totalPageCount);
resultData.put("boards", boards);
return resultData;
}
}
int totalCount = repo.selectCount(); // 총 게시글의 갯수;
int totalPageCount = totalCount % COUNT_PER_PAGE == 0 ? // 예를들어 글이 총 20개라면 2페이지가 필요하지만
totalCount / COUNT_PER_PAGE : totalCount / COUNT_PER_PAGE + 1; // 21~29개라면 3페이지가 필요함(그래서 + 1)
int startPage = (page - 1) / 10 * 10 + 1; // 현재 페이지가 11, 12, 13, ... , 20이었을 때 -1해서 10~19로 바꾸고 /10*10 하면 11, 12, ..., 19 다 동일하게 10으로 동일됨
int endPage = startPage + 9; // 한화면에 보여줄 페이지의 수는 총 10개 (예: 11~20)
if(totalPageCount < endPage) endPage = totalPageCount; // 만약 총 페이지의 갯수가 계산된 마지막 페이지 수보다 많다면 총 페이지의 갯수가 마지막 페이지가 됨
- 총 게시글의 갯수를 받아와서 총 페이지 수, 요청 페이지 번호를 통해 시작 페이지 번호, 마지막 페이지 번호를 계산했다.
List<Board> boards =
repo.selectPageList((page - 1) * COUNT_PER_PAGE, COUNT_PER_PAGE); // 0페이지 0~9번 인덱스 ,1페이지 10~19번 인덱스
- 위에서 언급했듯, 사용자가 원하는 페이지에서 1을 빼고 페이지당 글의 수를 곱해준 값이 start_row가 된다.
3️⃣ 컨트롤러 구현
@GetMapping
public ModelAndView getBoardsList(@RequestParam(name = "page", defaultValue = "1") int page) {
Map<String, Object> pageData = boardService.getBoards(page);
ModelAndView modelAndView = new ModelAndView("board/board_list");
modelAndView.addObject("pageData", pageData);
return modelAndView;
}
- 쿼리 파라미터로 페이지 번호를 받을 것이기 때문에 @RequestParam을 붙여야한다. 또한 defaultValue = “1”로 설정해 최초 조회시 첫 번째 페이지가 나오게 했다.
- Model값을 세팅해 준 뒤 view로 넘겨줬다.
4️⃣ View 구현
board/board_list.jsp
<%@ page import="java.util.List" %>
<%@ page import="org.example.board.domain.Board" %>
<%@ page import="java.util.Map" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>글 목록</title>
<style>
<%--CSS 생략--%>
</style>
</head>
<body>
<div class="container">
<h2>게시판 글 목록</h2>
<table>
<tbody>
<%
// request에서 "pageData"라는 이름의 attribute를 가져옴
Map<String, Object> pageData = (Map<String, Object>) request.getAttribute("pageData");
List<Board> boards = (List<Board>) pageData.get("boards");
int startPage = (int) pageData.get("startPage");
int endPage = (int) pageData.get("endPage");
int totalPageCount = (int) pageData.get("totalPageCount");
int currentPage = (int) pageData.get("page");
if (boards != null && !boards.isEmpty()) {
for (Board board : boards) {
%>
<tr>
<td class="title"><a href="<%=request.getContextPath()%>/boards/<%=board.getId()%>"><%= board.getTitle() %></a></td>
<td class="writer"><%= board.getWriter() %></td>
<td class="created-at"><%= board.getCreatedAt() %></td>
<td><%= board.getReadCount() %></td>
</tr>
<%
}
}
%>
</tbody>
</table>
<!-- 페이지 네비게이션 -->
<div class="pagination">
<%
if(startPage != 1) {
%>
<a href="<%=request.getContextPath()%>/boards?page=<%=startPage - 1%>"><이전</a>
<%
}
for (int i = startPage; i <= endPage; i++) {
if (i == currentPage) {
%>
<a class="current-page" href="<%=request.getContextPath()%>/boards?page=<%=i%>"><%=i%></a>
<%
} else {
%>
<a href="<%=request.getContextPath()%>/boards?page=<%=i%>"><%=i%></a>
<%
}
}
if(endPage != totalPageCount) {
%>
<a href="<%=request.getContextPath()%>/boards?page=<%=endPage + 1%>">다음></a>
<%
}
%>
</div>
</div>
</body>
</html>
<tr>
<td class="title"><a href="<%=request.getContextPath()%>/boards/<%=board.getId()%>"><%= board.getTitle() %></a></td>
<td class="writer"><%= board.getWriter() %></td>
<td class="created-at"><%= board.getCreatedAt() %></td>
<td><%= board.getReadCount() %></td>
</tr>
- 현재 페이지에 해당하는 글들을 뿌려줬다.
<%
if(startPage != 1) {
%>
<a href="<%=request.getContextPath()%>/boards?page=<%=startPage - 1%>"><이전</a>
<%
}
%>
- 시작 페이지 번호가 1이 아니면 이전 페이지 그룹이 있다는 것이기 때문에 이전 버튼을 추가해줬다.
<%
}
}
if(endPage < totalPageCount) {
%>
<a href="<%=request.getContextPath()%>/boards?page=<%=endPage + 1%>">다음></a>
<%
}
%>
- 만약 마지막 페이지 번호가 총 페이지 갯수보다 작다면 다음 페이지 그룹이 있다는 것이기 때문에 다음 버튼을 추가해줬다.
✅ 구현 결과 확인
'데브코스 > 실습 & 프로젝트' 카테고리의 다른 글
게시판 만들기 - 데이터 계층 리팩토링 (JPA 도입) (2) | 2024.09.02 |
---|---|
게시판 만들기 - 기능 추가: 댓글(대댓글) 기능 추가 (0) | 2024.08.30 |
게시판 만들기 - 데이터 계층 리팩토링(MyBatis 도입) (0) | 2024.08.26 |
게시판 만들기 - 웹 계층 리팩토링(Spring MVC 도입) (0) | 2024.08.22 |
게시판 만들기 - 웹 계층 리팩토링(FrontController 도입) (0) | 2024.08.21 |