본문 바로가기
Spring/Thymeleaf

Thymeleaf - 페이징 화면 그리기

by WangTak 2022. 8. 23.
반응형

오랜만에 블로그 포스팅을 하는 거 같은데 처음으로 블로그를 접했을 때의 기분이 드네요! 공백 시기의 내용은 다른 글에서 정리하는 것으로 하고 바로 글을 적어 내려가 보겠습니다. 이번 글에서는 Thymeleaf에서 간단한 페이징 화면을 그리는 내용을 정리해보려고 합니다.

 

프로젝트 버전

  • 개발 도구: IntelliJ Ultimate
  • Spring Boot: 2.6.11
  • Java 11
  • h2 Database
  • Thymeleaf, Spring Web, Spring Data JPA, Querydsl

 

전체 코드는 제 Github에 올려두었으니 조금 더 직관적으로 확인하고 싶으신 분들은 확인해주시면 좋을 거 같습니다!

 

GitHub - DahamLeee/Tistory-Code

Contribute to DahamLeee/Tistory-Code development by creating an account on GitHub.

github.com

 

프로젝트의 구조는 다음과 같습니다!

프로젝트 구조

 

간단하게 생성하고 조회할 수 있는 MVC 형태입니다! 본 글은 Thymeleaf를 작성하는 방법에 대한 내용이기 때문에 작성한 Java 코드 및 JPA, Querydsl 등 다른 기술에 대한 내용은 다른 글에서 조금 더 심화된 내용으로 정리해보도록 하겠습니다!

 

현재 프로젝트는 다음과 같습니다.

1. 프로젝트 실행 시 100명의 임시 회원 데이터를 추가합니다. [InitData에서 수행]

2. 메인 화면에서는 100명의 임시 회원에 대한 정보를 페이징을 통해 조회할 수 있습니다.

3. 한 페이지는 10명의 회원을 조회할 수 있으며, 총 10개의 페이지 번호를 확인할 수 있습니다.

-> 총 100명의 회원을 한 페이지 당 회원의 수(10명)로 나눴기 때문에 총 10개의 페이지 번호가 도출됩니다.

4. 맨 처음으로 가기, 이전 페이지(-1이 아닌 -10)로 가기, 페이지 번호, 다음 페이지(+1이 아닌 +10)로 가기, 맨 마지막으로 가기 이렇게 총 5개의 부분이 존재합니다.

 

프로젝트를 실행하면 아래 사진과 같이 임시 회원 정보를 조회할 수 있는 화면이 나옵니다.

프로젝트 실행 시 처음 화면

 

ul, li 테그로 보이는 번호는 페이지의 번호입니다! 해당 번호, 버튼을 누르면 각각의 페이지에 맞는 회원 정보를 조회할 수 있습니다!

 

members.html의 코드는 다음과 같습니다.

 

html 테그에 있는 class를 보면 d-flex, justify-content, btn 등을 그대로 남겨두긴 했지만 프로젝트에 Bootstrap과 같은 프레임워크를 사용하지 않아서 정말 기본적인 UI가 그려집니다! 그나마 괜찮은 UI를 그리고 싶으신 분들은 위를 참고하셔서 Bootstrap이나 css를 적절히 활용하셔서 커스텀해주시면 될 거 같습니다!
<!doctype html>
<html lang="ko"
      xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>

