elevne's Study Note

Pagination with Java & Javascript Ajax 본문

Frontend/JS & React

Pagination with Java & Javascript Ajax

elevne 2022. 11. 3. 23:03

게시판과 같은 List들을 쭉 뽑아와야 할 때에는 거의 대부분 Pagination 작업이 필요하다. 한 화면에 전체 데이터를 불러와서 띄우게 되면 로딩 속도도 많이 느려지고, 보기에도 매우 불편하다.



Paging 처리를 하기 위해서는 ROW_NUM을 SELECT 문에서 가져올 필요가 있다. 쿼리는 아래와 같이 작성해줄 수 있다. (아래 예는 Oracle의 경우)


SELECT * FROM (
SELECT ROW_NUMBER() OVER(ORDER BY COL1) AS ROW_NUM
, COL2
, COL3
FROM TABLE1
)
WHERE ROW_NUM BETWEEN 0 AND 10




위와 같은 쿼리를 실행하게 되면 ROW_NUMBER() OVER()을 통해 ROW_NUM을, COL1 기준으로 가져오게 된다. 그럼 그 ROW_NUM을 기준으로 특정 값과 값 사이의 데이터들만 가져와줄 수 있는 것이다.



위 쿼리를 잘 활용하기 위해 Pagination을 위한 Java Class를 만들어줄 필요가 있다. 다음과 같이 Pagination.java 라는 이름의 파일을 만들어줄 수 있다.


package com.kr.lgcare.qrmanage.web.paging;
 
public class Pagination {
 
    // 현재 페이지
    private int currentPage;
    // 페이지당 출력할 데이터 갯수(e.g., 10개씩, 20개씩...)
    private int cntPerPage;
    // 화면 하단 페이지 사이즈 (e.g., 1~10, 10~20 20~30)
    private int pageSize;
    // 전체 데이터의 개수 
    private int totalRecordCount;
    // 전체 페이지의 개수 
    private int totalPageCount;
    // 페이지 리스트의 첫 페이지 번호
    private int firstPage;
    // 페이지 리스트의 마지막 페이지 번호
    private int lastPage;
    // SQL의 조건절에 사용되는 첫 ROW_NUM
    private int firstRecordIndex;
    // SQL의 조건절에 사용되는 마지막 ROW_NUM
    private int lastRecordIndex;
    // 이전 페이지 존재 여부 
    private boolean hasPreviousPage;
    // 다음 페이지 존재 여부
    private boolean hasNextPage;
    
    public Pagination(int currentPage, int cntPerPage, int pageSize) {
        //강제입력방지
        if (currentPage < 1) {
            currentPage = 1;
        }
        // 10, 20, 30개 단위 이외 처리 방지
        if (cntPerPage != 10 && cntPerPage != 20 && cntPerPage != 30) {
            cntPerPage = 10;
        }
        // 하단 페이지 갯수 10개로 제한
        if (pageSize != 10) {
            pageSize = 10;
        }
        this.currentPage = currentPage;
        this.cntPerPage = cntPerPage;
        this.pageSize = pageSize;   
    }
 
    public void setTotalRecordCount(int totalRecordCount) {
        this.totalRecordCount = totalRecordCount;
        if (totalRecordCount > 0) {
            calculation();
        }
    }
 
    private void calculation() {
        // 전체 페이지 수 (현재 페이지 번호가 전체 페이지 수보다 크면 현재 페이지 번호에 전체 페이지 수를 저장)
        totalPageCount = ((totalRecordCount - 1) / this.getCntPerPage()) + 1;
        if (this.getCurrentPage() > totalPageCount) {
            this.setCurrentPage(totalPageCount);
        }
        // 페이지 리스트의 첫 페이지 번호
        firstPage = ((this.getCurrentPage() - 1) / this.getPageSize()) * this.getPageSize() + 1;
        // 페이지 리스트의 마지막 페이지 번호 (마지막 페이지가 전체 페이지 수보다 크면 마지막 페이지에 전체 페이지 수를 저장)
        lastPage = firstPage + this.getPageSize() - 1;
        if (lastPage > totalPageCount) {
            lastPage = totalPageCount;
        }
        // SQL의 조건절에 사용되는 첫 RNUM
        firstRecordIndex = (this.getCurrentPage() - 1) * this.getCntPerPage();
        // SQL의 조건절에 사용되는 마지막 RNUM
        lastRecordIndex = this.getCurrentPage() * this.getCntPerPage();
        // 이전 페이지 존재 여부
        hasPreviousPage = firstPage == 1 ? false : true;
        if(hasPreviousPage == false) {
            if(currentPage != firstPage) {
                hasPreviousPage = true;
            }else {
                hasPreviousPage = false;
            }
        }
        // 다음 페이지 존재 여부
        hasNextPage = (lastPage * this.getCntPerPage()) >= totalRecordCount ? false : true;
        if(hasNextPage == false) {
            //마지막 페이지에서 현재페이지가 마지막 페이지가 아닌경우 next처리
            if(currentPage != lastPage) {
                hasNextPage = true;
            }else {
                hasNextPage = false;
            }
        }
    }
    
