본문 바로가기
Spring Boot

스프링 부트(Spring Boot) - 페이징(Paging) & 검색(Search) 처리하기 2/2 [Thymeleaf, MariaDB, IntelliJ, Gradle, MyBatis]

by 도뎡 2023. 4. 13.
반응형
  • 본 게시판 프로젝트는 단계별(step by step)로 진행되니, 이전 단계를 진행하시는 것을 권장드립니다.
  • DBMS 툴은 DBeaver를 이용하며, DB는 MariaDB를 이용합니다. (MariaDB 설치하기)
  • 화면 처리는 HTML5 기반의 자바 템플릿 엔진인 타임리프(Thymeleaf)를 사용합니다.

이전 글에서는 페이징과 검색 기능에 필요한 공통 클래스들을 정의하고, 게시글 리스트 페이지에 페이징을 적용해 보았습니다.

이번에는 기존의 페이징에 게시글 검색 기능을 추가하고, 특정 페이지에서 어떠한 작업(액션)이 발생했을 때 이전 페이지 정보와 검색 조건을 유지시키는 기능을 구현해 보겠습니다.


이번 글은 구멍가게 코딩단의 코드로 배우는 스프링 웹 프로젝트 서적을 참고해서 포스팅하였습니다.


1. 검색 처리 (Dynamic SQL)

페이징과 검색 기능은 하나의 세트로 생각할 수 있습니다. 페이징은 전체 데이터를 카운팅 한 기준으로 1 ~ N개까지의 페이지를 보여주는 반면, 검색 기능은 SQL 쿼리에서 LIKE 검색 결과에 해당되는, 즉 필터링된 데이터를 카운팅 한 기준으로 1 ~ N개까지의 페이지를 보여줍니다.

검색 기능은 이전 글에서 생성한 SearchDto 클래스의 검색 키워드(keyword)와 검색 유형(searchType), 그리고 MyBatis의 Dynamic SQL 기능을 이용해서 처리합니다.

 

2. HTML(검색 영역) 수정하기

지금까지는 서버(Back-end)단 로직을 미리 구성해 두고 화면(Front-end)을 처리하는 순서로 진행했는데요. 이와 반대로 검색 기능은 화면 처리를 우선으로 해보겠습니다. 먼저 list.html의 검색 영역을 다음과 같이 변경해 주세요.

(Diffchecker를 이용하면, 기존 코드와 변경된 코드를 쉽게 비교해 볼 수 있습니다.)

    <!--/* 검색 */-->
    <div class="search_box">
        <form id="searchForm" onsubmit="return false;" autocomplete="off">
            <div class="sch_group fl">
                <select id="searchType" name="searchType" title="검색 유형 선택">
                    <option value="">전체 검색</option>
                    <option value="title">제목</option>
                    <option value="content">내용</option>
                    <option value="writer">작성자</option>
                </select>
                <input type="text" id="keyword" name="keyword" placeholder="키워드를 입력해 주세요." title="키워드 입력" />
                <button type="button" class="bt_search" onclick="movePage(1);"><i class="fas fa-search"></i><span class="skip_info">검색</span></button>
            </div>
        </form>
    </div>

 

구성요소 설명
searchForm 검색 폼(form)을 의미합니다. 폼의 검색 유형(searchType)과 키워드(keyword)를 SQL 쿼리의 검색 조건으로 이용해서 게시글을 검색합니다.
movePage( ) 검색 버튼에 연결된 클릭 이벤트입니다. movePage( )는 페이지를 이동하는 기능을 하는데요. 여기서 포인트는 검색 버튼을 클릭했을 때 movePage( )의 인자로 '1'을 전달한다는 것이며, 검색 처리에서 현재 페이지 번호(page)는 항상 '1'로 유지되어야 합니다.

 

페이지 번호(page)가 1로 유지되어야 하는 이유

예시를 하나 들어보겠습니다.

  •  A가 15페이지에서 게시글을 훑어보고 있다.
  •  A는 B가 작성한 게시글을 보기 위해 "작성자"를 B로 해서 검색을 했다.
  •  A는 B의 게시글을 조회하는 데 성공했으나 이상한 점이 있다. B가 가장 최근에 등록한 글부터 보고 싶은데, 현재 머물러 있는 페이지 번호가 15페이지인 것이다.