<div id="memberList">

    <div class="row">
        <div th:each="member: ${members}">
            <div th:text="|Name = ${member.name}, Age = ${member.age}|"></div> <!-- 1. -->
        </div>
    </div>

    <div class="d-flex justify-content-center box-footer clearfix">
        <ul th:style="${members.number >= members.totalPages ? 'display:none' : ''}"
            class="pagination pagination-sm no-margin pull-right"
            th:with="
            start=${(members.number / 10) * 10 + 1},
            last=(${members.number / 10 * 10 + 10 < members.totalPages ? (members.number / 10) * 10 + 10 : members.totalPages})
            "> <!-- 2. -->
            <li th:style="${members.first ? 'display:none' : ''}">
                <a class="btn btn-outline-black" th:href="@{/(page=${0})}">
                    <span aria-hidden="true">&laquo;</span> <!-- 3. -->
                </a>
                &nbsp;
            </li>
            <li th:style="${members.number < 10 ? 'display:none' : ''}">
                <a class="btn btn-outline-black" th:href="@{/(page=${members.number / 10} * 10 - 1)}">
                    <span aria-hidden="true">&lt;</span> <!-- 4. -->
                </a>
                &nbsp;
            </li>
            <li th:each="page: ${#numbers.sequence(start, last)}">
                <a class="btn btn-outline-black"
                        th:text="${page}"
                        th:href="@{/(page=${page - 1})}"
                        th:classappend="${page == members.number + 1} ? 'active'">
                </a> <!-- 5. -->
                &nbsp;
            </li>
            <li th:style="${last == members.totalPages ? 'display:none' : ''}">
                <a th:href="@{/(page=${last})}">
                    <span aria-hidden="true">&gt;</span> <!-- 6. -->
                </a>
                &nbsp;
            </li>
            <li th:style="${members.last ? 'display:none' : ''}">
                <a class="btn btn-outline-black" th:href="@{/(page=${members.totalPages - 1})}">
                    <span aria-hidden="true">&raquo;</span> <!-- 7. -->
                </a>
            </li>
        </ul>
    </div>
</div>

<script th:inline="javascript">
    /*<![CDATA[*/
    let members = /*[[ ${members} ]]*/;
    /*]]*/
    console.log(members);
</script>

</body>
</html>

 

코드에 번호로 주석을 남겨뒀는데 해당 부분을 설명하면 다음과 같습니다!

 

1.
Controller로부터 받은 [Model - Page<MemberDto> members]를 화면에 표현하기 위해 Thymeleaf 문법을 적용했습니다. 

2.
th:style을 보시면 members.numbermembers.totalPages를 비교하고 있습니다. 그 이유는 현재 query-string의 형태로 page 번호 상태를 유지하고 있는데, 현재 페이지 번호를 나타내는 number가 클라이언트의 외부 조작으로 변경될 수 있기 때문에 10 이상의 숫자(0부터 시작하기 때문에 10개는 0~9 임)를 입력할 경우 이동할 수 있는 페이지 버튼을 보여주지 않기(display:none) 위함입니다.

 

th:with특정 태그 밑에서 값을 활용할 수 있도록 커스텀 변수를 정의하는 것입니다. start, last 이렇게 2개를 정의하고 있는데요. 각각의 변수가 의미하는 바는 다음과 같습니다.

start: 페이지 번호의 시작 번호 (ex. 1, 11, 21, 31 ...)

last: 페이지 번호의 마지막 번호 (여기는 일반화할 수 없기에 현재 프로젝트로 예시를 들면 10입니다.)

 

startlast가 왜 필요한지 설명해드리면 다음과 같습니다.

먼저, 현 프로젝트는 한 화면에 보여주는 페이지 버튼의 수를 10으로 고정해둔 상태입니다. 즉, 무수히 많은 데이터가 존재한다고 가정할 때 (1, 2, 3,..., 10), (11, 12, 13,..., 20).. 이런 식으로 보여주게 됩니다. 그렇다면 우리가 알고 있는 정보, 현재 페이지 번호(members.number)를 토대로 어느 그룹에 속하고, 출발점이 어디인지를 구해야 합니다.

 

또한, 해당 그룹에서 마지막 번호가 무조건 10, 20, 30... 일 수는 없습니다. (11, 12, 13) 이런 식으로 해당 그룹의 마지막 번호는 현재 보유하고 있는 데이터의 수에 따라서 바뀌게 됩니다. 그렇기 때문에 현재 페이지 번호(members.number)와 최대 페이지 수(members.totalPages)를 활용하여 마지막 번호를 구해야 합니다.

 

