Skip to the content.

URL으니 파라미터를 이용해서 정상적으로 원하는 페이지로 이동하는 것을 확인했다면

화면 밑에 페이지 번호를 표시하고 사용자가 페이지 번호를 클릭할 수 있게 처리한다.

페이지를 보여주는 작업은 다음과 같은 과정을 통해서 진행한다.

  1. 브라우저 주소창에서 페이지 번호를 전달해서 결과를 확인하는 단꼐
  2. JSP에서 페이지 번호를 출력하는 단계
  3. 각 페이지 번호에 클릭 이벤트 처리
  4. 전체 데이터 개수를 반영해서 페이지 번호 조절

패이지 처리는 단순히 링크의 연결이기 때문에 어렵지는 않지만

다음 그림과 같이 목록 페이지에서 조회 페이지,수정 삭제 페이지까지 페이지 번호가 계속해서 유지되어야만 하기 때문에 끝까지 싱경써야 하는 부분들이 많은 편이다.다음 그림은 페이지 번호에 어떤 작업을 하던 유지되면서 링크가 연결되는 모습이다.

페이징 처리할 때 필요한 정보들

화면에 체이징 처리를 하기 위해서는 우선적으로 여러 가지 필요한 정보들이 존재한다.

화면에 페이지는 크게 다음과 같은 정보들이 필요하다.

  1. 현재 페이지 번호(page)
  2. 이전과 다음으로 이동 가능한 링크의 표시(prev,next)
  3. 화면에서 보여지는 페이지의 시작 번호와 끝 번호(startPage,endPage)

끝 페이지 번호와 시작 페이지 번호

페이징 처리를 하기 위해서 우선적으로 필요한 정보는 현재 사용자가 보고 있는 페이지의 정보이다.

예를 들어 사용자가 5페이지를 본다면 화면의 페이지 번호는 1부터 시작하지만,사용자가 19페이지를 본다면 11부터 시작해야 하기 때문이다.

흔히들 페이지를 계산할때 시작번호를 먼저 하려고 하지만 오히려 끝 번호를 먼저 계산해 두는 것이 수월하다.

끝 번호는 다음과 같은 공식으로 구할 수 있다.

this.endPage = (int)(Math.ceil(페이지번호/10.0))*10;

Math.ceil()은 소수점을 올림으로 처리하기 때문에 다음과 같은 상황이 가능하다.

끝 번호는 아직 개선의 여지가 있다.만일 전체 데이터 수가 적다면 10페이지로 끝나면 안되는 상황이 생길 수도 있기 때문이다.그럼에도 끝 번호를 먼저 계산하는 이유는 시작 번호를 계산하기 수월하기 때문이다.

만일 화면에 10개씩 보여준다면 시작 번호는 무조건 끝 번호에서 9라는 값을 뺀 값이다.

this.startPage = this.endPage-9;

끝 번호는 전체 데이터 수에 의해서 영향을 받는다.예를 들어 10개씩 보여주는 경우 전체 데이터 수가 80개라고 가정하면 끝 번호는 10이 아닌 8이 되어야만 한다.

만일 끝 번호와 한 페이지당 출력되는 데이터 수의 곱이 전체 데이터 수보다 크다면 끝번호는 다시 total을 이용해서 다시 계산되어야 한다.

realEnd = (int)(Math.ceil((total*1.0)/amount));
if(realEnd <this.endPage){
this.endPage = realEnd;
}

먼저 전체 데이터 수(total)를 이용해서 진짜 끝 페이지가 몇 번까지 되는지를 계산한다.

만일 진짜 끝 페이지가 구해둔 끝 번호보다 작다면 끝번호는 작은 값이 되어야만 한다.

이전(prev)과 다음(next)

이전과 다음은 아주 간단히 구할 수 있다.

이전의 경우는 시작 번호가 1보다 큰 경우라면 존재하게 된다.

this.prev = this.startPage>1;

다음으로 가는 링크의 경우 위의 realEnd가 끝번호보다 큰 경우에만 존재하게 된다.

this.next = this.endPage<realEnd;

페이징 처리를 위한 클래스 설계

화면에 페이징 처리를 위해서 위와 같이 여러 정보가 필요하다면 클래스를 구성해서 처리하는 방식도 꽤 편한 방식이 될 수 있다.클래스를 구성하면 Controller 계층에서 JSP화면에 전달할 때에도 객체를 생성해서 Model에 담아 보내는 과정이 단순해지는 장점도 있다.