포인트는 여기에 있습니다. 페이지 번호를 '1'이 아닌 기존에 머물러 있던 '15'로 전달한다면, 검색 조건에 해당되는 데이터가 있다고 하더라도, 중간에 등록된 글부터 보이거나, 조회 자체가 되지 않는 상황이 벌어질 수 있습니다.

 

예시를 하나 더 들어보겠습니다.

  • A가 작성한 게시글 20개가 있다.
  • B는 3페이지에서 게시글을 훑어보다가 A가 작성한 글을 보기 위해 "작성자"를 A로 검색했다.
  • 분명 A가 작성한 글을 보았으나, A의 글이 검색되지 않는다. (결과 데이터 X)

두 번째 예시의 포인트입니다. 페이지당 출력할 데이터 개수가 10개라고 가정했을 때, A가 작성한 게시글은 총 20개이고 "작성자"를 A로 검색하면, A의 게시글은 1페이지와 2페이지에서는 볼 수 있으나, B가 기존에 머물러 있던 3페이지를 기준으로 검색이 되어버리니 A의 게시글이 검색되지 않는 상황이 발생한 것입니다.

 

3. movePage( ) 함수 수정하기

SQL 쿼리의 검색 조건으로 사용하기 위해, 검색 영역의 searchType과 keyword를 파라미터로 전달해 주어야 합니다.

    // 페이지 이동
    function movePage(page) {

        // 1. 검색 폼
        const form = document.getElementById('searchForm');

        // 2. drawPage( )의 각 버튼에 선언된 onclick 이벤트를 통해 전달받는 page(페이지 번호)를 기준으로 객체 생성
        const queryParams = {
            page: (page) ? page : 1,
            recordSize: 10,
            pageSize: 10,
            searchType: form.searchType.value,
            keyword: form.keyword.value
        }

        /*
         * 3. location.pathname : 리스트 페이지의 URI("/post/list.do")를 의미
         *    new URLSearchParams(queryParams).toString() : queryParams의 모든 프로퍼티(key-value)를 쿼리 스트링으로 변환
         *    URI + 쿼리 스트링에 해당하는 주소로 이동
         *    (해당 함수가 리턴해주는 값을 브라우저 콘솔(console)에 찍어보시면 쉽게 이해하실 수 있습니다.)
         */
        location.href = location.pathname + '?' + new URLSearchParams(queryParams).toString();
    }

 

이제, 리스트 페이지에서 검색 유형과 키워드를 세팅하고 검색해 보면, searchType과 keyword가 파라미터로 함께 전송됩니다.

PostController - openPostList( )

 

쿼리 스트링 파라미터도 정상적으로 연결됩니다.

검색 이후 리스트 페이지 URL

 

4. MyBatis 동적(Dynamic) SQL 알아보기

마이바티스는 동적 SQL을 처리할 수 있는 몇 가지의 태그와 표현식을 제공합니다. 일반적으로 조건문과 반복문이 주로 사용되는데요. 이 중에서도 조건문의 if, choose와 반복문의 foreach가 가장 많이 사용됩니다.

반복문의 foreach는 대표적으로 SQL 쿼리에서 WHERE 조건의 IN( ) 구문에 주로 사용되는데요. 이때 파라미터의 타입은 컬렉션(Collection) 타입이어야 합니다. (List, Array, Map 등)

기능 예시 설명
if <if test="조건">
    ...
</if>
Java의 if 문과 동일합니다.
choose
when
otherwise
<choose>
    <when test="조건1">...</when>
    <when test="조건2">...</when>
    <when test="조건3">....</when>
    <otherwise>...</otherwise>
</choose>
Java의 switch 문과 동일합니다. when은 각각의 조건을 처리하고, otherwise는 그 외의 조건을 처리합니다.
trim
where
set
<trim prefix="WHERE" prefixOverrides="AND | OR">
    ...
</trim>
로직을 처리하면서 필요한 구문을 변경합니다.
foreach <foreach collection="list" item="item" index="index" open="(" separator="," close=")">
    #{item}
</foreach>
컬렉션에 대해 반복문을 실행합니다. List, Array, Map 등에 담긴 여러 개의 데이터를 처리할 수 있습니다.

 

5. XML Mapper 수정하기

화면 처리는 완료되었으니, XML Mapper에 동적 SQL 기능만 적용해 주면 됩니다.