그래서 (현재 페이지 번호(members.number)를 페이지 버튼의 수(10)로 나누고, 10을 곱한 후에 1을 더해줌)으로써 출발점을 구합니다.

 

(현재 페이지 번호(members.number)를 페이지 버튼의 수(10)로 나누고, 10을 곱한 후에 10을 더하면) 현재 그룹의 마지막 번호를 구한 것입니다. (ex. 10, 20, 30...) 그래서 현재 그룹의 마지막 번호와 최대 페이지 수의 값을 비교하여 더 작은 페이지 번호를 마지막 번호로 정의하였습니다.

 

3.

이 부분은 현재 프로젝트에서 존재하지는 않지만 "<<" 버튼으로 맨 앞으로 이동하기입니다. 하지만 맨 처음 페이지일 경우 보여줄 필요가 없기 때문에 위처럼 th:style을 적용하였습니다.

 

4.

이 부분 또한 현재 프로젝트에서 존재하지는 않지만 "<" 버튼으로 이전 그룹으로 이동하기입니다. 하지만 맨 처음 그룹일 경우 보여줄 필요가 없기 때문에 위처럼 th:style을 적용하였습니다.

 

5. 

이 부분은 위에서 정의한 start, last를 이용하여 현재 그룹의 페이지 번호 목록을 보여주는 구문입니다. th:each 문을 통해 start부터 last까지 li태그를 만들고 각각의 li > a에는 th:text를 통한 페이지 번호를 보여주고 눌렀을 때 어떤 페이지로 이동할지에 대한 th:href를 사용하였습니다.

 

6. 

이 부분은 ">" 버튼으로 다음 그룹으로 이동하기입니다. 다음 그룹이 존재하지 않는 경우 보여주면 안 되기 때문에 위처럼 th:style을 적용하였고 현재 프로젝트에서 11번으로 시작하는 그룹이 없기 때문에 보이지 않습니다.

 

7. 

이 부분은 ">>" 버튼으로 맨 마지막 페이지로 이동하기입니다. 맨 마지막 페이지일 경우 보여줄 필요가 없기 때문에 위처럼 th:style을 적용하였습니다.

 

밑에는 Page 인터페이스를 타입으로 가지는 Page<MemberDto> members를 통해 알 수 있는 정보를 정리한 것입니다. 이것 이외에도 다양한 정보를 가지고 있습니다! members.html 밑에 Page<MemberDto> members를 확인할 수 있는 console.log 코드까지 같이 포함하였으니 확인해보시면 좋을 거 같습니다!

 

members.number: 멤버 리스트의 현재 페이지 번호 (Page 인터페이스는 페이지 번호를 0번부터 시작함)
members.totalPages: 멤버 리스트로 도출할 수 있는 최대 페이지 수 (ex. 총 멤버 (100명) / 페이지 당 회원 수 (10명) = 10)
members.first: 현재 페이지 번호가 맨 처음 페이지인지를 알려주는 변수 (boolean 타입으로 true/false를 가짐)
members.last: 현재 페이지 번호가 가장 마지막 페이지인지를 알려주는 변수 (boolean 타입으로 true/false를 가짐)
members.numberOfElements: 한 페이지에서 보여주는 아이템의 개수

 

이 방법 이외에도 페이징을 적용할 수 있는 방법은 많이 있다고 생각합니다! 특히나 UX적인 측면에서 맨 앞으로 이동하기맨 뒤로 이동하기는 사용자가 잘 못 눌렀을 경우 안 좋은 상황을 초래할 수 있다는 정보를 접하기도 했었는데요! 각 상황에 맞게 잘 커스텀(페이지 당 아이템 수 변경, 회원 검색 후 페이징, 한 화면에 보여주는 페이지 버튼 조절)하여 좋은 서비스를 만들면 될 거 같습니다!

 

반응형