Thymeleaf 문법을 사용해 Pagination을 구현하고자 했다.
원하는 대로 서버쪽에서 코드를 구현하는 것은 그렇게 오래걸리지 않았다. (아닌가?)
단순히 findAll()
라는 JPA 메소드를 사용하여 List
형태로 데이터를 가져왔다.
하지만 나는 페이지네이션을 사용하고자 했으므로, 반환 타입을 Page
로 변경 후, Pageable을 인자로 받도록 Repository
에 메소드를 오버라이딩했다.
우선, Page<Post>
형태로 가져오는 내용을
@Entity
인 Post
를 그대로 가져오지 않고,
PostResponseDto
에 담아서 반환하도록 하기 위해서 map을 사용했다.
// 페이지 수
Integer page = 0;
// Pageable 사용
Pageable pageable = PageRequest.of(page, 10);
// Pageable 을 인자로 하여 findAll
Page<Post> postList = postRepository.findAll(pageable);
// Product 타입을 ProductResponseDto로 변환
postList.map(PostResposneDto::new);
처음에는 Stream
을 사용해야 하나 싶었는데,
Page 내부에서도 map 메소드를 지원하므로 따로 Stream을 사용하지 않아도 되었다.
map
으로 편하게 (ProductResponseDto::new)
를 사용해서 서비스 단의 메소드 반환 타입을 Page<ProductRepsonesDto>
로 만들 수 있었다.
다음으로 만들어둔 서버 쪽의 코드와 프론트 연결이 필요했는데,
이 부분이 아주 어려웠다.
부트스트랩에서 제공해주는 페이지네이션 코드를 가져와서 사용했다.
- 이전 페이지가 없다면 이전 페이지 버튼 비활성화
- 이후 페이지가 없다면 이후 페이지 버튼 비활성화
- 각 페이지 당 보이는 게시글의 개수는 5개로 제한
- 현재 페이지 기준으로 보이는 페이지 개수는 5개
여기서 주의해야 할 점은, 페이지는 0 페이지부터 시작한다는 것이다.
우리가 생각하기에는 1페이지부터 시작하기 때문에,
화면에 페이지를 보여줄 때 그 부분도 유의해야 했다.
우선 구현한 @Controller 의 코드이다.
@GetMapping("/api/posts")
public String getPostList(Model model,
@RequestParam(value = "page", defaultValue = "0" Integer page) {
Page<PostResponseDto> postList = postService.getPostList(page);
model.addAttribute("posts", postList);
return "posts";
}
Model에 Service 단에서 받아온 Page<PostResponseDto>
를 담아주고,
posts
페이지를 return 했다.
그리고 그에 맞게 html 코드를 수정해주었다.
<div class="paging" th:if="${!posts.isEmpty()}">
<nav aria-label="Page navigation">
<ul class="pagination">
<!-- 이전 -->
<li class="page-item" th:classappend="${!posts.hasPrevious} ? 'disabled'">
<a class="page-link" th:href="@{|?page=${posts.number-1}|}" aria-label="Previous">
<span aria-hidden="true">«</span>
</a>
</li>
<!-- 페이지 -->
<li class="page-item"
th:each="page : ${#numbers.sequence(0, products.totalPages-1)}"
th:if="${page >= products.number-5 and page<=posts.number+5}"
th:classappend="${page == posts.number} ? 'active'">
<a class="page-link" th:text="${page+1}" th:href="@{|?page=${page}|}"></a>
</li>
<!-- 이후 -->
<li class="page-item" th:classappend="${!posts.hasNext} ? 'disabled'" >
<a class="page-link" th:href="@{|?page=${posts.number+1}|}" aria-label="Next">
<span aria-hidden="true">»</span>
</a>
</li>
</ul>
</nav>
</div>
Thymeleaf 문법을 사용하여 코드를 작성했다.
해당 문법을 사용할 때의 주의점은 "" / ''
을 잘 열고 닫아야 한다는 점 ..?
해당 문자열을 막 작성하다가 놓친 단 하나의 부분 때문에 계속 실행이 안됐었다.
Caused by: org.attoparser.ParseException: (Line = 117, Column = 21) Malformed markup: Attribute "class" appears more than once in element
정말 아이러니 한 점은, 오류에서 알려준 117번째 줄에서 아무런 문제가 없었다는 점이다.
class 속성이 한 요소 내에 한 번 이상 나타난다고 하는데 ,, 아무리 코드를 뜯어봐도 그런 사항이 없었는데 왜 ..? 라는 의문이 끊이질 않았다.
결국 부분 부분 마다 주석 처리를 해서 확인해보니,
117번 째 두 줄 위에, 즉 115번 째 줄에 th:if
을 주는 부분에 "
하나가 닫혀있지 않았다 ^^
해당 부분을 수정해주니 다른 코드에서는 큰 문제 없이 잘 동작했다.
다만 현재 페이지를 출력하는 코드에서,
th:text="${page}"
가 아닌, th:text="${page + 1}"
로 해야 우리가 보는 것처럼 1페이지부터 시작할 수 있다.
페이지네이션은 언제나 어려운 것 같다.
적용할 때, 서버쪽에서 PageRequest.of()
에 세 번째 인자에 정렬에 관한 속성도 넣을 수 있다는 점과,
Thymeleaf 문법을 사용해서 구현할 때는 "" / ''
가 잘 열리고 닫혔는지 꼼꼼하게 확인하면 좋을 것 같다.
이후에는 무한 스크롤을 구현해볼 예정인데,
해당 기능을 구현할 때는 Page가 아닌 Slice로 구현한다고 한다.
집중해서 구현해봐야겠다 !