5-1) 검색용 SQL 조각 추가하기

 우선 PostMapper.xml에 다음의 코드를 작성해 주세요.

    <!-- 게시글 검색 -->
    <sql id="search">
        <!-- 검색 키워드가 있을 때 -->
        <if test="keyword != null and keyword != ''">
            <choose>
                <!-- 검색 유형이 있을 때 -->
                <when test="searchType != null and searchType != ''">
                    <choose>
                        <when test="'title'.equals( searchType )">
                            AND title LIKE CONCAT('%', #{keyword}, '%')
                        </when>
                        <when test="'content'.equals( searchType )">
                            AND content LIKE CONCAT('%', #{keyword}, '%')
                        </when>
                        <when test="'writer'.equals( searchType )">
                            AND writer LIKE CONCAT('%', #{keyword}, '%')
                        </when>
                    </choose>
                </when>
                
                <!-- 전체 검색일 때 -->
                <otherwise>
                    AND (
                           title LIKE CONCAT('%', #{keyword}, '%')
                        OR content LIKE CONCAT('%', #{keyword}, '%')
                        OR writer LIKE CONCAT('%', #{keyword}, '%')
                    )
                </otherwise>
            </choose>
        </if>
    </sql>

 

코드 설명

검색 키워드(keyword)가 파라미터로 넘어온 경우에만 실행되는 검색 쿼리입니다. 검색 유형(searchType)이 선택된 경우에는 각각의 <when> 조건에 해당되는 LIKE 쿼리가 실행되고, 전체 검색인 경우에는 <otherwise> 안에 선언한 LIKE 쿼리가 실행됩니다.

 

5-2) 검색용 SQL 조각 인클루드(Include) 하기

findAll 쿼리와 count 쿼리에서 search SQL을 인클루드해 주세요.

    <!-- 게시글 리스트 조회 -->
    <select id="findAll" parameterType="com.study.common.dto.SearchDto" resultType="com.study.domain.post.PostResponse">
        SELECT
            <include refid="postColumns" />
        FROM
            tb_post
        WHERE
            delete_yn = 0
            <include refid="search" />
        ORDER BY
            id DESC
        LIMIT #{pagination.limitStart}, #{recordSize}
    </select>
    
    
    <!-- 게시글 수 카운팅 -->
    <select id="count" parameterType="com.study.common.dto.SearchDto" resultType="int">
        SELECT
            COUNT(*)
        FROM
            tb_post
        WHERE
            delete_yn = 0
            <include refid="search" />
    </select>

 

6. 검색 기능 테스트 해보기

6-1과 6-2는 전체 검색과 제목을 기준으로 검색 기능을 테스트한 결과입니다. 내용과 작성자도 동일한 방법으로 테스트해 주시면 되며, 아직은 검색 조건이 유지되지 않는 게 정상입니다.


6-1) 전체 검색으로 "1000번" 검색

6-1 검색 결과

 

6-1 count 쿼리 실행 결과

 

6-1 findAll 쿼리 실행 결과

 

6-2) 제목으로 "500번" 검색

6-2 검색 결과

 

6-2 count 쿼리 실행 결과

 

6-2 findAll 쿼리 실행 결과

 

7. 키워드와 검색 조건 유지하기

모든 경우에서 문제없이 검색 기능이 작동하고 있습니다. 하지만, 검색 버튼을 클릭하면 검색 조건이 풀려버리는데요. 이를 해결하기 위해 쿼리 스트링 파라미터를 세팅해 주는 기능이 필요합니다.


7-1) 쿼리 스트링 유지용 함수 추가하기

list.html에 setQueryStringParams( ) 함수를 추가하고, onload( ) 함수에서 리스트 데이터를 조회하기 전에 해당 함수를 호출하도록 코드를 변경해 주세요.

    // 페이지가 로드되었을 때, 딱 한 번만 함수를 실행
    window.onload = () => {
        setQueryStringParams();

        findAllPost();
    }


    // 쿼리 스트링 파라미터 셋팅
    function setQueryStringParams() {

        if ( !location.search ) {
            return false;
        }

        const form = document.getElementById('searchForm');

        new URLSearchParams(location.search).forEach((value, key) => {
            if (form[key]) {
                form[key].value = value;
            }
        })
    }

 

로직 해석

JS에서 location 객체의 search를 이용하면 쿼리 스트링 파라미터를 조회할 수 있습니다.

form은 리스트 페이지의 검색 폼(searchForm)을 의미하며, new URLSearchParams( ) 함수의 인자로 현재 페이지의 쿼리 스트링을 전달해서 쿼리 스트링 문자열에 포함된 각 파라미터(key=value)를 객체화한 후, 검색 유형(searchType)과 키워드(keyword)의 값을 searchForm에 세팅합니다.

new URLSearchParams( ) - 쿼리 스트링 객체화 결과

 

7-2) 검색 기능 다시 테스트 해보기

다음은 작성자로 "테스터30"을 검색한 결과입니다.

작성자 - "테스트30" 검색 결과 (1페이지)

 

페이지를 이동하거나 새로고침을 해도 검색 조건이 정상적으로 유지됩니다.

작성자 - "테스트30" 검색 결과 (15페이지)

 

8. 이전 페이지 정보 유지하기

마지막으로 게시글을 "수정/삭제" 하거나, 상세 또는 수정 페이지에서 "뒤로" 버튼을 클릭했을 때 이전 페이지 정보가 유지되도록 해주면 페이징과 검색 처리는 모두 끝이 납니다.


8-1) 리스트 페이지 drawList( ) 함수 수정하기