   	Getter Setter~~
 }




위 처럼 필요한 값들을 설정 해줄 수 있을 뿐만 아니라, 다양한 함수를 만들어서 이 객체를 적절하게 활용할 수 있게끔 한다. 코드를 하나씩 읽어보면 어렵지 않게 이해할 수 있다.



Pagination 처리는 다양한 페이지에서 공통적으로 활용하는 경우가 많기 때문에 위처럼 객체화하여 사용하는 것이 편할 것이다. 나의 경우에는 이 Pagination 내 변수들에 Default 값들을 Setting 해준 후, 다른 DTO 객체를 활용하여 Query를 실행할 수 있도록 하였다. Controller는 아래와 같이 작성할 수 있었다.


@RequestMapping(value = "/search")
public String multiInputList(@ModelAttribute("searchDTO") SearchDTO dto, ModelMap model) throws Exception {

    Pagination paginationInfo = new Pagination();
    paginationInfo.setCurrentPageNo(dto.getPageIndex());
    paginationInfo.setRecordCountPerPage(dto.getPageUnit());
    paginationInfo.setPageSize(dto.getPageSize());

    dto.setFirstIndex(paginationInfo.getFirstRecordIndex());
    dto.setLastIndex(paginationInfo.getLastRecordIndex());
    dto.setRecordCountPerPage(paginationInfo.getRecordCountPerPage());

    // Service에서 만들어둔 검색 Function
    List<ResultObj> resultList = boardService.searchList(dto);
    if(resultList.size() == 0) {
        paginationInfo.setTotalRecordCount(0);
    } else {
        paginationInfo.setTotalRecordCount(resultList.get(0).getRowCnt());
    }

    model.addAttribute("resultList", resultList);
    model.addAttribute("paginationInfo", paginationInfo);

    return "example/index";
}




MyBatis에서 SearchDTO 객체를 받아서 Query를 돌릴 수 있게끔 작성해두었다. model 안에 검색결과와 함께 pagination에 대한 정보도 같이 넘겨준다. 그럼 해당 정보들을 토대로 어렵지 않게 Pagination 관련 처리를 html 상에서 처리할 수 있을 것이다.



위 코드들을 토대로 간단한 페이징 처리를 해줄 수 있긴 하지만, Ajax로 검색을 하고 이를 페이징 처리해야 하는 경우에는 위와 같은 방법을 똑같이 사용할 수 없다. Ajax로 불러온 값들을 Paging 처리 해주기 위해 다음과 같은 함수를 하나 만들어준다.


function getPagination(function, paginationInfo){
	var pagination = "";
	var firstPageNo = 1;
	var currentPageNo = paginationInfo.currentPageNo;
	var firstPageNoOnPageList = paginationInfo.firstPageNoOnPageList;
	var lastPageNoOnPageList = paginationInfo.lastPageNoOnPageList;
	var pageSize = paginationInfo.pageSize;
	var totalPageCount = paginationInfo.totalPageCount;
	var lastPageNo = totalPageCount;
	
	/* 총 페이지 수가 화면에 한 번에 표시하는 페이지 수보다 클 때 */
	if (totalPageCount > pageSize) {
    	/* 첫 번째로 표시하는 페이지 넘버가 화면에 한 번에 표시하는 페이지 수보다 클 때 */
		if (firstPageNoOnPageList > pageSize) {
        	/* 첫 번째 페이지로 이동하는 버튼 */
			pagination += "<li><a href='?pageIndex=1' onclick='"+function+"(1); return false;'><i aria-hidden='true'></i></a></li>";
            /* 1개 전 페이지로 이동하는 버트 */
			pagination += "<li><a href='?pageIndex="+(firstPageNoOnPageList - 1)+"' onclick='"+function+"("+(firstPageNoOnPageList - 1)+");  return false;'>Prev</a></li>";
		} else {
        	/* 그 외의 경우에는 두 버튼 다 1번 페이지로 이동하기 */
			pagination += "<li><a href='?pageIndex=1' onclick='"+function+"(1); return false;'><i aria-hidden='true'></i></a></li>";
			pagination += "<li><a href='?pageIndex=1' onclick='"+function+"(1); return false;'>Prev</a></li>";
		}
	}
	
    /* 그 후 첫 번째 표시 페이지~마지막 표시페이지 버튼 추가해주기 */
	for (var i = firstPageNoOnPageList; i <= lastPageNoOnPageList; i++) {
    	/* 현재 페이지를 누를 때는 아무것도 실행 안되게끔 */
		if (i == currentPageNo) {
			pagination += "<li><a onClick='return false;'>"+i+"</a></li>";
		} else {
        	/* Pagination~ */
			pagination += "<li><a href='?pageIndex="+i+"' onclick='"+function+"("+i+"); return false;'>"+i+"</a></li>";
		}
	}
	
    /* 총 페이지 수가 화면에 한 번에 표시하는 페이지 수보다 클 때 */
	if (totalPageCount > pageSize) {
    	/* 마지막 페이지 번호가 화면에 한 번에 표시하는 페이지 수보다 작을 때 */
		if (lastPageNoOnPageList < totalPageCount) {
        	/* 다음 페이지로 이동 */
			pagination += "<li><a href='?pageIndex="+(firstPageNoOnPageList + pageSize)+"' onclick='"+function+"("+(firstPageNoOnPageList + pageSize)+"); return false;'>Next</a></li>";
			/* 마지막 페이지로 이동 */
            pagination += "<li><a href='?pageIndex="+lastPageNo+"' onclick='"+function+"("+lastPageNo+"); return false;'><i aria-hidden='true'></i></a></li>";
		} else {
			pagination += "<li><a href='?pageIndex="+lastPageNo+"' onclick='"+function+"("+lastPageNo+"); return false;'>Next</a></li>";
			pagination += "<li><a href='?pageIndex="+lastPageNo+"' onclick='"+function+"("+lastPageNo+"); return false;'><i aria-hidden='true'></i></a></li>";
		}
	}
	
	return pagination;
}