com.osk2090.domain 패키지에 PageDTO 클래스를 설계한다.

PageDTO.class

package com.osk2090.domain;

import lombok.Getter;
import lombok.ToString;

@Getter
@ToString
public class PageDTO {
    private int startPage;
    private int endPage;
    private boolean prev, next;

    private int total;
    private Criteria cri;

    public PageDTO(int total, Criteria cri) {
        this.total = total;
        this.cri = cri;

        this.endPage = (int) (Math.ceil(cri.getPageNum() / 10.0)) * 10;
        this.startPage = this.endPage - 9;
        int realEnd = (int) (Math.ceil(total * 1.0) / cri.getAmount());

        if (realEnd < this.endPage) {
            this.endPage = realEnd;
        }

        this.prev = this.startPage > 1;
        this.next = this.endPage < realEnd;
    }
}

PageDTO는 생성자를 정의하고 Criteria와 전체 데이터 수를 파라미터로 지정한다.

Criteria안에는 페이지에서 보여주는 데이터 수와 현재 페이지 번호를 가지고 있기 때문에 이를 이용해서 필요한 모든 내용을 계산할 수 있다.

BoardController에서는 PageDTO를 사용할 수 있도록 Model에 담아서 화면에 전달해 줄 필요가 있다.

BoardController.class

@GetMapping("/list")
public void list(Criteria cri, Model model) {
    log.info("list: " + cri);
    model.addAttribute("list", service.getList(cri));
    model.addAttribute("pageMaker", new PageDTO(cri, 123));
}

list()는 pageMaker라는 이름으로 PageDTO 클래스에서 객체를 만들어서 Model에 담아준다.

PageDTO를 구성하기 위해서는 전체 데이터 수가 필요한데 아직 그 처리가 이루어지지 않았으므로 임의의 값으로 123을 지정했다.

JSP에서 페이지 번호 출력

JSP에서 페이지 번호를 출력하는 부분은 JSTL을 이용해서 처리할 수 있다.

기존의 <table> 태그가 끝나는 직후에 페이지 처리를 추가한다.

list.jsp

     </c:forEach>
                </table>
                
                <div class="pull-right">
                    <ul class="pagination">
                        <c:if test="${pageMaker.prev}">
                            <li class="paginate_button previous">
                                <a href="#">Previous</a> 
                            </li>
                        </c:if>
                        
                        <c:forEach var="num" begin="${pageMaker.startPage}"
                        end="${pageMaker.endPage}">
                            <li class="paginate_button">
                                <a href="#">${num}</a>
                            </li>
                        </c:forEach>
                        
                        <c:if test="${pageMaker.next}">
                            <li class="paginate_button next">
                                <a href="#">Next</a>
                            </li>
                        </c:if>
                    </ul>
                </div>

Model 창의 아래쪽에 별도의 <div class="row">를 구성하고 페이지 번호들을 출력한다.

pageMaker라는 이름으로 전달된 PageDTO를 이용해서 화면에 페이지 번호들을 출력한다.

예를들어 현재 total은 123이라는 숫자로 지정되어 있으므로 5페이지를 조회하면 next값은 true가 되어야 한다.

반면에 amount 값이 20인 경우에는 7페이지까지만 출력되어야 한다.

페이지 번호 이벤트 처리

화면에서 페이지 번호가 보이기는 하지만 아직 페이지 번호를 클릭했을 때 이벤트 처리가 남아있다.

일반적으로 태그의 href 속성을 이용하는 방법을 사용할 수도 있지만 직접 링크를 처리하는 방식의 경우 검색 조건이 붙고 난 후에 처리가 복잡하게 되므로 JS를 통해서 처리하는 방식을 이용한다.

우선 페이지와 관련된 태그의 href 속성값으로 페이지 번호를 가지도록 수정한다.

(번호의 출력 부분은 을 통해서 출력하는 것이 좋지만 예제에서는 가독성의 문제로 일반 EL을 이용한다.)

list.jsp

<c:if test="${pageMaker.prev}">
    <li class="paginate_button previous">
    	<a href="${pageMaker.startPage-1}">Previous</a>
    </li>
</c:if>

<c:forEach var="num" begin="${pageMaker.startPage}" end="${pageMaker.endPage}">
    <li class="paginate_button ${pageMaker.cri.pageNum == num?"active":""}">
    	<a href="${num}">${num}</a>
    </li>
</c:forEach>

<c:if test="${pageMaker.next}">
    <li class="paginate_button next">
    	<a href="${pageMaker.endPage+1}">Next</a>
    </li>