리스트 페이지에서 상세 페이지로 이동할 때 쿼리 스트링 파라미터를 전달하도록 list.html의 drawList( )를 다음과 같이 변경해 주세요.

    // 리스트 HTML draw
    function drawList(list, num) {
    
        // 1. 렌더링 할 HTML을 저장할 변수
        let html = '';
    
        /*
         * 2. 기존에 타임리프(Thymeleaf)를 이용해서 리스트 데이터를 그리던 것과 유사한 로직
         *    기존에는 게시글 번호를 (전체 데이터 수 - loop의 인덱스 번호)로 처리했으나, 현재는 (전체 데이터 수 - ((현재 페이지 번호 - 1) * 페이지당 출력할 데이터 개수))로 정밀히 계산
         */
        list.forEach(row => {
            html += `
                <tr>
                    <td><input type="checkbox" /></td>
                    <td>${row.noticeYn === false ? num-- : '공지'}</td>
                    <td class="tl"><a href="javascript:void(0);" onclick="goViewPage(${row.id});">${row.title}</a></td>
                    <td>${row.writer}</td>
                    <td>${dayjs(row.createdDate).format('YYYY-MM-DD HH:mm')}</td>
                    <td>${row.viewCnt}</td>
                </tr>
            `;
        })
    
        // 3. id가 "list"인 요소를 찾아 HTML을 렌더링
        document.getElementById('list').innerHTML = html;
    }

 

세 번째 <td>

기존에는 제목을 클릭했을 때, <a> 태그의 href 속성을 이용해서 게시글 상세 페이지로 이동하는 구조였습니다. 변경된 코드에서는 href 속성을 무효화시키고, onclick 이벤트로 goViewPage( ) 함수를 호출합니다.

 

8-2) 리스트 페이지에 goViewPage( ) 함수 선언하기

list.html에 goViewPage( ) 함수를 선언해 주세요.

    // 게시글 상세 페이지로 이동
    function goViewPage(id) {
        const queryString = (location.search) ? location.search + `&id=${id}` : `?id=${id}`;
        location.href = '/post/view.do' + queryString;
    }

 

로직 해석

location.search를 이용해서 게시글 번호(id)와 쿼리 스트링 파라미터를 상세 페이지로 함께 전달합니다.

해당 함수에서 queryString에 삼항 연산자가 사용되었는데요. 처음 리스트 페이지로 접근했을 때는 쿼리 스트링이 비어있는 상태가 되며, 이는 현재 페이지가 1페이지임을 의미합니다. 이때 location.search는 빈 문자열(' ')을 리턴하기 때문에 게시글 번호(id)만 쿼리 스트링으로 전달하고, 이외의 경우에는 페이지 정보, 검색 조건, 게시글 번호(id)를 함께 전달합니다.

 

8-3) 상세 페이지 버튼 영역 수정하기

이제, 상세 페이지에서 "수정" 버튼과 "뒤로" 버튼을 클릭했을 때 이전 페이지 정보가 유지되도록 해주어야 합니다. 우선 view.html의 버튼 영역을 다음과 같이 변경해 주세요.

    <p class="btn_set">
        <button type="button" onclick="goWritePage();" class="btns btn_bdr4 btn_mid">수정</button>
        <button type="button" onclick="deletePost();" class="btns btn_bdr1 btn_mid">삭제</button>
        <button type="button" onclick="goListPage();" class="btns btn_bdr3 btn_mid">뒤로</button>
    </p>

 

8-4) 상세 페이지에 goWritePage( ) 함수 선언하기

    // 게시글 수정 페이지로 이동
    function goWritePage() {
        location.href = '/post/write.do' + location.search;
    }

 

