// 서비스
@Transactional(readOnly = true)
public List<PostDto> getPosts(String username, PostsConditionForm cond) {
User userFind = userService.findUserByUsername(username);
List<Post> posts = postRepository.searchPosts(cond, username);
return posts.stream()
.map(post -> PostDto.of(post, userFind))
.toList();
}
//리포지토리
public List<Post> searchPosts(PostsConditionForm form, String username) {
QPost p = new QPost("p");
QPostTag pt = new QPostTag("pt");
QTag t = new QTag("t");
QSeries s = new QSeries("s");
QUser u = new QUser("u");
return queryFactory
.select(p)
.from(p)
.innerJoin(p.user, u).fetchJoin()
.leftJoin(p.postTags, pt).fetchJoin()
.leftJoin(pt.tag, t).fetchJoin()
.leftJoin(p.series, s).fetchJoin()
.where(
searchCondition(form.getSearch(), p),
tagCondition(form.getTagName(), p, pt, t),
seriesCondition(form.getSeriesName(), p, s),
userCondition(username, u)
)
.orderBy(p.createDt.desc())
.limit(5) // 추가됨
.distinct()
.fetch();
}
기존 로직의 서비스 코드이다. searchPosts는 동적으로 쿼리스트링으로 들어온 조건을 반영하여 post(게시글) 엔티티를 뽑아온다.
동적 쿼리를 구현하기 위해 Querydsl을 활용하여 손쉽게 구현할 수 있었다. 하지만 문제가 하나 있었는데, Paging 처리가 되어 있지 않아 특정 사용자가 1000개의 게시글을 가지고 있을 경우, 이를 모두 가져오는 상황이 발생한다는 점이다.
블로그를 방문하는 사용자 입장에서 처음부터 1000개의 게시글 리스트를 한 번에 보는 경험은 비효율적이며, 현실적이지 않다. 따라서 페이징 처리는 반드시 필요한 요소라 할 수 있다.
이를 통해 초기 로드 시 적절한 개수(예: 5개)의 게시글만 가져오고, 이후 추가적인 데이터를 점진적으로 가져오는 방식으로 UX를 개선할 수 있다.
기존 로직은 querydsl에 위 코드에서 "추가됨" 주석부분에 해당하는 최대 5개만 가져오도록 해서 변경이 없도록 두었다.
코드 테스트를 위해 단계마다 주석을 달아 console에서 관찰해보도록 하겠다.
$(window).on('scroll', function () {
console.log("Scroll event triggered");
if (shouldLoadMore()) {
console.log("Load more condition met. Current page: " + currentPage);
isLoading = true;
loadMorePosts(currentPage + 1);
}
});
// 하단 도달 여부 확인 함수
function shouldLoadMore() {
const nearBottom = $(window).scrollTop() + $(window).height() >= $(document).height() - 100;
console.log("Checking if near bottom: " + nearBottom + ", isLoading: " + isLoading);
return nearBottom && !isLoading;
}
하단에 도달했다면 loadMorePosts(currentPage + 1)로 하여금 새로운 데이터를 추가할 함수를 실행시킨다.
function loadMorePosts(page) {
console.log("Attempting to load posts for page: " + page);
$('#loading').show();
// URL에서 쿼리 스트링 파라미터 가져오기
const params = new URLSearchParams(window.location.search);
const tagName = params.get("tagName") || null;
const seriesName = params.get("seriesName") || null;
const search = params.get("search") || null;
const queryParams = {
page: page,
size: pageSize,
tagName: tagName,
seriesName: seriesName,
search: search
};
// AJAX 요청
$.ajax({
url: /*[[@{'/api/@' + ${postsViewDto.username} + '/posts'}]]*/, // 서버 API
type: 'GET',
data: queryParams,
success: function (data) {
console.log("AJAX success. Data received: ", data);
// 데이터가 존재하는 경우 DOM에 추가
if (Array.isArray(data) && data.length > 0) {
console.log("Appending posts. Number of posts: " + data.length);
appendPosts(data);
currentPage = page;
} else {
console.log("No more posts available. Removing scroll event.");
$(window).off('scroll'); // 더 이상 데이터가 없으면 스크롤 이벤트 해제
}
isLoading = false;
$('#loading').hide();
},
error: function (error) {
console.error("Error during AJAX request: ", error);
isLoading = false;
$('#loading').hide();
}
});
}
loadMorePosts(page)
로 하여금 서버로 데이터를 요청한다. 현재URL의 쿼리 스트링을 포함하여 조건에 맞는 데이터를 페이징해서 가져오도록 서버에 요청을 보낸다. 데이터가 존재하는 경우 appendPosts()
로 하여금 동적으로 받아온 데이터로 만든 subHtml을 현재 HTML에 추가한다.
function appendPosts(posts) {
console.log("Appending posts to the DOM");
posts.forEach(post => {
console.log("Appending post: ", post);
$('#post-list .row').append(`
<a href="/@${post.username}/post/${post.postUrl}" class="card posts-style my-5 text-decoration-none">
<img src="/image-print/${post.thumbnail || 'default/default_post.png'}" class="card-img-top thumbnail-posts-img" alt="Post Thumbnail" />
<div class="card-body my-2 mx-2">
<h3 class="card-title">${post.title}</h3>
<p class="card-text">${post.postSummary}</p>
</div>
<div class="card-footer">
<small class="text-muted">${post.createByDt}</small>
<small class="text-muted"> . </small>
<small class="text-muted">${post.commentCount}개의 댓글</small>
<small class="text-muted"> . </small>
<small class="text-muted">♥${post.likeCount}</small>
</div>
</a>
`);
});
}
이제 서버에서 ajax 요청을 어떻게 받는지 알아보자.
기존, 처음 5개 게시글 카드를 가져오는 로직은 그대로 두고 새로운 컨트롤러로 하여금 구현하기로 했으므로 PostsApiController
를 새로 만들어 이를 ajax 코드에서 타도록 만들었다.
@RestController
@RequiredArgsConstructor
@RequestMapping("/api")
public class PostsApiController {
private final PostsService postsService;
@GetMapping("/@{username}/posts")
public ResponseEntity<List<PostDto>> posts(
@PathVariable("username") String usernamePost,
@RequestParam("tagName") String tagName,
@RequestParam("search") String search,
@RequestParam("seriesName") String seriesName,
Pageable pageable
) {
PostsConditionForm form = new PostsConditionForm();
form.setTagName(tagName);
form.setSearch(search);
form.setSeriesName(seriesName);
List<PostDto> postsWithPageableDto = postsService.getPostsWithPageable(usernamePost, form, pageable);
return ResponseEntity.ok(postsWithPageableDto);
}
}
서비스 메서드는 통합해서 사용가능하겠지만 일단은 기존 로직을 변경하지 않도록 분리하여 새로 구현하였다. 새로운 getPosts로직은 Pageable
를 추가로 파라미터로 받게 된다.
Pageable을 리포지토리 조회 로직이 추가로 전달하는 것 말고 서비스 메서드는 달라지는 것이 없다.
@Override
public Page<Post> searchPostsWithPage(PostsConditionForm form, String username, Pageable pageable) {
QPost p = new QPost("p");
QPostTag pt = new QPostTag("pt");
QTag t = new QTag("t");
QSeries s = new QSeries("s");
QUser u = new QUser("u");
// 기본 쿼리
JPQLQuery<Post> query = queryFactory
.select(p)
.from(p)
.innerJoin(p.user, u).fetchJoin()
.leftJoin(p.postTags, pt).fetchJoin()
.leftJoin(pt.tag, t).fetchJoin()
.leftJoin(p.series, s).fetchJoin()
.where(
searchCondition(form.getSearch(), p),
tagCondition(form.getTagName(), p, pt, t),
seriesCondition(form.getSeriesName(), p, s),
userCondition(username, u)
)
.distinct();
// 페이징 설정 (offset과 limit)
long total = query.fetchCount(); // 전체 개수 조회
List<Post> content = query
.orderBy(p.createDt.desc()) // 정렬
.offset(pageable.getOffset()) // 시작 인덱스
.limit(pageable.getPageSize()) // 개수 제한
.fetch();
// Page 객체 반환
return new PageImpl<>(content, pageable, total);
}
처음 제시한 querydsl 조회로직과 다른 점은 offset과 limit에 넘겨받은 pageable를 활용하여 동적으로 페이징 설정이 가능해진 점이다.