본문으로 바로가기

※이 방식은 dataTables 1.9이하와 1.10이상의 버전 모두에서 사용 가능한 코드로 짜여진 예제입니다. 1.9 이하와 1.10 이상의 버전에서는 데이터를 주고받는 방식과 전송되는 매개변수가 다릅니다. 하지만 아래 설명한 예제는 모든 버전에서 호환되는 코드를 사용할 것입니다.


DataTables는 강력한 자바스크립트 제이쿼리 기반의 그리드 플러그인이다. 

server-side기능을 사용하지 않는다면 dataTables는 서버에서 한번에 모든 데이터를 가져온 뒤, 그 데이터를 설정된 대로 테이블로 그려준다. 가져온 데이터를 메모리에 가지고 있다가 검색이나 필터링, 정렬을 수행한다. 따라서 일단 로딩이 완료된 후로는 매우 빠른 속도로 사용자가 원하는 대로 데이터를 표현하도록 도와준다.

하지만 한번에 가져와야 할 데이터가 몇 천 건을 넘어 만개를 넘어갈 경우에는 서버와 클라이언트 모두에 부담이 될 수 있다. 

일반적으로 대부분의 웹은 DB-서버-클라이언트 간의 통신으로 데이터를 주고받기 때문에 두번의 데이터 전송이 필수적이다. 이 때, 한번에 너무 많은 데이터는 부담이 될 것이다.

보통 이런 이유로 대부분의 리스트성 데이터를 처리할 경우에는 일종의 페이징 기능을 사용한다. 이는 그리드가 어떤 형태이던 마찬가지이다. 한 페이지를 구성하는 데이터만 가져와 클라이언트의 화면을 그리고, 사용자가 페이지를 넘기거나 검색 혹은 정렬을 바꿀 경우에 다시 서버에 가서 알맞은 데이터를 조회해 오는 것이다. 보통은 이 기능을 개발하기 위해 프로젝트마다 표준을 정해 기능을 구현한다.


DataTabls는 이 Server-side processing을 위한 상당히 편리한 표준을 제공한다.