</c:if>

이제 화면에서는 태그는 href 속성값으로 단순히 번호만을 가지게 변경된다.

이 상태에서 페이지 번호를 클릭하게 되면 해당하는 URL이 존재하지 않기 때문에 문제가 생기게 된다.

태그가 원래의 동작을 못하도록 JS 처리를 한다.실제 페이지를 클릭하면 동작을 하는 부분은 별도의 <form> 태그를 이용해서 처리하도록 한다.

(을 사용하는 것이 더 좋은 방법이지만 간단히 사용하기 위해서 EL로 처리한다.)

list.jsp

<form id="actionForm" action="../board/list" method="get">
    <input type="hidden" name="pageNum" value="${pageMaker.cri.pageNum}">
    <input type="hidden" name="amount" value="${pageMaker.cri.amount}">
</form>

기존에 동작하던 JS 부분은 아래와 같이 기존의 코드에 페이지 번호를 클릭하면 처리하는 부분이 추가된다.

list.jsp

<script type="text/javascript">
    $(document).ready(
        function () {
       ...

            var actionForm = $("#actionForm");
            $(".paginate_button a").on("click", function (e) {
                e.preventDefault();
                console.log('click');
                actionForm.find("input[name='pageNum']").val($(this).attr("href"));
            });
        });
</script>

list.jsp에서는 <form> 태그를 추가해서 URL의 이동을 처리하도록 변경했다.JS에서는 태그를 클릭해도 페이지 이동이 없도록 preventDefault() 처리를 하고 <form> 태그 내 pageNum 값은 href 속성값으로 변경한다.이 처리를 하고나면

화면에서 페이지 번호를 클릭했을 때 <form> 태그 내의 페이지 번호가 바뀌는 것을 브라우저에서 개발자 도구를 통해 확인할 수 있다.

마지막 처리는 actionForm 자체를 submit() 시켜야 한다.

var actionForm = $("#actionForm");
    $(".paginate_button a").on("click", function (e) {
        e.preventDefault();
        console.log('click');
        actionForm.find("input[name='pageNum']").val($(this).attr("href"));
        actionForm.submit();
});

조회 페이지로 이동

목록 화면에서 페이지 번호를 클릭하면 정상적으로 원하는 페이지로 이동하는 것을 볼 수 있지만 몇가지 문제가 있다.

우선 사용자가 3페이지에 있는 게시글을 클릭한 후 다시 목록으로 이동해 보면 무조건 1페이지 목록 페이지로 이동하는 현상이 있다.

페이징 처리를 하고나면 특정 게시물의 조회 페이지로 이동한 후 다시 목록으로 돌아가는 문제가 생긴다.

조회 페이지에서 List를 선택하면 다시 1페이지의 상태로 돌아가는 문제가 발생하는 것을 볼 수 있다.

이를 해결하기 위해서는 조회 페이지로 갈 때 현재 목록 페이지의 pageNum과 amount를 같이 전달해야 한다.

이런 경우 페이지 이동에 사용했던 <form> 태그에 추가로 게시물의 번호를 같이 전송하고

action 값을 조정해서 처리할 수 있다.

원래 게시물의 제목에는 /board/get?bno=숫자 로 이동할 수 있는 링크가 직접 처리되어 있었다.

list.jsp 참고

<td><a href='../board/get?bno=<c:out value="${board.bno}"/>'>
<c:out value="${board.title}"/><a/></td>

페이지 번호는 조회 페이지에 전달되지 않기 때문에 조회 페이지에서 목록 페이지로 이동할 때는 아무런 정보가 없이 다시 /board/list를 호출하게 된다.

간단하게는 각 게시물의 링크에 추가로 &pageNum=숫자 와 같이 처리할 수도 있지만 나중에 여러 조건들이 추가되는 상황에서는 복잡한 링크를 생성해야만 한다.

태그로 복잡한 링크를 생성하는 방식이 나쁘다고는 말할 수 없다.가장 대표적인 예가 검색엔진이다.검색엔진에는 출력된 정보와 링크를 저장해서 사용하기 때문에 태그 내의 링크가 완전한 URL인 경우가 노출에 유리하다.

만일 웹페이지가 검색엔진에 의해서 노출이 필요한 경우라면 직접 모든 문자열을 구성해 주는 방식이 더 좋다.

직접 링크로 연결된 경로를 페이지 이동과 마찬가지로 <form> 태그를 이용해서 처리할 것이므로 태그에는 이동하려는 게시물의 번호만을 가지게 수정한다.(이벤트 처리를 수월하게 하기 위해서 태그에 class 속성을 하나 부여한다.)

