
최근 무한스크롤 기반의 일기 웹사이트 프로젝트를 진행하던 도중, 일기 전체, 내가 쓴 일기 목록, 열람 기록, 북마크 리스트 등 대부분의 리스트 API에 페이징이 필요했다.
매번 Page를 썼는데 Page에는 일기가 만약 수십만건이라하면 수십만건의 데이터 조회쿼리가 들어가기 때문에 비효율적이라고 생각해서 좀 더 무한 스크롤 기능에 어울리는 기능인 Slice를 써보았는데 효율적인 것 같아서 글을 적어보겠다.
| 구분 | Page | Slice |
|---|---|---|
| 총 개수 | totalElements, totalPages 제공 | 제공하지 않음 |
| 다음 페이지 여부 | hasNext() | hasNext() |
| 추가 정보 | totalElements, totalPages | 없음 |
| 쿼리 비용 | SELECT COUNT(*) 필요 | 불필요 |
| 용도 | 페이지 기반 UI, 전체 건수 필요할 때 | 무한 스크롤 |
내 프로젝트의 /api/diaries/public, /api/diaries/feed/recent, /api/diaries/me, /api/views/me API는 모두 Page 기반으로 구성되어 있었다.
하지만 피드나 일기 목록은 사용자가 스크롤로 내려가며 끝없이 내려보는 형태가 이상적이며, 전체 개수를 표시할 필요도 없었다.
무한 스크롤에 최적화
hasNext로 충분성능 개선
total count를 구하기 위한 SELECT COUNT(*) 쿼리가 무조건 발생30ms * 5000회 = 150,000ms 이 발생하므로 2.5분의 순수한 오버헤드가 줄어든 셈이다./api/diaries/feed/recent?page=0&size=10 요청LIMIT 10 OFFSET 0 으로 10개 데이터를 가져오고, 다음 페이지 존재 여부 판단 후 hasNext = true/false 반환hasNext=true 면 다음 페이지(page=1) 요청, 아니면 호출 중단Slice를 쓰려면 리포지토리에서 메서드 반환 타입만 Page → Slice로 바꿔주면 끝이다.
public interface DiaryRepository extends JpaRepository<Diary, Long> {
Slice<Diary> findAllByVisibleTrueOrderByCreatedAtDesc(Pageable pageable);
}
서비스/컨트롤러에서 기존 Page로 받던 부분을 Slice로 변경하면 된다.
@GetMapping("/api/diaries/feed/recent")
public ResponseEntity<Slice<VisibleDiarySummaryDto>> getRecentFeed(
@PageableDefault(size = 10, sort = "createdAt", direction = DESC) Pageable pageable
) {
Slice<VisibleDiarySummaryDto> feed = diaryService.getRecentFeed(pageable);
return ResponseEntity.ok(feed);
}
이렇게 하면 무한 스크롤에 필요한 hasNext, content, size, number 등의 정보만 담긴 가벼운 응답으로 전환할 수 있다.
{
"content": [
{
"id": 8,
"title": "월요일 싫어",
"content": "내일 출근하기 싫다...",
"createdAt": "2025-07-02T09:26:57.496",
"viewed": true,
"totalReactionCount": 3,
"commentCount": 2
}
],
"pageable": { ... },
"last": false,
"first": true,
"hasNext": true,
"size": 10,
"number": 0
}
프론트는 hasNext만 보고 다음 페이지를 호출하면 되므로 구현과 유지가 단순해진다.
그 외의 대부분의 피드/리스트에서는 Slice 기반 무한 스크롤이 유리하다.
내 프로젝트 기준으로도 체감되는 효율 개선이 있었고, 불필요한 COUNT 쿼리를 없앰으로써 대량 데이터에서 성능 병목을 방지할 수 있다 !
구현하는 서비스가 무한 스크롤 기반이라면, Page 대신 Slice로 전환해 보길 추천한다 👍