ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 스프링(Spring) 개발 - (18) 페이징 (jQuery와 Ajax)
    Spring 2016.01.24 23:33

    이번글에서는 jQuery를 이용하여 페이징 태그를 만들고 Ajax를 이용하는 방법에 대해서 이야기 합니다.


    사실 jQuery를 이용하여 페이징 태그를 만드는건 실제로 적용을 해본적이 없었습니다. 


    Ajax를 이용한 페이징은 스크롤페이징 기능을 만들기 위해서 해봤었던 거라서 어떠한 구멍이 있을지는 잘 모르겠네요.


    이 글을 보고 이상한점이 발견되면 말씀해주세요. 


    그리고 이번글에서 처음에 jsonView를 설정하는 부분이 있습니다. 


    기존글에서 jsonView를 설정하는데 에러가 발생한다는 분들도 있어서 해당 라이브러리를


    글을 쓰는 현재 최신버전으로 바꿨습니다. 기존에 jsonView가 설정이 되어있던 분들도 해당 내용을 따라해주세요.


    ------------------------------------------------------------------------------------


    1. 공통 기능 

    지난번 글에서 작성한 전자정부 프레임워크를 이용한 페이징과 마찬가지로 공통적으로 사용하는 기능을 먼저 만들도록 하자. 

    거의 대부분은 비슷하지만 약간씩 다른데, 쿼리는 똑같기 때문에 다시 언급하지 않도록 하겠다.


    1. 라이브러리 및 jsonView 설정

    먼저 Ajax를 사용하여 클라이언트와 서버의 데이터 통신을 할때 사용할 jsonView를 설정하려고 한다. JSON이 어떤것인지는 대부분 알것이라 생각해서 특별히 언급을 할 필요는 없을것이라고 생각한다. 


    1) 라이브러리 추가

    먼저 pom.xml에 다음의 내용을 추가한다. 

    <dependency>
    	<groupId>com.fasterxml.jackson.core</groupId>
    	<artifactId>jackson-core</artifactId>
    	<version>2.6.4</version>
    </dependency>
    
    <dependency>
    	<groupId>com.fasterxml.jackson.core</groupId>
    	<artifactId>jackson-databind</artifactId>
    	<version>2.6.4</version>
    </dependency>
    

    이 라이브러리를 이용하면 어떠한 형태의 데이터도 json 형식의 데이터로 자동으로 변환을 해준다. 이것은 나중에 어떻게 보여지는지 살펴보도록 하겠다.


    2) jsonView 설정

    위에서 추가한 라이브러리를 이용하여 jsonView를 설정할 차례이다. 

    action-servlet.xml을 다음과 같이 변경한다.

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns:context="http://www.springframework.org/schema/context"
        xmlns:p="http://www.springframework.org/schema/p"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns="http://www.springframework.org/schema/beans"
        xmlns:mvc="http://www.springframework.org/schema/mvc"
        xmlns:aop="http://www.springframework.org/schema/aop"
        xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd
           http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd
           http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
    
    	<context:component-scan base-package="first" use-default-filters="false">
    		<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
    	</context:component-scan>
    	
    	<mvc:annotation-driven>
    		<mvc:argument-resolvers>
    			<bean class="first.common.resolver.CustomMapArgumentResolver"></bean>		
    		</mvc:argument-resolvers>
    	</mvc:annotation-driven>
    	
    	<mvc:interceptors>
    		<mvc:interceptor>
    			<mvc:mapping path="/**"/>
    			<bean id="loggerInterceptor" class="first.common.logger.LoggerInterceptor"></bean>
    		</mvc:interceptor>
    	</mvc:interceptors>
    	
    	<aop:aspectj-autoproxy/>
    	<bean id="loggerAspect" class="first.common.logger.LoggerAspect" />
    	
        <bean class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping"/>
        
        <bean class="org.springframework.web.servlet.view.BeanNameViewResolver" p:order="0" />
        <bean id="jsonView" class="org.springframework.web.servlet.view.json.MappingJackson2JsonView" />
        
        <bean
        	class="org.springframework.web.servlet.view.UrlBasedViewResolver" p:order="1" 
        	p:viewClass="org.springframework.web.servlet.view.JstlView" 
        	p:prefix="/WEB-INF/jsp/" p:suffix=".jsp">
       	</bean>
    </beans>
    

    36번째 줄에 jsonView라는것이 추가된것을 확인할 수 있다. 여기서 jsonView라는 것은 추후 Controller에서 사용될 것이다.


    2. AbstractDAO

    먼저 페이징을 처리하는 로직을 만들도록 하자. AbstractDAO.java에 다음의 내용을 작성하자.

    @SuppressWarnings("unchecked")
    public Object selectPagingList(String queryId, Object params){
    	printQueryId(queryId);
    	Map<String,Object> map = (Map<String,Object>)params;
    	
    	String strPageIndex = (String)map.get("PAGE_INDEX");
    	String strPageRow = (String)map.get("PAGE_ROW");
    	int nPageIndex = 0;
    	int nPageRow = 20;
    	
    	if(StringUtils.isEmpty(strPageIndex) == false){
    		nPageIndex = Integer.parseInt(strPageIndex)-1;
    	}
    	if(StringUtils.isEmpty(strPageRow) == false){
    		nPageRow = Integer.parseInt(strPageRow);
    	}
    	map.put("START", (nPageIndex * nPageRow) + 1);
    	map.put("END", (nPageIndex * nPageRow) + nPageRow);
    	
    	return sqlSession.selectList(queryId, map);
    }
    

    간단히 소스를 살펴보자.


    먼저 6~16번째 줄이 현재 페이지 번호와 한 페이지에 보여줄 행의 개수를 계산하는 부분이다.

    화면에서 PAGE_INDEX와 PAGE_ROW라는 값을 보내주도록 되어있지만, 혹시 모를 예외상황에 대비하여 해당 값을 각각 0과 20으로 설정하였다.

    그 다음 17, 18번째 줄에서 페이징 쿼리의 시작과 끝값을 계산하도록 하였다. 

    그 후 일반적인 리스트 조회를 호출하도록 하였다.


    전자정부 프레임워크를 사용할때와는 다르게 계산할 것이 많이 없고, 반환값도 List인것을 확인할 수 있다. 


    3. CSS

    사실 여기서는 안해도 되는 부분이지만 나중에 페이징 결과를 좀 더 보기 쉽게 할수있게 스타일시트를 수정한다.

    ui.css를 열어서 다음의 내용을 추가한다.

    .pad_5 {padding: 5px;}
    


    4. 자바스크립트

    이제 이번글의 핵심인 스크립트의 작성 부분이다. 여기서는 크게 두가지에 대해서 이야기를 할것이다.

    첫번째로는 Ajax 호출에 대한 내용이고 두번째는 jQuery를 이용한 페이징 태그를 만드는 함수이다.


    1) Ajax

    먼저 Ajax가 무엇인지 아주 간단히 이야기를 하려고 한다. 

    Ajax는 Asynchronous JavaScript and Xml 의 약자로 클라이언트(웹브라우저)와 서버의 비동기적 통신을 통한 데이터 전송을 이용하는 방법이다. Ajax는 클라이언트와 서버가 내부적으로 데이터 통신을 하고 그 결과를 웹페이지에 프로그래밍적으로 반영한다. 그 결과 화면의 로딩없이 그 결과를 보여줄 수 있다.

    jQuery에서는 Ajax 통신을 쉽게 하는 ajax() 함수를 제공하고 있다. 

    인터넷에서 ajax 함수에 대한 기본적인 설명 및 사용법은 많이 나와있으니 넘어가고, 여기서는 기존의 ComSubmit과 같이 Ajax를 공통함수로 만들어서 사용하려고 한다.

    common.js에 다음의 내용을 작성하자.

    var gfv_ajaxCallback = "";
    function ComAjax(opt_formId){
    	this.url = "";		
    	this.formId = gfn_isNull(opt_formId) == true ? "commonForm" : opt_formId;
    	this.param = "";
    	
    	if(this.formId == "commonForm"){
            var frm = $("#commonForm");
            if(frm.length > 0){
    	        frm.remove();
            }
            var str = "<form id='commonForm' name='commonForm'></form>";
            $('body').append(str);
    	}
    	
    	this.setUrl = function setUrl(url){
    		this.url = url;
    	};
    	
    	this.setCallback = function setCallback(callBack){
    		fv_ajaxCallback = callBack;
    	};
    
    	this.addParam = function addParam(key,value){ 
    		this.param = this.param + "&" + key + "=" + value; 
    	};
    	
    	this.ajax = function ajax(){
    		if(this.formId != "commonForm"){
    			this.param += "&" + $("#" + this.formId).serialize();
    		}
    		$.ajax({
    			url : this.url,    
    			type : "POST",   
    			data : this.param,
    			async : false, 
    			success : function(data, status) {
    				if(typeof(fv_ajaxCallback) == "function"){
    					fv_ajaxCallback(data);
    				}
    				else {
    					eval(fv_ajaxCallback + "(data);");
    				}
    			}
    		});
    	};
    }
    

    간단히 소스를 보자. 

    일단 기본적인 틀은 기존에 만들었던 ComSubmit 객체와 비슷한것을 알 수 있다. 

    여기서 추가된것은 크게 setCallback이라는 함수와 submit() 대신에 ajax() 라는 함수가 추가되었다. 


    먼저 setCallback은 ajax를 이용하여 데이터를 전송한 후 호출될 콜백함수의 이름을 지정하는 함수이다. 

    Ajax는 클라이언트와 비동기적으로 수행되기 때문에 return을 받을수가 없다. 따라서 클라이언트가 서버에 어떠한 동작을 요청하고 그에 따른 결과가 다시 클라이언트측에 전달될 때 호출되는것이 콜백함수다. 여기서는 setCallback이라는 함수를 이용하여 ajax 요청 후 호출될 함수의 이름을 지정하는 것이다.

    이에 대한 내용은 잠시 후 실제 소스에서 다시 보도록 하겠다. 


    그 다음으로는 ajax() 함수이다. 이 함수가 실질적인 ajax 기능을 수행한다. 

    submit의 경우 어떠한 요청을 하면 화면이 바뀌기때문에 그 안의 기능이 많지 않았지만, ajax의 경우는 설정을 해야할 게 몇가지가 있다. 

    jQuery를 이용한 ajax는 여러가지 설정할 수 있는데, 여기서는 간단히 몇가지만 설정을 하였다. 

    url은 호출할 url을 의미하고, type은 POST 또는 GET 방식의 통신을 설정한다. 여기서는 그냥 POST로 지정을 하였다. 

    그리고 data 부분이 ajax를 이용하여 서버에 요청을 할 때 서버로 전달할 인자(Parameter)를 의미한다. 

    원래는 저런 방식으로 하지않고 object 형식으로 data를 지정하지만, 여기서는 addParam또는 form 자체를 전송하기 때문에 저런식으로 하였다.

    그 다음 async는 동기식과 비동기식의 통신방식을 의미한다. 

    동기식은 클라이언트 -> 서버 -> 클라이언트의 과정에서 서버의 답변이 올때까지 다른 일을 수행하지 못하고 기다리기만 하는 방식이고,

    비동기식은 요청을 보내고 다른일을 수행할 수 있다. (이에 대한 좀 더 자세한 설명은 쉽게 찾아볼 수 있으니 이정도만 이야기하려고 한다.)

    여기서는 그냥 비동기식으로 설정을 하였다. 

    여기서 나온 설정 및 다른 설정은 jQuery 공식홈페이지의 ajax 부분을 보면 자세하게 나와있다. (http://api.jquery.com/jquery.ajax/)


    여기까지 해서 ajax 함수에 대한 설명은 간단히 끝났다. 아직은 무슨소리인지 이해가 가지 않는게 많을수도 있고 이걸 어떻게 쓰는지 모르겠지만, 나중에 화면에서 이를 어떻게 사용하는지를 보면서 다시 한번 설명을 하도록 하겠다.


    2) 페이징 태그

    다음으로는 페이징 태그를 만드는 부분이다. 

    프로젝트에서는 페이징을 사용하는 화면이 여러개가 있기 때문에 JSP에서는 함수를 호출하여 간단히 페이징 태그 작성 및 기능을 수행하도록 공통함수로 만들었다.

    /*
    divId : 페이징 태그가 그려질 div
    pageIndx : 현재 페이지 위치가 저장될 input 태그 id
    recordCount : 페이지당 레코드 수
    totalCount : 전체 조회 건수 
    eventName : 페이징 하단의 숫자 등의 버튼이 클릭되었을 때 호출될 함수 이름
    */
    var gfv_pageIndex = null;
    var gfv_eventName = null;
    function gfn_renderPaging(params){
    	var divId = params.divId; //페이징이 그려질 div id
    	gfv_pageIndex = params.pageIndex; //현재 위치가 저장될 input 태그
    	var totalCount = params.totalCount; //전체 조회 건수
    	var currentIndex = $("#"+params.pageIndex).val(); //현재 위치
    	if($("#"+params.pageIndex).length == 0 || gfn_isNull(currentIndex) == true){
    		currentIndex = 1;
    	}
    	
    	var recordCount = params.recordCount; //페이지당 레코드 수
    	if(gfn_isNull(recordCount) == true){
    		recordCount = 20;
    	}
    	var totalIndexCount = Math.ceil(totalCount / recordCount); // 전체 인덱스 수
    	gfv_eventName = params.eventName;
    	
    	$("#"+divId).empty();
    	var preStr = "";
    	var postStr = "";
    	var str = "";
    	
    	var first = (parseInt((currentIndex-1) / 10) * 10) + 1;
    	var last = (parseInt(totalIndexCount/10) == parseInt(currentIndex/10)) ? totalIndexCount%10 : 10;
    	var prev = (parseInt((currentIndex-1)/10)*10) - 9 > 0 ? (parseInt((currentIndex-1)/10)*10) - 9 : 1; 
    	var next = (parseInt((currentIndex-1)/10)+1) * 10 + 1 < totalIndexCount ? (parseInt((currentIndex-1)/10)+1) * 10 + 1 : totalIndexCount;
    	
    	if(totalIndexCount > 10){ //전체 인덱스가 10이 넘을 경우, 맨앞, 앞 태그 작성
    		preStr += "<a href='#this' class='pad_5' onclick='_movePage(1)'>[<<]</a>" +
    				"<a href='#this' class='pad_5' onclick='_movePage("+prev+")'>[<]</a>";
    	}
    	else if(totalIndexCount <=10 && totalIndexCount > 1){ //전체 인덱스가 10보다 작을경우, 맨앞 태그 작성
    		preStr += "<a href='#this' class='pad_5' onclick='_movePage(1)'>[<<]</a>";
    	}
    	
    	if(totalIndexCount > 10){ //전체 인덱스가 10이 넘을 경우, 맨뒤, 뒤 태그 작성
    		postStr += "<a href='#this' class='pad_5' onclick='_movePage("+next+")'>[>]</a>" +
    					"<a href='#this' class='pad_5' onclick='_movePage("+totalIndexCount+")'>[>>]</a>";
    	}
    	else if(totalIndexCount <=10 && totalIndexCount > 1){ //전체 인덱스가 10보다 작을경우, 맨뒤 태그 작성
    		postStr += "<a href='#this' class='pad_5' onclick='_movePage("+totalIndexCount+")'>[>>]</a>";
    	}
    	
    	for(var i=first; i<(first+last); i++){
    		if(i != currentIndex){
    			str += "<a href='#this' class='pad_5' onclick='_movePage("+i+")'>"+i+"</a>";
    		}
    		else{
    			str += "<b><a href='#this' class='pad_5' onclick='_movePage("+i+")'>"+i+"</a></b>";
    		}
    	}
    	$("#"+divId).append(preStr + str + postStr);
    }
    
    function _movePage(value){
    	$("#"+gfv_pageIndex).val(value);
    	if(typeof(gfv_eventName) == "function"){
    		gfv_eventName(value);
    	}
    	else {
    		eval(gfv_eventName + "(value);");
    	}
    }
    

    페이징 태그를 작성하는 함수는 두개로 구성이 되어있다. 

    첫번째는 gfn_renderPaging이라는 함수로 페이징 태그를 작성하는 역할을 한다.

    두번째로는 내부적으로 사용할 함수로 _movePage라는 함수가 있다. 이는 페이징 태그를 클릭하였을 경우 해당 페이지로 이동하는 역할을 한다. 


    이제 소스를 간단히 살펴보도록 하자.

    먼저 gfn_renderPaging 함수부터 보자.

    1~7번째줄에는 주석이 달려있는것을 볼 수 있다. 이는 이 함수를 사용할 때 필요한 파라미터들을 적어놓은 것이다. 

    함수에서 파라미터는 params라는 값 하나만 받는데 어떻게 저런 파라미터들을 받는지는 JSP에서 이야기를 할것이다. 여기서는 그냥 저런 이름들이 있다는 것만 확인하면 된다. 

    이 함수에서 코드가 복잡한 부분은 없다. 단지 페이지당 레코드 수나 인덱스 수를 계산하거나, 아니면 태그를 만드는 부분이 살짝 복잡하다. 

    27~29번째 줄에는 각각 3개의 변수가 선언되어 있는것을 볼 수 있다. 

    이는 각각 맨앞으로 이동 태그,  1~10 등과 같은 인덱스 태그, 맨 뒤로 이동 태그를 담당한다. 

    전체의 인덱스가 10을 초과할 경우 preStr 변수에는 맨앞, 앞 태그를 작성하고, 전체의 인덱스가 10 이하일 경우, 맨앞으로 이동 태그만 만들것이다.

    마찬가지로 맨뒤, 뒤 태그도 postStr에 작성된다. 이는 전체의 인덱스에 따라 유동적으로 결정될것이다. 

    그 다음 str 변수에는 인덱스가 담길것이다. 


    각 태그는 <a>태그를 사용해서 작성했으며, 각 태그가 클릭되었을 때 _movePage라는 함수를 호출하게 되어있다.

    _movePage는 해당 태그가 클릭되었을 때, JSP에서 선언한 함수를 호출하게끔 구성되어있다. 


    지금 이러한 태그를 가지고 아무리 자세하게 설명한다고 하더라도 이해하기는 쉽지 않을것이다. 일단은 대충 이런 역할을 한다는것만 살펴보고 넘어가도록 하자.

    다음에 나올 내용인 JSP 부분에서 다시 한번 살펴볼 것이다.


    2. 개발 소스

    이제 위에서 작성한 공통기능을 사용하여 페이징 기능을 사용할 차례이다. 위에서 작성한것을 바탕으로 기존의 게시판을 변경하도록 하겠다. (지난글에서 작성한 전자정부 프레임워크를 사용한 페이징이 아닌 일반 게시판을 기준으로 진행한다.)


    1. JSP

    먼저 jsp를 변경하자.

    boardList.jsp를 다음과 같이 수정한다. 

    <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
    <!DOCTYPE html>
    <html lang="ko">
    <head>
    <%@ include file="/WEB-INF/include/include-header.jspf" %>
    </head>
    <body>
    	<h2>게시판 목록</h2>
    	<table class="board_list">
    		<colgroup>
    			<col width="10%"/>
    			<col width="*"/>
    			<col width="15%"/>
    			<col width="20%"/>
    		</colgroup>
    		<thead>
    			<tr>
    				<th scope="col">글번호</th>
    				<th scope="col">제목</th>
    				<th scope="col">조회수</th>
    				<th scope="col">작성일</th>
    			</tr>
    		</thead>
    		<tbody>
    			
    		</tbody>
    	</table>
    	
    	<div id="PAGE_NAVI"></div>
    	<input type="hidden" id="PAGE_INDEX" name="PAGE_INDEX"/>
    	
    	<br/>
    	<a href="#this" class="btn" id="write">글쓰기</a>
    	
    	<%@ include file="/WEB-INF/include/include-body.jspf" %>
    	<script type="text/javascript">
    		$(document).ready(function(){
    			fn_selectBoardList(1);
    			
    			$("#write").on("click", function(e){ //글쓰기 버튼
    				e.preventDefault();
    				fn_openBoardWrite();
    			});	
    			
    			$("a[name='title']").on("click", function(e){ //제목 
    				e.preventDefault();
    				fn_openBoardDetail($(this));
    			});
    		});
    		
    		
    		function fn_openBoardWrite(){
    			var comSubmit = new ComSubmit();
    			comSubmit.setUrl("<c:url value='/sample/openBoardWrite.do' />");
    			comSubmit.submit();
    		}
    		
    		function fn_openBoardDetail(obj){
    			var comSubmit = new ComSubmit();
    			comSubmit.setUrl("<c:url value='/sample/openBoardDetail.do' />");
    			comSubmit.addParam("IDX", obj.parent().find("#IDX").val());
    			comSubmit.submit();
    		}
    		
    		function fn_selectBoardList(pageNo){
    			var comAjax = new ComAjax();
    			comAjax.setUrl("<c:url value='/sample/selectBoardList.do' />");
    			comAjax.setCallback("fn_selectBoardListCallback");
    			comAjax.addParam("PAGE_INDEX",pageNo);
    			comAjax.addParam("PAGE_ROW", 15);
    			comAjax.ajax();
    		}
    		
    		function fn_selectBoardListCallback(data){
    			var total = data.TOTAL;
    			var body = $("table>tbody");
    			body.empty();
    			if(total == 0){
    				var str = "<tr>" + 
    								"<td colspan='4'>조회된 결과가 없습니다.</td>" + 
    							"</tr>";
    				body.append(str);
    			}
    			else{
    				var params = {
    					divId : "PAGE_NAVI",
    					pageIndex : "PAGE_INDEX",
    					totalCount : total,
    					eventName : "fn_selectBoardList"
    				};
    				gfn_renderPaging(params);
    				
    				var str = "";
    				$.each(data.list, function(key, value){
    					str += "<tr>" + 
    								"<td>" + value.IDX + "</td>" + 
    								"<td class='title'>" +
    									"<a href='#this' name='title'>" + value.TITLE + "</a>" +
    									"<input type='hidden' name='title' value=" + value.IDX + ">" + 
    								"</td>" +
    								"<td>" + value.HIT_CNT + "</td>" + 
    								"<td>" + value.CREA_DTM + "</td>" + 
    							"</tr>";
    				});
    				body.append(str);
    				
    				$("a[name='title']").on("click", function(e){ //제목 
    					e.preventDefault();
    					fn_openBoardDetail($(this));
    				});
    			}
    		}
    	</script>	
    </body>
    </html>
    

    지난번보다 좀 길어진것을 볼 수 있다. 

    먼저 살펴봐야 할것은 29~30번째 줄이다. 

    29번째 줄의 <div id="PAGE_NAVI"></div> 부분은 앞으로 페이징 태그가 그려질 부분이다. 

    30번째 줄은 현재 페이지 번호가 저장될것이다.


    그 다음으로는 38번째 줄에서 fn_selectBoardList(1)을 호출하는것을 봐야한다.

    이것은 최초에 화면이 호출되면 1페이지의 내용을 조회하는 것을 의미한다.

    그럼 fn_selectBoardList는 어떻게 되어있는지 확인해보자.


    65번째 줄의 fn_selectBoardList를 살펴보면 파라미터로 pageNo를 받는것을 알 수 있다. 여기서 pageNo는 호출하고자 하는 페이지 번호를 의미한다.

    위에서 만든 ComAjax를 사용하는것을 볼 수 있다.

    대부분은 ComSubmit과 비슷하지만 setCallback이라는 함수가 추가된것을 확인할 수 있다. setCallback은 Ajax 요청이 완료된 후 호출될 함수의 이름을 지정하는 함수이다. 

    여기서는 콜백함수로 fn_selectBoardListCallback 이라는 이름의 함수를 지정했는데, 이것은 74번째줄의 fn_selectBoardListCallback(data) 함수를 의미한다. 

    ComAjax함수에서는 두개의 파라미터를 전송하고 있다. PAGE_INDEX와 PAGE_ROW가 그것인데 각각 현재 페이지 번호와 한 페이지에 보여줄 행(데이터)의 수를 의미한다. 

    여기까지만 이야기하고 먼저 실행된 결과화면을 살펴보도록 하자.


    나중에 서버단까지 개발을 완료하고 실행을 시키면 다음과 같은 화면을 볼 수 있다. 

    한 페이지에 15개의 데이터를 보여주며 맨앞, 맨뒤를 의미하는 [<<], [>>] 버튼과 앞 뒤를 의미하는 [<], [>] 버튼, 그리고 1~10의 페이지 번호가 있는것을 볼 수 있다.

    여기서 만들어진 페이징 태그가 앞에서 만들었던 gfn_renderPaging 함수에 의해서 만들어지는 부분이다. JSP에서는 gfn_renderPaging 함수를 fn_selectBoardListCallback 함수에서 호출했는데, 이 부분은 잠시 후에 살펴볼것이다. 

    여기서는 JSP에서 단지 <div id="PAGE_NAVI"></div> 태그만 작성해 놓고 공통함수를 이용해서 페이징 태그가 작성되는것을 확인하면 된다.


    그다음으로 fn_selectBoardListCallback 함수를 보자. 

    이 함수는 ajax 호출이 되고 난 후 실행되는 콜백함수로 여기서는 화면을 다시 그리는 역할을 수행한다. 

    Ajax는 기본적으로 비동기식호출이기 때문에 서버에 요청을 하고 그 결과값을 받더라도 화면의 전환이 일어나지 않는다. 따라서 결과값을 받은 후, 데이터의 갱신 등을 따로 해줘야한다. 이것은 submit을 할때와 다른 점으로, 화면 갱신이 일어나지 않기 때문에 JSTL등을 이용하여 목록 등을 만들수가 없다. 


    그 과정을 이제 하나씩 살펴보도록 하겠다. 

    여기서는 바로 전글(http://addio3305.tistory.com/89)에서 테이블의 <tbody> 부분과 비교를 하면서 보는것이 좋다.

    가장 먼저 fn_selectBoardListCallback의 파라미터인 data는 서버에서 전송된 json 형식의 결과값이다. 이 값을 어떻게 보내주는지는 잠시 후에 보도록 하겠다. 

    만약 조회된 결과가 0일 경우 (data.TATAL == 0) 조회된 결과가 없기 때문에 화면에는 조회된 결과가 없다고 표시한다. 

    이것은 바로 이전글에서 <c:otherwise> 태그에 해당한다. 


    반대로 데이터가 존재할 경우 84번째 줄부터가 실행된다. 

    85~90번째줄은 앞에서 만든 gfn_renderPaging 함수를 수행하기 위해서 파라미터를 만드는 과정이다. Javascript에서 "var 변수명 = {} " 이렇게 선언을 하면 Object가 만들어지고, 거기에 각각 key와 value 형식으로 값을 추가할 수 있다.

    divId, pageIndex, totalCount, eventName은 key가 되고, "PAGE_NAVI", "PAGE_INDEX", totla, "fn_selectBoardList"는 value가 된다. 

    그 후 gfn_renderPaging 함수를 호출하면 object의 값을 이용하여 페이징 태그를 만들게 된다.


    94번째 줄 부터는 이전글의 <c:forEach> 태그를 이용하여 테이블의 목록을 만든것과 같은 역할을 수행한다. 94번째 줄의 data.list 가 서버에서 보내준 데이터이고, 이를 이용해서 jQuery의 .each 함수를 사용하여 HTML 태그를 만들어주는것을 볼 수 있다.


    그리고 마지막으로 새롭게 추가된 각각의 목록의 제목에 상세보기로 이동할 수 있도록 click 이벤트를 바인딩 해주게 된다. 


    2. JAVA

    1) Contoller

    SampleContoller를 열어서 다음을 작성하자. 

    @RequestMapping(value="/sample/openBoardList.do")
    public ModelAndView openBoardList(CommandMap commandMap) throws Exception{
    	ModelAndView mv = new ModelAndView("/sample/boardList");
    	
    	return mv;
    }
    
    @RequestMapping(value="/sample/selectBoardList.do")
    public ModelAndView selectBoardList(CommandMap commandMap) throws Exception{
    	ModelAndView mv = new ModelAndView("jsonView");
    	
    	List<Map<String,Object>> list = sampleService.selectBoardList(commandMap.getMap());
    	mv.addObject("list", list);
    	if(list.size() > 0){
    		mv.addObject("TOTAL", list.get(0).get("TOTAL_COUNT"));
    	}
    	else{
    		mv.addObject("TOTAL", 0);
    	}
    	
    	return mv;
    }
    

    지난글에서 작성한 것과는 약간 다른것을 확인할 수 있다.

    지난글에서는 openBoardList에서 sampleService.selectBoardList를 호출했었는데(http://addio3305.tistory.com/89 의 2.Java > 1) Controller 참조), 이번에는 openBoardList는 단순히 boardList.jsp를 호출하는 역할만 수행한다.


    그 다음으로 selectBoardList.do가 새로 생겼다. 

    먼저 10번째 줄을 살펴보면 여태까지와는 약간 다른점을 확인할 수 있다. 

    기존에는 ModelAndView에서 호출할 JSP 파일명이나 redirect를 수행했는데, 이번에는 jsonView라는 값이 들어가있는 것을 볼 수 있다. 

    이는 앞에서 action-servlet.xml에 <bean id="jsonView" class="org.springframework.web.servlet.view.json.MappingJackson2JsonView" /> 를 선언했었던것을 기억해야 한다. 여기서 bean id가 jsonView였는데, 여기서 선언된 bean을 사용하는 것이다. 

    이 jsonView는 데이터를 json 형식으로 변환해주는 역할을 수행한다. 

    주소창에서 http://localhost:8080/first/sample/selectBoardList.do를 한번 호출해보면 다음과 같은 화면을 볼 수 있다.


    생소한 화면인데, 여기서 볼 수 있는 데이터가 바로 json 형식의 데이터다. 여기서는 데이터의 크기를 줄이기 위해서 Indent(띄어쓰기) 등의 처리가 안되어있어서 데이터의 형식을 보기가 힘들다. 이러한 JSON 형식의 데이터가 올바른지 여부를 검사하고 포맷팅도 해주는 사이트들이 있다. 

    http://jsonlint.com/ 역시 그러한 사이트 중에 하나다. 여기에 들어가서 앞에서 조회한 데이터를 모두 복사해서 Validate를 눌러보면 다음과 같은 화면을 볼 수 있다.

    아까의 데이터가 위에서 보는것과 같이 정렬되어서 나온다. 지금은 JSON에 대해서 이야기를 하는것이 아니기 때문에 간단히 이런게 있다는것만 살펴보고 넘어가도록 하겠다. 이에 대한 자세한 설명은 인터넷에서 많이 찾을수 있다.


    다시 소스로 돌아와서 selectBoardList.do는 sampleService.selectBoardList를 호출하여 목록 정보를 조회하고 그 값을 화면에 전달하는 역할을 한다. 

    여기서 mv.addObject에("list", list)와 mv.addObject("TOTAL", 어떤 값) 두가지의 값을 화면에 보내주는것을 확인하자. 

    앞에서 ajax callback 함수에서 data.TOTAL과 data.list가 있었던 것을 다시 한번 찾아보자. Controller에서 json 형식의 데이터를 화면에 전달하는데 그 값은 data라는 이름으로 화면에 전달된다. (꼭 data일 필요는 없다. 그렇지만 필자는 ComAjax에서 callback을 수행할 때, data라는 이름으로 보내주도록 해놨다. ComAjax를 보면 확인할 수 있다.) 그리고 mv에는 각각 list와 TOTAL이라는 key로 값을 보내줬고 이는 다시 화면에서 각각 data.list, data.TOTAL이라는 형식으로 값에 접근할 수 있다.


    그 이후 service, serviceImpl, DAO, sql은 지난번과 거의 같다. 


    2) SampleService

    SampleService.java에 다음을 작성한다.

    List<Map<String, Object>> selectBoardList(Map<String, Object> map) throws Exception;
    

    3) SampeServiceImpl

    SampleServiceImpl.java에 다음을 작성한다.

    @Override
    public List<Map<String, Object>> selectBoardList(Map<String, Object> map) throws Exception {
    	return sampleDAO.selectBoardList(map);
    }
    

    4) SampleDAO

    SampleDAO.java에 다음을 작성한다.

    @SuppressWarnings("unchecked")
    public List<Map<String, Object>> selectBoardList(Map<String, Object> map) throws Exception{
    	return (List<Map<String, Object>>)selectPagingList("sample.selectBoardList", map);
    }
    

    3. SQL

    SQL은 지난글과 비교해서 바뀐게 없다.

    <select id="selectBoardList" parameterType="hashmap" resultType="hashmap">
    	<include refid="common.pagingPre"/> 
    	<![CDATA[
    		SELECT
    			ROW_NUMBER() OVER (ORDER BY IDX DESC) RNUM,
    			IDX,
    			TITLE,
    			HIT_CNT,
    			TO_CHAR(CREA_DTM, 'YYYY.MM.DD') AS CREA_DTM
    		FROM
    			TB_BOARD
    		WHERE
    			DEL_GB = 'N'
    	]]>
    	<include refid="common.pagingPost"/> 
    </select>
    

    여기까지 하고 실행을 시키면 앞에서 봤던 화면을 볼 수 있다.

    실행화면과 로그를 다시 한번 살펴보자.



    페이징 태그의 버튼을 이것저것 누르면서 화면이 제대로 이동을 하는지, 쿼리 역시 제대로 동작하는지 살펴보면 된다.


    ------------------------------------------------------------------------------------


    몇달만에 글을 다시 작성하게 되었습니다. 


    먼저 그동안 글을 기다려주신 분들께 감사의 인사와 함께 죄송하다는 말씀을 같이 드리고 싶네요. 


    그동안 개인적으로 많은 일이 있었습니다. 개인적으로 시도했던 일이 잘 안풀리고 그걸 정리하고 회사도 이직을 했거든요. 


    그러다보니 많이 정신이 없기도 하고, 글을 쓰는 개인 랩탑도 한동안 여기저기 다른곳에 있었기도 했고, 글을 쓸 시간도 좀 부족했었습니다.


    아니, 글을 쓸 시간이 있어도 오히려 개인시간을 보냈었네요. 


    지금은 어느정도 정리가 되었고 시간적인 여유도 좀 있어서 그동안 못썼던 만큼 포스팅을 더 하려고 합니다.


    사실 이번글은 개인적으로는 정말 마음에 안드는 포스팅입니다. 처음에 생각했던것보다 설명해야 할 내용이 많아지다보니 글이 너무 길어지는 느낌이라서


    간단히 설명하고 넘어갔는데, 충분히 이해하기에는 부족한 설명이었던 것 같습니다. 또 Ajax나 JSON 등에 대한 설명도 너무 부족했던것 같구요. 


    또한 원래 처음에 이 포스팅을 작성하기 시작한것은 10월 말이었는데, 중간에 한참 글을 안써서 매끄럽게 연결되지 못한점도 많네요. 


    글을 두개로 나눌까 생각도 했었는데, 그러기에는 너무 많이 써서 변경하기도 힘들었습니다. 


    이번글은 가능하면 소스를 많이 살펴보시고 이해를 하는데 시간을 많이 투자하셔야 될 내용들이었습니다. 


    제 사정이야 어찌되었던 오랜만에 쓴 글이 이렇게 부족한 내용이어서 죄송한 마음이 크네요. 


    다음 글 부터는 좀 더 잘 계획해서 더욱 도움이 되도록 포스팅을 하도록 노력하겠습니다~


    first.zip



    댓글 82

    • 이전 댓글 더보기
    • zero2one 2016.10.09 14:52

      1편부터 정주행 했는데, 덕분에 재밌게 배우면서 도움도 많이 되었습니다.
      좋은 강의 감사합니다.

    • 왕초보 2016.10.25 11:06

      잘보고있습니다. 그런데 한 화면에 4개의 페이지을 할려니 마지막에 실행된 것만 움직여서 모든 페이징번호들이 공유됩니다. 각 각 따로 구분하는 방법은 어케 하나요 ㅠㅠ

    • 박병장 2016.12.20 16:47

      HTTP Status 500 - Request processing failed; nested exception is org.springframework.jdbc.UncategorizedSQLException:
      초보입문자입니다.
      500에러가 나네요... 전자정부프레임에서 제공하는 페이징 처리 시에는 문제 없이 상세페이지 잘 띄워졌는데 뭐가 잘못됬나 싶어 계속 확인해도 문제요인을 모르겠네요... json으로 날릴 때 <input type='hidden' id='IDX">로 바꿨고 리스트 화면도 잘 나오는데 문제는 상세 페이지가 안나오는 현상입니다...ㅜㅜ
      혹시나 싶어 json으로 넘기는 int값을 serviceImpl이나 DAO에서 SQL.xml로 넘어가 처리되기 전에 map에 담겨있는 IDX value값을 parseInt로 변형해 보았는데, 현상은 바뀌지 않네요...
      도움 부탁드립니다...ㅜㅜ

    • 힘내셈 2017.01.11 16:19

      감사합니다^^ 많이 배웠습니다~~~

    • 최고 2017.01.16 17:45

      고생하셨습니다. 잘 배우고 갑니다.

    • comAjax 2017.01.18 17:42

      다들 잘되시나요? 저는 boardList.jsp에 fn_selectBoardList함수에서 comAjax를 생성하는 부분에서 객체가 제대로 생성이 되지 않네요ㅠ

      • ㅎㅎ 2017.01.19 16:26

        일단 common.js ->common2.js
        include-header.jspf 가셔서
        script 경로에 common.js ->common2.js 변경해주시면 될겁니다.

    • ㅎㅎ 2017.01.19 16:45

      안녕하세여 !! 페이징 때문에 어쭙니다211 건수
      제 페이지 수가 15 입니다
      << < 1 2 3 4 5 6 7 8 9 10 > >>
      여기서
      << < 1 2 3 4 5 6 7 8 9 10 [>] >>
      > 애를 클릭하면 11 페이지넘어가는데
      데이터는 제대로 나온느데 페이징 부부분에서
      << < 1 2 3 4 5 > >>
      이렇게 나워서 보니간
      var first = (parseInt(( -1) / 10) * 10) + 1 ;first가 1 더라거요
      그래서 11페이지 일떼 11번
      << < 1 2 3 4 5 > >>
      11번 부터 15 까지 나오게 할려면 어찌 하나요?

      • 도데체왜... 2017.02.23 17:25

        저도 같은 문제가 발생하네요.
        script
        var last 에서 나는 문제 같은데 어찌 고쳐야 할지 모르겠네요.

      • 도데체왜... 2017.02.23 18:06

        방금 해결됨.
        var last = (parseInt(totalIndexCount/10) < parseInt(currentIndex)/10) ? totalIndexCount%10 : 10;

        아직까지는 문제 없음.

    • 뒹구르르 2017.01.26 14:15

      JQuery으로 페이징 처리하는건 잘 몰랐는데 설명을 너무 잘해주셔서 다시 정주행하여 이해됐네요 ㅎㅎ 감사합니다. 좋은 강의입니다. 테스트 하다보니 조회수가 두번씩 증가하네요. 제목 클릭 시 fn_openBoardDetail 이게 두번 호출되서 그런것 같아요 ㅎㅎ 나중에 세션쪽도 강의가능하실까요? 로그인쪽도 알고 싶어요~

    • SpringWorld 2017.02.03 17:39

      MySql사용하시는분들은
      AbstractDAO.java에서
      map.put("END", (nPageIndex * nPageRow) + nPageRow); 를
      map.put("END", 15); 로 변경하셔야 합니당
      이유는 역시 LIMIT때문입니다
      정말 좋은강의 감사합니다. 덕분에 기초를 많이 다지고 갑니다.

    • JJH 2017.03.06 16:17

      잘봤습니다. 도움이 많이 됬어요~ 제가 궁금한게하나있는데 국제화하는것좀 올려주실수있나요? 국제화를 할려고하는데 전혀 이해도안되고 구글에서 찾아서 해봐도 이거보면서만들었는 테이블에 적용도 안되더라고요 ㅠ 부탁좀드리겠습니다. ㅠ

    • Favicon of https://kstoryo.tistory.com BlogIcon kk3390 2017.04.28 15:09 신고

      저도 위에 박병장님과 비슷하게 조회가 잘 되다가 이번 글부터 접속 후 조회가 안되더군요(500). 두번째 페이지부터는 조회가 되는 상황이였습니다.
      boardList.jsp 에서 fn_selectBoardList(1); 를 window.onload=function(){fn_selectBoardList(1);}; 로 바꾸니 정상적으로 실행됩니다.
      자바도 그렇고 웹개발은 해본적이없어서 따라하고있습니다. 좋은글 감사합니다. 이정도로 자세히 설명되어있는 곳이 없더군요 잘보고있습니다.

    • asasd 2017.11.23 15:54

      전자정부 페이징은 100만건 데이터 페이징 잘되는데 이건 맨뒤로 가면 번호가 안나오네여 흠

    • kkmy 2018.03.08 11:09

      정말 많은 도움이 되고있습니다 혹시 저와같은 현상있으신분들 계신가요
      AbstractDAO.java 71라인의
      PAGE_INDEX와 PAGE_ROW 값을 map.get을 통해 가져오는 부분이있는데
      String strPageIndex = (String)map.get("PAGE_INDEX";)
      페이징 tag 까지 생성이 되는데 화면에서 페이징 클릭시 해당값이 넘어오지 않습니다
      db : postgres를 사용하였는데 혹시 같은 현상이 발생하신분있으시면 도움을 부탁드립니다
      감사합니다!~

    • Favicon of https://lsjsj92.tistory.com BlogIcon 오키여 2018.04.02 15:02 신고

      정말 너무너무 감사드립니다.
      JSP뿐이 못하고 스프링을 도대체 어떻게 해야 할지 막막했는데 ㅠㅠ 정말 큰 도움됩니다.
      너무 감사드립니다. 염치없게 설치부터 하나하나 다 따라가고 있습니다.
      저도 블로그 운영중이라서 만약 퍼가게 되면 꼭 출처 남기고 가겠습니다.
      큰 도움되었습니다.

    • 감따합니다 2018.05.09 13:43

      혹시 저같이 안되는 분은 참고해주세요!

      게시판 제목을 클릭했을 때 객체값이 나오면서 에러 뜰 때
      boardList.jsp에서 javascript 부분에 제목 클릭 이벤트 코드가 2번 작성되어 있습니다.
      하나를 생략해주시면 됩니다!

    • 초보개발자 2018.05.21 18:24

      끝 쪽 페이지가 출력이 안 되는 분은 아래와 같이 수정해주세요.
      common.js
      if(gfn_isNull(recordCount) === true) {
      recordCount = 15; // (원래 20으로 되어있음)
      }

      실제로는 화면에 15개씩 뿌리는데 recordCount가 20으로 잡혀있어서 totalIndexCount가 실제 데이터만큼의 인덱스를 생성하지 못하는게 문제였네요.

    • 어맛 2018.11.23 17:47

      같은 파일을 두번다운 받으면 에러 발생하시거나
      IDX = [Ljava.lang.String;@7e69e749 에러 나는 분들은 편법인것 같지만 간단하게 수정가능합니다.

      #boardDetail.jsp 파일의 함수 안에 아래와 같이 제일 마지막 한 줄만 추가해주시면 됩니다.
      function fn_downloadFile(obj){
      var idx = obj.parent().find("#IDX";).val();
      var comSubmit = new ComSubmit();
      comSubmit.setUrl("<c:url value='/common/downloadFile.do' />";);
      comSubmit.addParam("IDX", idx);
      comSubmit.submit();
      location.reload(true); //현재 페이지 강제로 새로고침
      }

      IDX값을 형변환해서 다시 불러오는 것에 문제가 있는것 같은데 어느 부분을 고쳐야 할지 모르겠어서
      테스트 해보니 새로고침 후 받으면 정상적으로 작동 되는것을 확인하고 파일 다운로드 함수부분에 실행될때마다
      강제로 새로고침 하는 부분을 넣었습니다. 해결법을 알고 계신 분들은 답글 부탁드리겠습니다.(저는 MySQL 사용중입니다.)

    • 개린이 2018.12.17 15:54

      드디어 여기까지 완료했습니다. 너무나도 감사합니다!!!! 혼자 해결하느라 시간이 많이 걸렸지만, 뿌듯합니다. 이제 다시 돌아가서 주석을 달며 프로그램 자체를 제 것으로 만들어 보려고 합니다!! 책 나오면 꼭 사겠습니다! 다시 한 번 감사합니다.

    • 감사합니다너무나도 2019.01.30 17:28

      감사합니다. 너무 유익한 내용이고 쉽게 잘 써주셔서 잘따라왔습니다.
      댓글들도 함께보다보면 다들 금방 해결하실 수 있을꺼라 믿습니다.

      한가지 수정해주셨으면 하는 부분이 있는데요 해당 쿼리 사용하시면

      페이지마다 낮은 IDX의 값을 가진 게시글들이 상단에 출력됩니다.

      게시판은 나중에 쓴글부터 위에서 정렬되고 페이지를 넘어갔을때도 동일하게 적용되어야 보기가 편하다고 생각합니다.

      간단하게 아래 ORDER BY RNUM ASC 추가하여 수정했습니다. 참고하세요

      <!-- 게시판리스트 조회 -->
      <select id="board.selectBoardList" parameterType="hashmap" resultType="hashmap">
      <include refid="common.pagingPre"/>
      <![CDATA[
      SELECT
      ROW_NUMBER() OVER (ORDER BY IDX DESC) RNUM,
      IDX,
      TITLE,
      HIT_CNT,
      CREA_DTM
      FROM
      JIN_BOARD
      WHERE
      DEL_GB = 'N'
      ORDER BY RNUM ASC
      ]]>
      <include refid="common.pagingPost"/>
      </select>



    • 우옷 2019.02.11 16:07

      도움이 정말 많이 되었습니다. 그런데 궁금한게

      openBoardList 이렇게 한번 거쳐서
      selectBoardList 다시 이게 호출하는데 이부분을 합쳐서 한번에 처리를 할순 없나요?


      @RequestMapping(value="/sample/selectBoardList.do";) <=== URL 치고 들어가서
      public ModelAndView selectBoardList(CommandMap commandMap) throws Exception{

      ModelAndView mv = new ModelAndView("/sample/boardList";); <----- 이렇게 경로를 지정하고
      ModelAndView mv = new ModelAndView("jsonView";); <----- 이렇게 jsonView 를 바로 할순없을까요?

      List<Map<String,Object>> list = sampleService.selectBoardList(commandMap.getMap());
      mv.addObject("list", list);
      if(list.size() > 0){
      mv.addObject("TOTAL", list.get(0).get("TOTAL_COUNT";));
      }
      else{
      mv.addObject("TOTAL", 0);
      }

      return mv;
      }

Designed by Tistory.