list.jsp

<td><a class="move" href='<c:out value="${board.bno}"/>'>
		<c:out value="${board.title}"/></a>
</td>

화면에서는 조회 페이지로 가는 링크 대신에 단순히 번호만이 출력된다.

실제 클릭은 JS를 통해서 게시물의 제목을 클릭했을 때 이동하도록 이벤트 처리를 새로 작성한다.

list.jsp

$(".move").on("click", function (e) {
        e.preventDefault();
        actionForm.append("<input type='hidden' name='bno' value='" +
            $(this).attr("href") + "'>')");
        actionForm.attr("action", "../board/get");
        actionForm.submit();
    });

게시물의 제목을 클릭하면 <form> 태그에 추가로 bno 값을 전송하기 위해서 태그를 만들어 추가하고

태그의 action은 /board/get 으로 변경한다.위의 처리가 정상적으로 되었다면 게시물의 제목을 클릭했을 때 pageNum과 amount 파라미터가 추가로 전달되는 것을 볼 수 있다. ```html http://localhost:8080/jex02_war_exploded/board/get?pageNum=5&amount=10&bno=8914931 ``` #### 조회 페이지에서 다시 목록 페이지로 이동 - 페이지 번호 유지 조회 페이지에 다시 목록 페이지로 이동하기 위한 파라미터들이 같이 전송되었다면 조회 페이지에서 목록으로 이동하기 위한 이벤트를 처리해야 한다.BoardController의 get() 메서드는 원래는 게시물의 번호만 받도록 처리되어 있었지만 추가적인 파라미터가 붙으면서 Criteria를 파라미터로 추가해서 받고 전달한다. BoardController.class ```java @GetMapping({"/get", "/modify"}) public void get(@RequestParam("bno") Long bno, @ModelAttribute("cri") Criteria criteria, Model model) { log.info("/get or modify"); model.addAttribute("board", service.get(bno)); } ``` @ModelAttribute는 자동으로 Model에 데이터를 지정한 이름으로 담아준다. @ModelAttribute를 사용하지 않아도 Controller에서 화면으로 파라미터가 된 객체는 전달이 되지만 좀 더 명시적으로 이름을 지정하기 위해서 사용한다. 기존 get.jsp에서는 버튼을 클릭하면 태그를 이용하는 방식이었으므로 필요한 데이터를 추가해서 이동하도록 수정한다. get.jsp ```html
``` get.jsp는 openForm이라는 id를 가진
태그를 이미 이용했기 때문에 cri라는 이름으로 전달된 Criteria 객체를 이용해서 pageNum과 amount 값을 태그로 구성하고 버튼을 클릭했을 때 정상적으로 목록 페이지로 이동하게 처리한다. ![](/images/93/img_4.png) 실제 동작은 아래의 그림과 같이 pageNum과 amount 같이 정상적으로 이동되어야 한다. #### 조회 페이지에서 수정/삭제 페이지로 이동 조회 페이지에서는 Modify 버튼을 통해서 수정/삭제 페이지로 이동하게 된다. 수정/삭제 페이지에서는 다시 목록으로 가는 버튼이 존재하므로 동일하게 목록 페이지에 필요한 파라미터들을 처리해야 한다.BoardController에서는 get() 메서드에서 /get 과 /modify를 같이 처리하므로 별도의 추가적인 처리 없이도 Criteria를 Model에 cri라는 이름으로 담아서 전달한다. 조회 페이지에서 태그는 목록 페이지로의 이동뿐 아니라 수정/삭제 페이지 이동에도 사용되기 때문에 파라미터들은 자동으로 같이 전송된다. ### 수정과 삭제 처리 modify.jsp에서는 태그를 이용해서 데이터를 처리한다.거의 입력과 비슷한 방식으로 구현되는데 이제 pageNum과 amount 라는 값이 존재하므로 태그내에서 같이 전송할 수 있게 수정해야 한다. modify.jsp ```java
Board Modify Page
<input type="hidden" name="pageNum" value=""> <input type="hidden" name="amount" value=""> ... ``` modify.jsp 역시 Criteria를 Model에서 사용하기 때문에 위와 같이 태그를 만들어서 태그 전송에 포함한다. #### 수정/삭제 처리 후 이동 POST 방식으로 진행하는 수정과 삭제 처리는 BoardController에서 각각의 메서드 형태로 구현되어 있으므로 페이지 관련 파라미터들을 처리하기 위해서는 변경해 줄 필요가 있다. BoardController.class ```java @PostMapping("/modify") public String modify(BoardVO board, @ModelAttribute("cri") Criteria cri, RedirectAttributes rttr) { log.info("modify:" + board); if (service.modify(board)) { rttr.addFlashAttribute("result", "success"); } rttr.addAttribute("pageNum", cri.getPageNum()); rttr.addAttribute("amount", cri.getAmount()); return "redirect:/board/list"; } ``` 메서드의 파라미터에는 Criteria가 추가된 형태로 변경되고 RediredtAttributes 역시 URL 뒤에 원래의 페이지로 이동하기 위해서 pageNum과 amount 값을 가지고 이동하게 수정한다. 삭제 처리 역시 동일하게 Criteria를 받아들이는 방식으로 수정한다. BoardController.class ```java @PostMapping("/remove") public String remove(@RequestParam("bno") Long bno, @ModelAttribute("cri") Criteria cri, RedirectAttributes rttr) { log.info("remove..." + bno); if (service.remove(bno)) { rttr.addFlashAttribute("result", "success"); } rttr.addAttribute("pageNum", cri.getPageNum()); rttr.addAttribute("amount", cri.getAmount()); return "redirect:/board/list"; } ``` 위와 같은 방식을 이용하면 수정/삭제 후 기존 사용자가 보던 페이지로 이동하는 것을 볼 수 있다. 수정과 달리 삭제는 처리 후 1페이지로 이동해도 무방하지만 이오아이면 사용자들에게 자신이 보던 정보를이어서 볼 수 있게 조치해주는 방식 역시 어렵지 않다. #### 수정/삭제 페이지에서 목록 페이지로 이동 페이지 이동이 마지막은 수정/삭제를 취소하고 다시 목록 페이지로 이동하는 것이다. 목록 페이지는 오직 pageNum과 amount만을 사용하므로 태그의 다른 내용들은 삭제하고 필요한 내용만을 다시 추가하는 형태가 편리하다. modify.jsp ```javascript ``` 만일 사용자가 List 버튼을 클릭한다면 태그에서 필요한 부분만 잠시 복사(clone)해서 보관해 두고 태그 내의 모든 내용을 지워버린다(empty).이후에 다시 필요한 태그들만 추가해서 /board/list를 호출하는 형태를 이용한다. ### MyBatis에서 전체 데이터의 개수 처리 페이지의 이동이 모든 작업에서 정상적으로 이루어지는 것을 확인했다면 최종적으로 데이터베이스에 있는 실제 모든 게시물의 수(total)을 구해서 PageDTO를 구성할 때 전달해주어야 한다. 전체의 개수를 구하는 SQL은 어렵거나 복잡하지 않기 때문에 어노테이션으로 처리해도 무방하지만 BoardMapper 인터페이스에 getTotalCount()에서 메서드를 정의하고 XML을 이용해서 SQL을 처리한다. BoardMapper.interface ```java public int getTotalCount(Criteria cri); ``` getTotalCount()는 Criteria를 파라미터를 전달받도록 설계하지 않아도 문제가 생기지는 않지만 게시물의 목록과 전체 데이터 수를 구하는 작업은 일관성 있게 Criteria를 받는 것이 좋다. BoardMapper.xml ```sql ``` BoardService와 BoardServiceImple에서는 별도의 메서드를 작성해서 BoardMapper와 getTotalCount()를 호출한다. BoardService.interface ```java public int getTotalCount(Criteria cri); ``` BoardService의 getTotal()에 굳이 Criteria는 파라미터로 전달될 필요가 없기는 하지만 목록과 전체 데이터 개수는 항상 같이 동작하는 경우가 많기 때문에 파라미터로 지정한다. BoardServiceImpl 클래스는 getTotal() 메서드를 구현한다. BoardServiceImpl.class ```java @Override public int getTotalCount(Criteria cri) { log.info("get total count"); return mapper.getTotalCount(cri); } ``` BoardController에서는 BoardService 인터페이스를 통해서 getTotal()을 호출하도록 한다. BoardController.class ```java @GetMapping("/list") public void list(Criteria cri, Model model) { log.info("list: " + cri); model.addAttribute("list", service.getList(cri)); // model.addAttribute("pageMaker", new PageDTO(cri, 123)); int total = service.getTotalCount(cri); log.info("total: " + total); model.addAttribute("pageMaker", new PageDTO(cri, total)); } ```