ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 최종 프로젝트 - 게시판 페이지 처리
    프로그래밍/프로젝트 2019. 7. 14. 01:05

    Github 주소: https://github.com/LuiGee3471/BitCampFinal

     

    LuiGee3471/BitCampFinal

    Contribute to LuiGee3471/BitCampFinal development by creating an account on GitHub.

    github.com

     페이지 처리, 저번 프로젝트 때 많이 고생했던 문제 중 하나입니다. 그래서 이번 프로젝트에는 괜찮지 않을까했는데 여전히 힘들더라고요. 그래도 저번 프로젝트 때보다 좀더 개선한 방법이 있어서 소개해보려고 합니다.

     

    1. LIMIT를 이용한 페이지 처리

     저번 프로젝트에서는 rownum 개념을 이용해서 페이지에 맞는 게시물을 가져왔습니다. MySQL에는 자체적으로 rownum이 없기도 하고 rownum 기능이 있는 오라클에서도 개인적으로는 여러 번 서브쿼리를 이용해야 하는 것이 좀 불편했던지라 그렇게 좋아하지는 않았는데요. 이번에도 일단은 그렇게 하다가 다른 조원에게 LIMIT를 이용해 더 간단하게 페이지 처리를 하는 방법을 알아왔습니다.

    SET @rownum:=0;
    SELECT * 
    FROM (SELECT @rownum:=@rownum + 1 as no, p.*
          , round(time_to_sec(timediff(NOW(), time)) / 60) as diff
          , date_format(time, '%m/%d %H:%i') as timeFormat
          , s.staff_id "
          FROM post p
          LEFT JOIN staff s 
          ON p.writer_id = s.id
          WHERE boardtype_id = ? 
          ORDER BY time DESC) q
    WHERE no > ? "
    LIMIT 20;

     이것이 지난 프로젝트 때 이용한 rownum 이용 코드입니다. rownum이 부여가 된 후에 사용이 가능하기 때문에 단순히 그 이유 하나로 서브쿼리를 쓰게 되고 막상 핵심인 SQL문을 읽기 힘들어집니다.

    SELECT id, title, content, time, updated_time, view_count, original_id, enable, level, username, board_id,
    FROM ARTICLE
    WHERE BOARD_ID=#{board_id} AND ENABLE=1
    ORDER BY original_id DESC, level ASC
    LIMIT 0, 10

      이 코드는 이번에 사용하는 LIMIT를 이용한 페이지 쿼리문입니다. 불필요한 서브쿼리가 없으니 훨씬 읽기가 편해집니다. LIMIT가 숫자를 두 개 받을 수 있는데요. 앞의 숫자는 제외할 행의 숫자, 뒤의 숫자는 최대 몇 개까지 행을 불러올 지 알려주는 숫자입니다. 이 경우는 0개 제외, 10개까지를 의미합니다. 한 번에 글 10개를 보여주는 게시판의 1페이지라고 할 수 있겠죠. 2페이지는 LIMIT 10, 10이 될 것입니다. rownum처럼 몇 번부터 몇 번까지를 생각할 필요도 없고 페이지에 맞게 LIMIT 앞의 숫자만 바꿔주면 됩니다.

     

     다만, DB에 별도로 숫자를 부여하지는 않았는데 숫자를 나타내고 싶다면 역시 임의로 붙여주는 rownum을 써야겠죠. 그 외에는 LIMIT를 이용한 페이지가 훨씬 편하다고 생각합니다.

     

    2. 페이지 처리를 위한 별도의 클래스 생성

     저번 프로젝트 때는 게시판에 페이지 수와 현재 페이지 등을 넘겨주고 그 게시판에서 바로 페이지 수를 계산해서 사용했습니다. JSP니까, 또 게시판 페이지 종류가 하나밖에 없어서 가능한 일이긴 했지만 생각해보면 꽤나 무식한 방법이기도 했습니다. 이번에는 조원이 인터넷에서 찾은 아이디어를 기반으로 우리만의 페이지 처리 도우미 클래스를 하나 만들어보기로 했습니다. 아래가 저번 프로젝트 때 JSP 파일 상단에 있던 페이지였습니다.

      int totalPages = (int) request.getAttribute("pages");
      int currentPage = (int) request.getAttribute("currentPage");
      int startPage = 1;
      if (currentPage >= 4) {
        if (currentPage % 3 == 1) {
          startPage = currentPage;
        } else if (currentPage % 3 == 2) {
          startPage = currentPage - 1;
        } else {
          startPage = currentPage - 2;
        }
      }
      
      int endPage = 0;
      if (totalPages <= 3) {
        endPage = totalPages;
      } else {
        if (totalPages - startPage >= 3) {
          endPage = startPage + 2;
        } else {
          endPage = totalPages;
        }
      }
      
      pageContext.setAttribute("startPage", startPage);
      pageContext.setAttribute("endPage", endPage);

     이번 프로젝트에서 사용한 방법은 이렇습니다. 페이지 정보를 담고 있는 클래스를 생성한다. 해당 클래스 객체를 페이지에 넘겨준다. 페이지의 템플릿 엔진(여기서는 Thymeleaf)를 이용해서 페이지 버튼과 이전, 다음 버튼 등을 구성한다. 그리고 아래 코드가 저희가 만든 페이지 클래스입니다.

    public class Pager {
      private final int articlesOnPage = 10; // 한 페이지에 보여주는 글 개수
      private final int pageButtons = 5; // 한 번에 보여줄 페이지 버튼의 개수
      private int currentPage; // 현재 페이지
      private int startPage;   // 페이지 버튼의 가장 앞의 버튼 숫자
      private int endPage;     // 페이지 버튼의 가장 뒤의 버튼 숫자
      private int totalPages;  // 총 페이지 수
      private boolean prev;    // 이전 버튼이 필요한지
      private boolean next;    // 다음 버튼이 필요한지
      private int prevPage;    // 이전 버튼을 누르면 넘어갈 페이지 번호
      private int nextPage;    // 다음 버튼을 누르면 넘어갈 페이지 번호
    
      private int start;       // 페이지에 맞는 글을 위해 SQL에 삽입할 숫자 (위의 쿼리문 LIMIT 첫 번째 숫자)
      private int end;         // rownum 사용 시 가져올 마지막 행 번호 
    
      public Pager(int currentPage, int totalArticles) {
        this.currentPage = currentPage;
        this.start = (currentPage - 1) * articlesOnPage;
        this.end = currentPage * articlesOnPage;
        int q = totalArticles / articlesOnPage;
        int r = totalArticles % articlesOnPage;
        this.totalPages = (r == 0 && totalArticles != 0) ? q : (q + 1);
    
        this.startPage = ((currentPage - 1) / pageButtons) * pageButtons + 1;
    
        if (((currentPage - 1) / pageButtons + 1) * pageButtons < totalPages) {
          this.endPage = ((currentPage - 1) / pageButtons + 1) * pageButtons;
          this.next = true;
          this.nextPage = endPage + 1;
        } else {
          this.endPage = totalPages;
        }
    
        this.prev = (currentPage <= pageButtons) ? false : true;
        this.prevPage = startPage - 1;
      }

     지금 다시 보니까 복잡해보이기 짝이 없네요. 사용 방법을 간단하게 만드려고 하다보니 그 내부가 좀 복잡해졌습니다. 글을 쓰다보니 뭐하러 저렇게 만들었지 싶은 것들이 많아서 절반 이상은 고쳐버렸네요. 아무튼 사용 방법은 생성자 함수에 현재 페이지와 그 게시판의 총 글 개수만 넣어주면 끝납니다.

     

     페이지 당 게시글 개수가 10개고 페이지 버튼은 5개라는 가정 하에 한 줄씩 살펴보겠습니다.

     

     우선 현재 페이지를 그대로 받습니다. SQL에서 사용할 숫자를 만듭니다. 10개짜리 글을 보여주는 게시판의 1페이지를 가져오기 위해서는 LIMIT 0, 10이 필요하겠죠. 그래서 2번째 줄에서 start가 (1 - 1) * 10 = 0이 됩니다. end도 비슷한 개념입니다. 프로젝트 초반에 rownum을 사용했기 때문에 있는 변수입니다.

     

     총 페이지 수는 몫(q, quotient를 줄임)과 나머지(r, remainder를 줄임)를 이용해 구합니다. 나머지가 0이라면 10, 20, 30 등 딱 맞는 숫자라는 이야기고 이 경우는 페이지 수가 몫과 일치합니다. (10 / 10 = 1페이지), 그 외 경우는 몫보다 하나 더 큰 페이지 숫자를 보여줘야 나머지 글을 보여줄 수 있겠죠. totalArticles != 0 조건의 경우 글이 아예 없을 때 2페이지를 보여주는 경우가 있어 추가했습니다.

     

     그 다음 startPage와 endPage입니다. startPage는 어느 경우에나 1, 6, 11, 16...입니다. (사실 쓰면서 떠올랐습니다. 훨씬 복잡하게 따졌었는데) 그렇기 때문에 현재 페이지에서 1을 뺀 값을 5로 나눈 몫에다 5를 곱하고 1을 더해줍니다. 

     

     endPage는 startPage보다는 복잡해집니다. 왜냐면 이쪽은 상황에 따라 바뀌거든요. 다음 페이지가 있어서 꽉 채우는 경우, 5, 10, 15...가 마지막 페이지일 거고 페이지가 넘어가지 않는 경우면 중간에 끊겨버리겠죠. if-else문은 그걸 나타낸 페이지입니다. if는 아직 다음 버튼을 눌러 더 보여줄 페이지가 있다는 조건이고 그 경우 다음 버튼이 나타날지 결정하는 next는 true가 되고 다음 버튼을 눌렀을 때 페이지는 endPage의 바로 다음 페이지가 됩니다. 만약 모자라다면 총 페이지 수가 마지막 페이지가 되겠죠.

     

     이전 버튼이 있는지는 훨씬 간단합니다. 첫 버튼들만 아니면 되겠죠. 이 경우 5페이지 이하라면 전부 이전 페이지는 없습니다. 이전을 누르면 가는 페이지는 startPage의 바로 이전 페이지가 되니까 -1입니다.

     

     그리고 이 객체를 컨트롤러나 서비스에서 만들어서 페이지에 전달해줍니다.

            <div class="page-btns">
              <span th:if="${pager.prev}">
                <a th:href="@{/myclass/board(page=${pager.prevPage},board_id=${board.id})}">
                  &laquo;
                </a>
              </span>
              <span th:each="idx, iterStat : ${#numbers.sequence(pager.startPage,pager.endPage)}">
                <a
                  th:if="${boardSearch} == null"
                  th:href="@{/myclass/board(page=${idx},board_id=${board.id})}"
                  th:classappend="${pager.currentPage} == ${idx} ? active"
                  th:text="${idx}"
                ></a>
              </span>
              <span th:if="${pager.next} and ${pager.endPage > 0}">
                <a th:href="@{/myclass/board(page=${pager.nextPage},board_id=${board.id})}">
                  &raquo;
                </a>
              </span>
            </div>

     Thymeleaf에서는 이렇게 활용합니다. if와 each가 많기는 하지만 그렇게 복잡하지는 않습니다. prev와 next가 true일 때만 이전, 다음 버튼이 생기고 그 링크에는 prevPage와 nextPage로 가는 값이 들어갑니다. 그 사이에는 startPage와 endPage를 끝으로 하는 each문이 돌아가면서 페이지 버튼을 만들고 만약 현재 페이지라면 active라는 클래스를 추가해서 다르게 표현될 수 있는 CSS를 추가해줍니다. 

     

    3. 무한 스크롤 방식 페이지 처리

     모바일에서 자주 쓰이는 것처럼 무한 스크롤을 만들 때도 결국은 페이지 처리와 이론은 똑같더라고요. 다만 페이지 버튼이나 현재 페이지같은 것을 사용자에게 보여주지 않을 뿐이죠. 다만 이번에는 이걸 비동기로 하고 클라이언트 쪽에서 처리해야 합니다. 몇 번째 글까지 불러왔는지 알아야 다음 글도 불러올 수 있겠죠. 전역 변수를 하나 선언하고 거기다 저장해도 되겠지만 전역 변수를 가능한 쓰지 말라는 이야기도 들었고 해서 예전에 살짝 봤던 클로저 함수를 한 번 적용해보고 싶었습니다.

    function getNextArticles() {
      let lastArticleId = [[${videoList[videoList.size() - 1].id}]];
            
      function changeId(id) {
        lastArticleId = id;
      }
            
      return function() {
        $.ajax({
          url: "/ajax/video/scroll",
          data: {
            article_id: lastArticleId
          },
          method: "post",
          success: function(data) {
            if (data[0]) {
              changeId(data[data.length - 1].id);
              addNewPage(data);
            }
          }
        });
      }
    }

     lastArticleId는 이 함수 내에서만 존재하는 변수입니다. 하지만 이 함수의 반환값이 또다른 함수(ajax를 사용하는 함수)이고 그 함수가 lastArticleId를 참고하기 때문에 함수가 끝난다고 사라지지 않고 계속 존재하게 됩니다. (참고로 변수 선언 시 옆에 있는 이상한 표현은 thymeleaf에서 JavaScript에 값을 넘겨줄 때 사용하는 표현입니다) 다른 곳에서 사용할 수는 없지만 계속 살아는 있는 그런 변수가 되죠. success의 함수에서 활용하기 위해 changeId라는 내부 함수를 만들어주고 변경했습니다.

     

     getNextArticles가 반환하는 함수를 변수에 저장해 사용할 수 있습니다. 이 페이지에 들어오면 lastArticleId는 맨 처음 불러온 글 목록의 가장 마지막 글의 글 번호를 가지게 됩니다. 그리고 변수에 저장한 함수를 사용하면 저장되어있던 lastArticleId는 비동기 요청에 포함되어 보내지고 응답이 오면 거기서 보내준 글 목록의 마지막 글 번호로 값이 바뀝니다. addNewPage는 글을 페이지에 새롭게 붙여주는 함수입니다.

     

     개인적으로 이번 프로젝트 때 꼭 해보고 싶었던 것 중 하나가 이 무한 스크롤 구현인데 이렇게 클로저와 내부 함수까지 사용해서 구현할 수 있어서 기쁩니다. 기회가 된다면 jQuery 없이 순수 자바스크립트로 구현하고 싶네요. fetch API를 쓰면 가능할 것 같습니다.

     

     

     

     다양하게 고민하고 찾아보면서 여러 방법을 알아내고 시도해볼 수 있었습니다. 혹시나 방법을 찾는 다른 분들이 있다면 도움이 될만한 정보면 기쁘겠네요.

Designed by Tistory.