요즘 프로젝트를 잘 진행하지 못하였다 면접도 봐보고 잠깐잠깐 일을하느라 진행이 되지않았다 .. 얼른 취업해야지 ~ 화이팅 !
오늘은 페이징에 대해서 공부해보려고 한다 !
기존에는 페이지가 새로고침 되면서 페이지 이동이 되는 방식으로 하였지만 이번 프로젝트에서는 디자인을 하다보니 새로고침이 아닌 비동기 처리로 페이징 처리를 해야할 것 같아서 이번에는 AJAX를 활용하여 페이지 처리를 해보려고 한다.
차근차근 하나씩 원리와 구현방법에 대해서 알아보자.
Ajax는 Asynchronous JavaScript and XML의 약자로, 말 그대로 JavaScript와 XML을 이용한 비동기적 정보 교환 기법이다. 다만 요즘에는 XML보다는 JSON을 주로 사용한다. 브라우저의 XMLHttpRequest를 이용해 전체 페이지를 새로 가져오지 않고 페이지 일부만 변경할 수 있도록 JavaScript를 실행하여 서버에 데이터만을 별도로 요청하는 기법이다. HTTP 프로토콜을 이용한 비동기 통신이며 브라우저는 정적 HTML파일과 CSS파일, 데이터를 어떻게 요청하면 되는지를 설명한 JavaScript를 통해 HTML, CSS를 이용해 골격을 먼저 형성하고 Ajax 실행부가 담긴 JavaScript 영역을 실행하여 데이터를 별도로 가져와 적절한 방법으로 데이터를 끼워 넣은 후 페이지를 로딩한다.
간단하게 말하자면, Ajax를 사용하면 사용자가 웹 페이지에서 어떤 작업을 할 때마다 전체 페이지를 다시 로드할 필요 없이 필요한 데이터만을 서버에서 동적으로 가져와서 페이지를 업데이트할 수 있다. 이것은 사용자 경험을 향상시키고 웹 페이지의 응답성을 높이는 데 도움이 된다.
즉, Ajax는 웹 페이지의 동적인 기능을 구현하는 데 사용되는 기술로, 웹 페이지와 서버 간의 데이터 교환을 효율적으로 처리할 수 있도록 도와준다.
출처 https://jbground.tistory.com/4
동기
동기 방식은 요청과 그에 대한 응답이 순차적으로 진행된다. 즉, 한 작업이 끝나야 다음 작업이 시작되는 것이다. 이는 요청을 보내면 해당 작업이 완료될 때까지 대기하게 되는 것을 의미힌다. 동기방식은 단순하고 직관적이지만, 하나의 작업이 끝날 때까지 다른 작업을 수행할 수 없으므로 대규모 작업이나 여러 작업을 처리할 때는 효율성이 떨어질 수 있다.
비동기
비동기 방식은 요청과 응답이 독립적으로 처리된다. 요청을 보낸 후에도 다른 작업을 계속할 수 있고, 응답을 기다리지 않고 다른 요청을 보낼 수 있다. 이러한 방식은 대규모 작업이나 여러 작업을 효율적으로 처리할 수 있으며 대기 시간을 최소화하여 성능을 향상시킬 수 있다.
코드에 대한 설명은 코드에 주석처리로 거의 해놓았기 때문에 코드에 있는 주석처리를 잘 봐주면 좋을 것 같다.
우선 페이지 구현을 하기위한 설정이 필요하다.
package com.petpark.dto;
import lombok.Data;
@Data
public class PageDTO {
private static final int postPerPage = 10; // 한 페이지에 보일 게시글 수
private static final int blockPerPage = 5; // 페이지 번호 개수 보이기 ( ex : 1 2 3 4 5 )
private int totalPost; // 전체 게시글
private int totalPages; // 전체 페이지
private int currentPage; // 현재 페이지
private int startPage; // 시작 페이지
private int endPage; // 끝 페이지
public PageDTO(int totalPost, int currentPage) {
this.totalPost = totalPost;
this.currentPage = currentPage;
totalPages = (totalPost - 1) / postPerPage + 1;
startPage = currentPage - (currentPage - 1) % blockPerPage;
endPage = currentPage - (currentPage - 1) % blockPerPage + blockPerPage - 1;
if(endPage > totalPages) {
endPage = totalPages;
}
}
public int getPostPerPage() {
return postPerPage;
}
public int getBlockPerPage() {
return blockPerPage;
}
}
PageDTO라는 클래스를 만들어 주어 페이징을 하기 위한 설정을 해주었다.
각 설정을 위한 변수들을 선언해주었고, 한 페이지에 보일 게시글 수(postPerPage)와 페이지 번호 개수 보이기(blockPerPage)는 상수로 설정해주었다.
생성자 부분을 확인해보면 매개변수로 totalPost와 currentPage를 받아오는데 totalPost는 해당 데이터의 개수 값을 받아오며 currentPage에는 클릭한 현재 페이지 번호를 받아오는데,
이번호는 AJAX에서 Controller로 받아오며 Controller에서 PageDTO 생성자를 호출하며 매개변수로 넘겨주어 받아온 값이다.
그 값들을 활용하여 totalPages, startPage, endPage를 설정해주었다.
if문의 경우 endPage가 totalPages보다 많아질 경우 페이지 목록이 더 생겨버려서 많아질 경우 endPage에 totalPages의 값을 넣어준다
컨트롤러에서는 AJAX에서 받은 데이터로 Page설정과 Service를 통해 DB 데이터를 가져왔다.
/*
* AJAX를 사용하여 페이징 처리와 페이지에 따른 데이터를 보여주기 위해 JSON형태로 값을 반환
* json으로 데이터를 컨트롤하기 위해 Map 사용
*
* */
@GetMapping("/newsList.do")
public Map<String, Object> newsList(HttpServletRequest req) throws Exception {
// 뉴스별 카테고리를 눌렀을 때 초기값 1페이지로 설정 ( AJAX로 받아올 때 이미 1을 받아오기는 한다. )
int currentPage = 1;
// AJAX의 data로 보낸 값(페이지 번호)을 받아온다.
String currentPageStr = req.getParameter("currentPage");
// 카테고리별로 뉴스 데이터가 필요하여 카테고리를 가져옴
String category = req.getParameter("category");
// 페이지를 누르지 않았을 때 초기값이 null이여서 에러가 나기때문에 if문으로 처리, String이라서 문자열로 비교를 해야할 것 같지만 request로 받아온 것이라 비교연산자로 처리
if(currentPageStr != null) {
// AJAX로 받아온 현재 페이지 번호를 int형으로 변환 ( 페이지 번호 설정을 INT로 하였음 )
currentPage = Integer.parseInt(currentPageStr);
}
// 페이징 설정을 위해 해당 카테고리의 뉴스 데이터 size를 가져옴
int newsSize = newsServiceImpl.newsDataSize(category);
// PageDTO 생성자에 해당 뉴스 데이터 size와 현재 페이지를 넘겨주어 PageDTO에 페이지 설정을 한다.
PageDTO page = new PageDTO(newsSize, currentPage);
// DB 에서 페이지 번호에 따라 가져올 데이터의 범위를 지정하기 위한 변수
int startIndex = (currentPage - 1) * page.getPostPerPage();
int countIndex = page.getPostPerPage();
// 해당 범위의 뉴스 데이터 가져옴
ArrayList<CrawlingDTO> postsPerPage = new ArrayList<CrawlingDTO>();
postsPerPage = newsServiceImpl.postsPerPage(category, startIndex, countIndex);
/*
* AJAX 요청은 비동기적으로 데이터를 주고받기 때문에, 여러 데이터를 한 번에 받아오는 것이 효율적인데,
* Map<String,Object>를 사용하면 한 번의 요청으로 필요한 모든 데이터를 받을 수 있다.
*
* */
Map<String, Object> response = new HashMap<>();
response.put("page", page);
response.put("postsPerPage", postsPerPage);
return response;
}
AJAX에서 클릭한 페이지 번호 데이터와 해당 뉴스가 어떤 뉴스인지 category 데이터를 받아와서 페이지 설정과 해당 데이터를 DB에서 가져오며 그 데이터를 Map으로 저장하였다.
Service와 Mybatis로 데이터를 가져온 것은 따로 설명하지 않겠다.
우선 AJAX를 사용하기 위해서는 JQuery 라이브러리가 필요하다.
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
그럼 이제 AJAX로 어떻게 처리했는지 확인해보자
<script type="text/javascript">
// 페이징 + 페이지별 뉴스 데이터 가져오기 ( 카테고리별 )
function page(pageNum, category) {
$.ajax({
type:'GET', // 타입 ( GET, POST, PUT 등... )
url:'/newsList.do', // 요청할 URL 서버
async:true, // 비동기화 여부 ( default : true ) / true : 비동기 , false : 동기
dataType:'json', // 데이터 타입 ( HTML, XML, JSON, TEXT 등 .. )
data: { // 보낼 데이터 설정
'currentPage' : pageNum,
'category' : category
},
success: function(page) { // 결과 성공 콜백함수
let postsPerPages = page.postsPerPage;
let newsSB = new StringBuilder();
postsPerPages.forEach(postsPerPage => {
newsSB.append("<tr>");
newsSB.append("<td>" + postsPerPage.newsId + "</td>");
newsSB.append("<td class='tit'>");
newsSB.append("<a href='/newsView.do?newsId=" + postsPerPage.newsId + "'><img class='board_list_image' src='" + postsPerPage.main_image + "'>" + postsPerPage.subject + "</a>");
newsSB.append("</td>");
newsSB.append("<td>" + postsPerPage.writer + "</td>");
newsSB.append("<td>" + postsPerPage.date + "</td>");
newsSB.append("<td>" + postsPerPage.view_count + "</td>");
newsSB.append("</tr>");
});
// 해당 페이지와 카테고리별 데이터를 가져온다.
document.getElementById(category + "TableBody").innerHTML = newsSB.toString();
// 페이징
let pagination = page.page;
let pageSB = new StringBuilder();
pageSB.append("<a href='#' class='bt' onclick='page("+1+",\""+category+"\")'>첫 페이지</a>");
// 시작 페이지가 1보다 클 경우에는 이전 페이지가 실행이 되지만 그러지 않을 경우에는 반응하지 않게 설정
if(pagination.startPage > 1) {
pageSB.append("<a href='#' class='bt' onclick='page("+(pageNum - 1)+",\""+category+"\")'>이전 페이지</a>");
} else {
pageSB.append("<a href='#' class='bt disabled'>이전 페이지</a>");
}
// 페이지가 5개씩 보이게 설정하였으며 시작 페이지 ~ 끝페이지(5페이지)가 뜨도록 반복문을 통해 처리
for(let i = pagination.startPage; i <= pagination.endPage; i++) {
// 매개변수로 받은 현재페이지번호(pageNum)와 i의 값이 같으면 class='num on'을 통해 현재 페이지 색깔 넣어줌
if(pageNum == i) {
pageSB.append("<a href='#' class='num on'>" + i + "</a>");
} else { // 현재페이지가 아닌 번호에는 클릭시 카테고리와 페이지 번호를 page함수로 다시 전달하여 페이지 이동이 가능하도록 처리
pageSB.append("<a href='#' class='num' onclick='page("+i+",\""+category+"\")'>" + i + "</a>");
}
}
// 끝페이지가 전체 페이지보다 작다면 다음 페이지 버튼이 작동하도록 처리 아닐경우 무반응
if(pagination.endPage < pagination.totalPages) {
pageSB.append("<a href='#' class='bt' onclick='page("+(pageNum + 1)+",\""+category+"\")'>다음 페이지</a>");
} else {
pageSB.append("<a href='#' class='bt disabled'>다음 페이지</a>");
}
// 마지막 페이지로가기는 전체 페이지의 수를 넣어 마지막페이지로 가도록 처리
pageSB.append("<a href='#' class='bt' onclick='page("+pagination.totalPages+",\""+category+"\")'>마지막 페이지</a>");
document.getElementById(category + "Paging").innerHTML = pageSB.toString();
$("#"+ category +"_board-size").text(pagination.totalPost+"개의 게시글");
},
error: function(error) { // 결과 에러 콜백함수
console.log("AJAX 요청 실패");
}
});
}
// StringBuilder 유틸리티
class StringBuilder {
constructor() {
this._stringArray = [];
}
append(string) {
this._stringArray.push(string);
}
toString() {
return this._stringArray.join("");
}
}
</script>
function page(pageNum, category){}
function page(... , ...) 함수에 매개변수로 해당 클릭한 페이지 번호와 어떤 뉴스인지 category를 넣어주어 요청할 url 서버로 보내준다. 그리고 아까 Controller에서 Map을 사용하여 JSON형식으로 데이터를 저장하였는데 dataType을 보면 JSON으로 설정되어있는 것을 볼 수 있다.
data
그리고 data 를 통해 받았던 매개변수의 값을 요청 url로 보내주었다.
success
AJAX 요청 성공시 요청 url에서 성공적으로 데이터를 받아왔다면 success가 실행이 되어 page에 값이 담기며 그 값을 필요한대로 활용하여 코드를 작성하면 된다.
error
요청이 처리가 되지 않으면 error가 실행이 된다.
<li class="nav-item">
<a class="board-nav-link" data-bs-toggle="tab" href="#veterinary_field" onclick="page(1, 'veterinary_field')">수의계</a>
</li>
나는 수의계라는 뉴스 카테고리 말고도 5개가 더 있는데 각각 onclick으로 page함수를 호출하며 매개변수로 값을 전달해주었다.
<tbody id="industryTableBody">
</tbody>
이런식으로 넣어주면 데이터를 삽입해줄 수 있다.
혹시 내가 정리한 것이 틀린 것이 있다면 바로 피드백 해주시면 감사하겠습니다 !