위 함수는 function, paginationInfo 두 개의 인자를 받는다. 여기서 function에는 ajax를 통해 검색결과 리스트 및 Pagination 정보를 불러오는 함수를 넣어준다. 그리고 paginationInfo에는 그 함수를 실행시키고 얻은 data 안에 담긴 paginationInfo를 넣어주는 것이다. (위 함수는 ajax 사용 함수 success 부분 안에 들어가게 될 것임)



Ajax를 사용하여 리스트, Pagination 정보를 가져오는 함수는 다음과 같은 형식으로 작성할 수 있다.


function getData(pageNo){
	// 결과 리스트 넣을 곳 초기화
	$("#result").empty();
	$("#resultPagination").empty();
	
    // 검색어 가져오기
	var tmpSearchTerm = $("#searchTerm").val();
    
    // ajax 시작
	$.ajax({
		type: "POST",
		url: "/searchAjax",
		dataType: "json",
		data: {
			searchTerm : tmpSearchTerm,
			pageIndex : pageNo
		},
		success: function(data){
			if (data.resultList.length > 0){
				for (var obj of data.resultList){
					var trString = "";
					trString += "<tr>";
					trString += "<td>"+obj.id+"</td>";
					trString += "<td>"+obj.title+"</td>";
					trString += "<td>"+obj.content+"</td>";
					trString += "<td>"+obj.date+"</td></tr>";
					$("#result").append(trString);
				}
				$("#resultPagination").html(getPagination("getData", data.paginationInfo));
			} else {
				$("#result").append("<tr><td colspan='4'><br/>No Result For Search</td></tr>");
			}
		},
		error: function(data){
			alert("검색 실패");
		}
	});
}




위 코드 또한 어렵지 않게 이해할 수 있을 것이다. 위 함수를 보면 Ajax를 사용하고 있고, Paination을 처리해줄 부분에 위에서 만든 함수인 getPagination을 사용하고, 인자로 getData(위 함수 이름)과 ajax를 통해 가져온 paginationInfo 객체를 넘겨주고 있는 것을 확인할 수 있다. 마지막으로, Controller에서는 아래와 같이 처리해줄 수 있을 것이다.


@RequestMapping(value = "/searchAjax")
public ModelAndView setMultiRearingList(@ModelAttribute("searchDto") SearchDTO dto, Model model) throws Exception {

    Pagination paginationInfo = new PaginationInfo();
    paginationInfo.setCurrentPageNo(dto.getPageIndex());
    paginationInfo.setRecordCountPerPage(dto.getPageUnit());
    paginationInfo.setPageSize(dto.getPageSize());
    dto.setFirstIndex(paginationInfo.getFirstRecordIndex());
    dto.setLastIndex(paginationInfo.getLastRecordIndex());
    dto.setRecordCountPerPage(paginationInfo.getRecordCountPerPage());

    List<DataObj> resultList = searchService.search(dto);
    if (resultList.size() == 0) {
        paginationInfo.setTotalRecordCount(0);
    } else {
        paginationInfo.setTotalRecordCount(resultList.get(0).getRowCnt());
    }
    
    model.addAttribute("resultList", resultList);
    model.addAttribute("paginationInfo", paginationInfo);
    ModelAndView modelAndView = new ModelAndView("jsonView", model);
    return modelAndView;
}





출처:
https://hanhyx.tistory.com/46