이전에 클라이언트 사이드로 구현한 예제 (http://www.leafcats.com/62 참고) 를 server-side를 사용하여 다시 바꾸어 보겠다.

클라이언트 사이트로 구현했던 예제를 함께 열어 비교해 보면 조금 더 이해가 쉬울 듯 하다.


STEP1. HTML

1
2
3
4
5
6
7
8
9
10
11
<table id="userTable" class="table table-striped table-bordered table-hover" >
    <thead>
        <tr>
            <th>Email</th>
            <th>Name</th>
            <th>User Status</th>
            <th>Super User</th>
        </tr>
    </thead>
    <!-- tbody 태그 필요 없다. -->
</table>
cs

HTML은 client-side나 server-side나 차이가 없다. 가져올 컬럼에 따라 컬럼 헤더만 잘 신경써서 맞춰주면 된다.



STEP2. script

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
    <script>
    $(document).ready(function(){
            var columns = ["EMAIL""FULL_NM_KR""USER_STAT_CD""SUPER_USER"];
            
            $('#userTable').dataTable({
                pageLength: 3,
                pagingType : "full_numbers",
                bPaginate: true,
                bLengthChange: true,
                lengthMenu : [ [ 13510-1 ], [ 13510"All" ] ],
                responsive: true,
                bAutoWidth: false,
                processing: true,
                ordering: true,
                bServerSide: true,
                searching: false,
           /*     ajax : {
                    "url":"/getUserList.do",
                    "type":"POST",
                    "data": function (d) {
                        d.userStatCd = "NR";
                    }
                }, */
                sAjaxSource : "/getUserList.do?userStatCd=NR&columns="+columns,
                sServerMethod: "POST",
                columns : [
                    {data: "email"},
                    {data: "fullNmKr"},
                    {data: "userStatCd"},
                    {data: "superUser"}
                ],
                
                columnDefs : [
                    {
                        "targets": [0,1,3],
                        "visible"true,
                    },
                    {
                        "targets"2,
                        "visible"false,
                    },
                ]
 
            });
 
    });
    </script>
cs

스크립트를 몇 가지로 구분해 설명하겠다.


1. ajax : 주석을 친 ajax부분을 잘 보자. 저 방식은 dataTables 1.10 이상의 버전에서 사용하는 방식이다. 우리는 버전1.9 이하에서도 작동할 수 있게끔 레거시 방식을 사용할 것이다. 데이터테이블 플러그인 1.9 이하에서 dataTables가 제공하는 표준화된 ajax 통신 구조를 사용하기 위해서는 "sAjaxSource"를 사용해야 한다. 

"sAjaxSource"를 사용하면 서버와 통신이 발생할 때마다 아래와 같은 데이터들을 자동으로 HttpServletRequest 에 담에 전송한다.


TypeNameInfo
intiDisplayStartDisplay start point in the current data set.
intiDisplayLengthNumber of records that the table can display in the current draw. It is expected that the number of records returned will be equal to this number, unless the server has fewer records to return.
intiColumnsNumber of columns being displayed (useful for getting individual column search info)
stringsSearchGlobal search field
boolbRegexTrue if the global filter should be treated as a regular expression for advanced filtering, false if not.
boolbSearchable_(int)Indicator for if a column is flagged as searchable or not on the client-side
stringsSearch_(int)Individual column filter
boolbRegex_(int)True if the individual column filter should be treated as a regular expression for advanced filtering, false if not
boolbSortable_(int)Indicator for if a column is flagged as sortable or not on the client-side
intiSortingColsNumber of columns to sort on
intiSortCol_(int)Column being sorted on (you will need to decode this number for your database)
stringsSortDir_(int)Direction to be sorted - "desc" or "asc".
stringmDataProp_(int)The value specified by mDataProp for each column. This can be useful for ensuring that the processing of data is independent from the order of the columns.
stringsEchoInformation for DataTables to use for rendering.

출처 : http://legacy.datatables.net/usage/server-side (dataTables 공식 api 웹)


서버에서는 클라이언트에서 request에 담아 보내주는 위 데이터들을 잘 받을 준비만 해 주문 되기 때문에 편리하다. 위 구조에 맞게 VO를 만들던, getParameter로 하나하나 받아서 사용하던 구현하기 나름일 것이다.


(tip. DataTables의 거의 모든 변수들 앞에는 "sSearch"처럼 소문자 알파벳이 붙는다. 저게 뭘까 잠깐 고민했었는데, 바로 변수의 type이다. int 타입이면 i가 붙을 것이고 String 타입이면 s가 붙는 식이다.)


2. columnDefs : 각 컬럼들에 대한 커스터마이징을 한다. 한개씩 설정도 가능하고, 위 코드처럼 묶어서 설정할 수도 있다. render를 사용해 아래 코드처럼 컬럼에 링크를 걸거나 컬럼을 꾸밀 수도 있다.

1
2
3
4
5
6
7
columnDefs : [
            {
             "targets": 0,
             "render": function(data){
                           return '<a href="링크">'+data+'</a>'
                        }
             }
cs


3. 정렬에 관한 문제 : 다른것들은 크게 문제 없이 편리하게 처리가 가능하지만, 정렬이 조금 귀찮다. DataTables는 다중 sorting을 제공하기 때문에, 정렬할 컬럼을 "iSortCol_0", "iSortCol_1"과 같은 식의 변수에 각 컬럼의 인덱스를 담아서 보내준다. 예를들어 단일 컬럼을 정렬하고, 정렬 대상 컬럼이 2번 인덱스라면 "iSortCol_0"에 2 라는 값이 담겨서 전송될 것이다.

때문에 각 index가 어떤 컬럼인지에 대한 정보를 가지고 있어야 한다. 나는 코드에서처럼 클라이언트에서 서버로 보내줬는데, ( var columns = ["EMAIL""FULL_NM_KR""USER_STAT_CD""SUPER_USER"]; ) 어떤 방법이던 개발하는 사람의 스타일에 따라 구현해 주면 될 것이다. 다중 Sorting은 구현하기 귀찮기 때문에 단일 소팅으로 예제를 작성했다.


4. 서버로 다른 값을 전달 : 나는 DataTables에서 제공하는 전역 Search 기능을 사용하지 않을 것이다. 전역 Search 기능은 Server-side방식에서 구현하기도 귀찮을 뿐더러 한 글자 타이핑 할 때마다 서버에 요청을 보내기 때문에 추천하지 않는다. 전역 Search를 사용하지 않는다면 별도의 검색조건 폼을 만들어서 사용하게 될 것이다. 이럴 경우에는 플러그인에서 알아서 전달해 주는 값들 이외에 서버로 값을 던져 줘야 한다. 나는 그냥 url 뒤에 get방식으로 전송하듯 붙여서 보냈다. 신기하게도 "sServerMethod"를 "POST"로 줬더니 post 방식으로 알아서 잘 넘어가는 듯 한데, 더 알아봐야 할 필요가 있다.



STEP3. Controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
    @RequestMapping("/getUserList.do")
    private String getUserList(@ModelAttribute UserVO inVO, HttpServletRequest request) throws Exception {
        
        //사용자 리스트 조회 서비스 호출
        List<UserVO> userList = userMgmtSvc.selectUserList(inVO);
        //페이징 고려하지 않은 전체 수 조회 서비스 호출
        int totalCount = userMgmtSvc.selectUserListCount(inVO);
        
        //클라이언트로 값 전송을 위해 WrapperVO로 감싸기
        WrapperVO rtnVO = new WrapperVO();
        rtnVO.setAaData(userList);
        rtnVO.setiTotalRecords(totalCount);    //실제 전체 데이터
        //필터링 된 전체 데이터 - 필터링 기능 사용하지 않기 때문에 실제 전체 데이터와 동일하다.
        rtnVO.setiTotalDisplayRecords(totalCount);     
        rtnVO.setsEcho(inVO.getsEcho());
        
        //json string으로 parsing
        String jsonString = JsonUtil.objectToJsonString(rtnVO);
        
        return jsonString;
    }
cs

주석으로 각 코드에 대한 설명은 해 두었다.

클라이언트로 전송해 주는 데이터 역시 표준에 맞추어 json string 형식으로 전송해 주어야 하는데, 각 항목들에 대한 설명은 이렇다.

aaData : 조회한 데이터의 2차원 배열을 aaData에 담는다.

iTotalRecords : 실제로 해당 데이터의 전체 개수이다.

iTotalDisplayRecords : 필터링을 거쳐 조회된 전체 데이터의 수이다. 페이징과 혼동할 수 있는데, 페이징은 고려 대상이 아니다. 예를들어 2번 컬럼에 a가 들어간 값으로 필터링 했다면 이 때의 전체 개수를 뜻하는 것이다. 페이징에 해당되는 현재 페이지 정보는 클라이언트단에서 알아서 계산해 주니 서버에서 신경쓰지 않아도 된다.



STEP4. QUERY

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
    <select id="selectUserList" parameterType="com.leafCat.common.user.vo.UserVO" resultType="com.leafCat.common.user.vo.UserVO">
        SELECT /*userListMapper.selectUserList*/
               USER_ID,
               EMAIL,
               FIRST_NM,
               MIDDLE_NM,
               LAST_NM,
               FULL_NM_KR,
               FULL_NM_GLOBAL,
               PASSWORD,
               PHOTO,
               FIRST_CRE_TIMESTAMP,
               LAST_MOD_TIMESTAMP,
               USER_STAT_CD,
               PASSWORD_ERR_CNT,
               DELETE_YN,
               SUPER_USER
          FROM lcusrbase
         WHERE DELETE_YN = 'N'
           AND SUPER_USER = 'N'
            <if test="userStatCd != null">
                   AND USER_STAT_CD = #{userStatCd}
            </if>
            <if test="sSortCol != null">
                   order by ${sSortCol}  
            </if>
            <if test="sSortCol != null and sSortDir_0 != null">
                   ${sSortDir_0}  
            </if>
            <if test="iDisplayStart != null and iDisplayLength != null">
                LIMIT ${iDisplayStart} , ${iDisplayLength}
            </if>
    </select>
cs

Mybatis를 사용했으며, 만일 hibernate를 사용한다면 그에 맞게 개발하면 될 것이다.

sSortCol은 클라이언트에서 가져온 "iSortCol_0" 값과 컬럼 정보에 맞춰 재 설정해준 값이다.




이제 서버를 구동해 보면 아래 화면과 같은 테이블이 완성된다.


client-side 때와는 다르게 사용자가 sorting을 한다던가, 페이징을 하는 등의 행위를 할 때마다 서버와 통신을 해서 server side에서 기능을 수행한 뒤 다시 클라이언트로 데이터를 보내준다. 아래는 위 화면에서 2번 페이지를 누른 화면과 서버 로그이다.




의도한 대로 쿼리가 잘 수행된 것을 볼 수 있다.



개발자가 직접 공통된 표준을 만들어 자신의 스타일대로 server-side processing을 할 수 없기 때문에 처음에 익숙해지는 것에 대해 러닝커브가 있을 것이다. 또한 '이미 정해진 표준' 이라는 것에 많은 개발자들이 반감을 가지고 있기 때문에 거부감이 들 수도 있다. 하지만 익숙해지고 나면 훨씬 편리하고, 반 강제적인 표준으로 개발자들간의 코드 통일성도 쉽게 잡을 수 있기 때문에 활용도가 높다고 생각한다. 무엇보다 강력한 그리드 부가 기능들을 제공하고, 이 밖에도 수많은 기능들과 추가 확장 플러그인들이 많으니 익혀두면 유용하게 써먹을 수 있을 것이다.

 Other Contents 

댓글을 달아 주세요

  1. 2018.01.26 11:11

    비밀댓글입니다

  2. 김성남 2018.10.11 12:01 신고

    감사합니다 .도움이 많이 됐습니다. 혹시 WrapperVo 정보를 알 수 있을까요? echo역할도 궁금합니다

티스토리 툴바