로직 해석

앞에서 말씀드렸듯이 location.search는 URL의 쿼리 스트링을 리턴해 주는데요. 리스트 페이지에서 전달받은 쿼리 스트링을 있는 그대로 수정 페이지로 전달해 주면 되기 때문에 URI 경로(Path)만 view.do에서 write.do로 변경되고, 나머지 쿼리 스트링은 그대로 가지고 갑니다.

 

8-5) 상세 페이지에 goListPage( ) 함수 선언하기

    // 게시글 리스트 페이지로 이동
    function goListPage() {
        const queryString = new URLSearchParams(location.search);
        queryString.delete('id');
        location.href = '/post/list.do' + '?' + queryString.toString();
    }

 

로직 해석

new URLSearchParams( )를 이용해서 쿼리 스트링을 객체화합니다. 리스트 페이지는 게시글 번호(id)를 필요로 하지 않기 때문에 delete( ) 함수를 이용해서 게시글 번호(id)를 삭제한 후 나머지 쿼리 스트링(이전 페이지 정보)을 리스트 페이지로 전달합니다.

 

8-6) 상세 페이지 deletePost( ) 함수 수정하기

    // 게시글 삭제
    function deletePost() {
        
        const id = [[ ${post.id} ]];
        
        if ( !confirm(id + '번 게시글을 삭제할까요?') ) {
            return false;
        }

        let inputHtml = '';
        
        new URLSearchParams(location.search).forEach((value, key) => {
            inputHtml += `<input type="hidden" name="${key}" value="${value}" />`;
        })

        const formHtml = `
            <form id="deleteForm" action="/post/delete.do" method="post">
                ${inputHtml}
            </form>
        `;
        
        const doc = new DOMParser().parseFromString(formHtml, 'text/html');
        const form = doc.body.firstChild;
        document.body.append(form);
        document.getElementById('deleteForm').submit();
    }

 

로직 해석

기존에는 deleteForm을 그릴 때 게시글 번호만 hidden 파라미터로 추가했는데요. 지금은 전달받은 쿼리 스트링 파라미터를 전부 inputHtml에 담아서 폼에 추가해 주는 구조로 변경되었습니다.

참고로, 게시글 번호(id)도 쿼리 스트링에 포함되어 있기 때문에 따로 추가해주지 않아도 됩니다.

body에 추가되는 deleteForm 구조 (예시)

 

8-7) PostController의 deletePost( ) 메서드 수정하기

이제, 게시글을 삭제하면 게시글 번호(id)와 이전 페이지 정보(쿼리 스트링)가 함께 전송되기 때문에, 게시글이 삭제된 후에 이전 페이지 정보가 유지될 수 있도록 컨트롤러 구조를 변경해 주어야 합니다.

    // 게시글 삭제
    @PostMapping("/post/delete.do")
    public String deletePost(@RequestParam final Long id, final SearchDto queryParams, Model model) {
        postService.deletePost(id);
        MessageDto message = new MessageDto("게시글 삭제가 완료되었습니다.", "/post/list.do", RequestMethod.GET, queryParamsToMap(queryParams));
        return showMessageAndRedirect(message, model);
    }

 

로직 해석

기존에는 파라미터로 게시글 번호(id)만 수집했으나, 이전 페이지 정보까지 수집하기 위해 SearchDto 타입의 객체인 queryParams를 파라미터로 선언해 주었습니다.

추가적으로 message 객체를 생성할 때, 생성자의 마지막 파라미터에 처음 보는 메서드를 전달하는데요. 자세한 내용은 8-8에서 설명드리겠습니다.

 

8-8) PostController에 queryParamsToMap( ) 메서드 추가하기

    // 쿼리 스트링 파라미터를 Map에 담아 반환
    private Map<String, Object> queryParamsToMap(final SearchDto queryParams) {
        Map<String, Object> data = new HashMap<>();
        data.put("page", queryParams.getPage());
        data.put("recordSize", queryParams.getRecordSize());
        data.put("pageSize", queryParams.getPageSize());
        data.put("keyword", queryParams.getKeyword());
        data.put("searchType", queryParams.getSearchType());
        return data;
    }

 

로직 해석

해당 메서드는 deletePost( )에서 수집한 이전 페이지 정보(queryParams의 모든 멤버)를 Map에 담아 리턴해주는 역할을 하는데요. MessageDto의 생성자는 마지막 파라미터로 Map을 전달받아 객체를 생성하기 때문에 컨트롤러에 해당 메서드를 추가해 주었습니다.

참고로, 해당 기능은 컨트롤러에서 알러트 메시지 처리하기 기능의 연장선입니다. 추가적으로 뷰(HTML)로 전달할 데이터가 있다면, 유사한 방법으로 처리해 주시면 됩니다.

 

9. 이전 페이지 정보 유지 테스트 해보기


9-1) 상세 페이지에서 뒤로 가기

먼저, 제목으로 "30번"을 검색한 후 15페이지로 이동한 결과입니다.

제목 - "30번" 검색 결과 (15페이지)

 

15페이지 최상단의 180번 게시글 상세 페이지로 이동했습니다.
(이때 URL 쿼리 스트링이 정상적으로 연결되는지 꼭 확인해 주세요.)

180번 게시글 상세 페이지 (15페이지)

 

상세 페이지에서 "뒤로" 버튼을 클릭하면, 상세 페이지로 이동하기 전의 검색 조건과 페이지 번호가 유지됩니다.

제목 - "30번" 검색 결과 유지 (15페이지)

 

9-2) 상세 페이지에서 게시글 삭제하기

다음은 전체 검색으로 "테스터77"을 검색한 후 마지막(36) 페이지로 이동한 결과입니다.

전체 검색 - "테스터77" 검색 결과 (36페이지)

 

36페이지의 2번 게시글 상세 페이지에서 게시글을 삭제해 보겠습니다.

2번 게시글 상세 페이지 (36페이지)

 

2번 게시글이 삭제된 상태로, 이전 페이지 정보가 정상적으로 유지되고 있습니다.

전체 검색 - "테스터77" 검색 결과 유지 (15페이지)

 

다음은 deletePost( )에 추가로 선언한 SearchDto 타입의 객체인 queryParams로 넘어온 파라미터 구조입니다.

PostController - deletePost( )

 

queryParams를 queryParamsToMap( ) 메서드의 인자로 전달하면, 다음과 같이 Map에 이전 페이지 정보를 담아 리턴해 줍니다.

queryParamsToMap( ) 실행 결과

 

9-3) 수정(등록) 페이지에서 게시글 수정하기 & 뒤로 가기

이제, 게시글을 수정했을 때와 뒤로 버튼을 클릭했을 때, 두 가지 경우에 대해서만 이전 페이지 정보를 유지해 주면 되는데요. 여기서부터는 여러분에게 숙제를 드려보고자 합니다.

상세 페이지에서 처리한 로직들과 유사하기 때문에 여기까지 잘 따라오셨다면 충분히 진행하실 수 있을 겁니다. 전체 소스에 로직을 작성해 두었으니, 급하신 분들은 첨부파일을 다운로드해서 전체 소스 코드를 확인해 주세요.

추가적으로 "새로운 게시글을 작성할 땐 이전 페이지 정보를 유지할 필요가 없나요?"라는 생각을 하시는 분들도 계실 텐데요. 신규(최신) 게시글은 리스트의 가장 첫 번째 페이지인 '1페이지'에 노출되기 때문에 이전 페이지 정보를 유지하지 않아도 됩니다.

 

마치며

드디어 페이징과 검색 기능 구현이 모두 끝이 났습니다. 이번의 두 기능은 두 번의 포스팅으로 나누었음에도 정말 짧지 않은 분량이었네요... 따라오시느라 정말 고생 많으셨습니다!

아직 웹 개발에 익숙하지 않으신 분들은, 당장은 페이징과 검색 처리가 어렵게 느껴지실 수도 있는데요. 노력은 절대로 여러분을 배신하지 않습니다. 꾸준히 반복적으로 연습하고 또 연습하다 보면, 아주 조금씩이나마 본인이 성장하고 있음을 꼭 느끼시게 될 겁니다.

다음 글부터는 REST API 방식의 댓글 기능을 구현해 볼 건데요. 자바스크립트 라이브러리 중 하나인 jQuery의 Ajax를 이용해서, 화면의 움직임(페이지 이동 또는 새로 고침) 없이 데이터를 주고받을 수 있는 비동기(Asynchronous) 방식으로 데이터와 화면(HTML)을 컨트롤해 보겠습니다.

오늘도 방문해 주신 여러분께 진심으로 감사드립니다. 좋은 하루 보내세요 :)

 

Board.zip
0.85MB

반응형

댓글