๐Ÿ’ป PROJECT/[Spring Boot, React] ๋…์„œ ์Šต๊ด€ ๊ด€๋ฆฌ ์„œ๋น„์Šค

[ํ”„๋กœ์ ํŠธ] ํŽ˜์ด์ง€๋„ค์ด์…˜ ๋ฐฉ์‹ ์„ ํƒ ๊ณผ์ •

devCloud 2026. 5. 1. 12:41
728x90
PROJECT

ํŽ˜์ด์ง€๋„ค์ด์…˜ ๋ฐฉ์‹ ์„ ํƒ ๊ณผ์ •

1. ๋ฐฐ๊ฒฝ

ํŽ˜์ด์ง€๋„ค์ด์…˜์ด๋ž€?

ํŽ˜์ด์ง€๋„ค์ด์…˜(Page Pagination)์€ ๋งŽ์€ ๋ฐ์ดํ„ฐ๋ฅผ ํ•œ ๋ฒˆ์— ์กฐํšŒํ•˜์ง€ ์•Š๊ณ , ์ผ์ • ๊ฐœ์ˆ˜์”ฉ ๋‚˜๋ˆ„์–ด ์กฐํšŒํ•˜๋Š” ๋ฐฉ์‹์ด๋‹ค. ๋ฐ์ดํ„ฐ ์–‘์ด ๋งŽ์•„์งˆ์ˆ˜๋ก ํ•œ ๋ฒˆ์— ๋ชจ๋“  ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ๊ฒƒ์€ ์„ฑ๋Šฅ ์ €ํ•˜๋กœ ์ด์–ด์งˆ ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์—, ์‚ฌ์šฉ์ž์—๊ฒŒ๋Š” ํ•„์š”ํ•œ ๋งŒํผ๋งŒ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณด์—ฌ์ฃผ๊ณ  ์„œ๋ฒ„ ๋ถ€ํ•˜๋ฅผ ์ค„์ด๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉ๋œ๋‹ค.

์˜ˆ์‹œ

 

๊ฒŒ์‹œ๋ฌผ, ๋ฉ”๋ชจ, ์ถ”์ฒœ ๋„์„œ ๋ชฉ๋ก์ด ์ „์ฒด ์กฐํšŒ ๋ฐฉ์‹์œผ๋กœ ๊ตฌํ˜„๋˜์–ด ์žˆ์—ˆ๋‹ค. ์ดˆ๊ธฐ ๋ฐ์ดํ„ฐ๊ฐ€ ์ ์„ ๋•Œ๋Š” ๋ฌธ์ œ๊ฐ€ ์—†์ง€๋งŒ, ๋ฐ์ดํ„ฐ๊ฐ€ ๊ณ„์† ์Œ“์ด๋ฉด ํ•œ ๋ฒˆ์— ๋งŽ์€ ๋ฐ์ดํ„ฐ๋ฅผ ์กฐํšŒํ•˜๊ฒŒ ๋˜์–ด ์‘๋‹ต ์†๋„์™€ ์„œ๋ฒ„ ๋ถ€ํ•˜ ์ธก๋ฉด์—์„œ ๋ฌธ์ œ๊ฐ€ ์ƒ๊ธธ ์ˆ˜ ์žˆ๋‹ค.

๊ทธ๋ž˜์„œ ๋ชฉ๋ก ์กฐํšŒ API์— ํŽ˜์ด์ง€๋„ค์ด์…˜์„ ๋„์ž…ํ•˜๊ธฐ๋กœ ํ–ˆ๊ณ , ๊ทธ ๊ณผ์ •์—์„œ ์˜คํ”„์…‹ ๊ธฐ๋ฐ˜ ํŽ˜์ด์ง€๋„ค์ด์…˜๊ณผ ์ปค์„œ ๊ธฐ๋ฐ˜ ํŽ˜์ด์ง€๋„ค์ด์…˜ ์ค‘ ์–ด๋–ค ๋ฐฉ์‹์„ ์‚ฌ์šฉํ• ์ง€ ๋น„๊ตํ–ˆ๋‹ค.

ํ•ต์‹ฌ ๊ธฐ์ค€์€ ํ˜„์žฌ ์„œ๋น„์Šค ๊ทœ๋ชจ, ๊ตฌํ˜„ ๋ณต์žก๋„, UI ๋ฐฉ์‹์ด์—ˆ๋‹ค. Booktine์€ ๋…์„œ ๊ธฐ๋ก ๋ชฉ๋ก์„ ํŽ˜์ด์ง€ ๋‹จ์œ„๋กœ ๋ณด์—ฌ์ฃผ๋Š” ๊ตฌ์กฐ๊ฐ€ ๋” ์ž์—ฐ์Šค๋Ÿฝ๋‹ค๊ณ  ํŒ๋‹จํ–ˆ๋‹ค.

2. ๋ฐฉ์‹ ๋น„๊ต

์˜คํ”„์…‹ ๊ธฐ๋ฐ˜ ํŽ˜์ด์ง€๋„ค์ด์…˜

์˜คํ”„์…‹ ๊ธฐ๋ฐ˜ ํŽ˜์ด์ง€๋„ค์ด์…˜์€ Spring Data์˜ Pageable์„ ํ™œ์šฉํ•˜๋Š” ๋ฐฉ์‹์ด๋‹ค. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ๋Š” LIMIT๊ณผ OFFSET์„ ์‚ฌ์šฉํ•ด ํŠน์ • ํŽ˜์ด์ง€์˜ ๋ฐ์ดํ„ฐ๋ฅผ ์กฐํšŒํ•œ๋‹ค.

PostRepository.java/PostService.java
Page<Post>๋ฅผ ๋ฐ˜ํ™˜ ํƒ€์ž…์œผ๋กœ ์ง€์ •ํ•˜๋ฉด Spring Data JPA๊ฐ€ ์ž๋™์œผ๋กœ count ์ฟผ๋ฆฌ๋ฅผ ํ•จ๊ป˜ ์‹คํ–‰ํ•ด ์ „์ฒด ํŽ˜์ด์ง€ ์ˆ˜๋ฅผ ๊ณ„์‚ฐํ•œ๋‹ค. Pageable์€ ์ปจํŠธ๋กค๋Ÿฌ์—์„œ ์ „๋‹ฌ๋ฐ›์€ page, size ๊ฐ’์„ ๋‹ด๋Š” ๊ฐ์ฒด๋‹ค.
Repository์—์„œ ๋ฐ˜ํ™˜๋œ Page<Post>๋ฅผ map()์œผ๋กœ DTO๋กœ ๋ณ€ํ™˜ํ•œ๋‹ค. ์—”ํ‹ฐํ‹ฐ๊ฐ€ ์™ธ๋ถ€๋กœ ๋…ธ์ถœ๋˜์ง€ ์•Š๋„๋ก PostResponse::from ์ •์  ํŒฉํ† ๋ฆฌ ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ–ˆ๋‹ค.

 

PostController.java
page์™€ size์— ๊ธฐ๋ณธ๊ฐ’์„ ์„ค์ •ํ•ด ํŒŒ๋ผ๋ฏธํ„ฐ ์—†์ด ์š”์ฒญํ•ด๋„ ๋™์ž‘ํ•˜๋„๋ก ํ–ˆ๋‹ค. page=0์€ ์ฒซ ๋ฒˆ์งธ ํŽ˜์ด์ง€, size=10์€ ํ•œ ํŽ˜์ด์ง€์— 10๊ฐœ๋ฅผ ์˜๋ฏธํ•œ๋‹ค.

 

์žฅ์  ๋‹จ์ 
Spring Data์—์„œ ๊ธฐ๋ณธ ์ง€์›ํ•˜๋ฏ€๋กœ ๊ตฌํ˜„์ด ๋‹จ์ˆœํ•˜๋‹ค. ํŽ˜์ด์ง€๊ฐ€ ๋’ค๋กœ ๊ฐˆ์ˆ˜๋ก OFFSET N๋งŒํผ ๊ฑด๋„ˆ๋›ฐ๋Š” ๋น„์šฉ์ด ์ปค์ง„๋‹ค.
ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ๋ฅผ ๊ธฐ์ค€์œผ๋กœ ์›ํ•˜๋Š” ํŽ˜์ด์ง€์— ๋ฐ”๋กœ ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋‹ค. ๋ฐ์ดํ„ฐ ์ถ”๊ฐ€ ๋˜๋Š” ์‚ญ์ œ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด ์ค‘๋ณต์ด๋‚˜ ๋ˆ„๋ฝ์ด ์ƒ๊ธธ ์ˆ˜ ์žˆ๋‹ค.
์ „์ฒด ํŽ˜์ด์ง€ ์ˆ˜์™€ ์ „์ฒด ๋ฐ์ดํ„ฐ ๊ฐœ์ˆ˜๋ฅผ ์ œ๊ณตํ•  ์ˆ˜ ์žˆ๋‹ค. ๋Œ€๊ทœ๋ชจ ๋ฐ์ดํ„ฐ์—์„œ๋Š” ์„ฑ๋Šฅ ์ €ํ•˜๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋‹ค.

 

์ปค์„œ ๊ธฐ๋ฐ˜ ํŽ˜์ด์ง€๋„ค์ด์…˜

์ปค์„œ ๊ธฐ๋ฐ˜ ํŽ˜์ด์ง€๋„ค์ด์…˜์€ ๋งˆ์ง€๋ง‰์œผ๋กœ ์กฐํšŒํ•œ ํ•ญ๋ชฉ์˜ ์‹๋ณ„์ž๋ฅผ ์ปค์„œ๋กœ ์‚ผ์•„, ๊ทธ ์ดํ›„ ๋˜๋Š” ์ด์ „ ๋ฐ์ดํ„ฐ๋ฅผ ์กฐํšŒํ•˜๋Š” ๋ฐฉ์‹์ด๋‹ค.

// Repository
List<Post> findByUserIdAndIdLessThanOrderByIdDesc(Long userId, Long cursor, Pageable pageable);

// Service
public List<PostResponse> getPosts(Long userId, Long cursor, int size) {
    Pageable pageable = PageRequest.of(0, size);
    return postRepository.findByUserIdAndIdLessThanOrderByIdDesc(userId, cursor, pageable)
            .stream().map(PostResponse::from).toList();
}โ€‹

์ปค์„œ ๊ธฐ๋ฐ˜ ํŽ˜์ด์ง€๋„ค์ด์…˜ ๊ตฌํ˜„ ์˜ˆ์‹œ

์žฅ์  ๋‹จ์ 
์ผ์ •ํ•œ ์„ฑ๋Šฅ ์œ ์ง€ ์ง์ ‘ ๊ตฌํ˜„ ํ•„์š”, ๋ณต์žก๋„ ๋†’์Œ
์ค‘๋ณต/๋ˆ„๋ฝ ์—†์Œ ์ „์ฒด ํŽ˜์ด์ง€ ์ˆ˜ ์ œ๊ณต ์–ด๋ ค์›€
๋ฌดํ•œ ์Šคํฌ๋กค์— ์ ํ•ฉ ์ž„์˜ ํŽ˜์ด์ง€ ์ ‘๊ทผ ๋ถˆ๊ฐ€

3. ๊ฒฐ์ •

Booktine์—์„œ๋Š” ์˜คํ”„์…‹ ๊ธฐ๋ฐ˜ ํŽ˜์ด์ง€๋„ค์ด์…˜์„ ์„ ํƒํ–ˆ๋‹ค.

ํ˜„์žฌ ์„œ๋น„์Šค ๊ทœ๋ชจ์—์„œ๋Š” ์˜คํ”„์…‹ ๊ธฐ๋ฐ˜์˜ ์„ฑ๋Šฅ ๋ฌธ์ œ๋ณด๋‹ค, Spring Data์˜ Pageable์„ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ๊ตฌํ˜„ ๋‹จ์ˆœ์„ฑ์ด ๋” ํฐ ์žฅ์ ์ด๋ผ๊ณ  ํŒ๋‹จํ–ˆ๋‹ค.

 

PageRequest ๊ธฐ๋ณธ๊ฐ’ ์„ค์ • ์ด์œ 

PageRequest.of(page, size)์˜ ๊ธฐ๋ณธ๊ฐ’์„ page=0, size=10์œผ๋กœ ์„ค์ •ํ–ˆ๋‹ค. ์ฒซ ํŽ˜์ด์ง€๋ถ€ํ„ฐ 10๊ฐœ์”ฉ ๋…ธ์ถœํ•˜๋Š” ๋ฐฉ์‹์ด ๋…์„œ ๊ธฐ๋ก ๋ชฉ๋ก UI์— ์ ํ•ฉํ•˜๋‹ค๊ณ  ํŒ๋‹จํ–ˆ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

 

Page<T> ์‘๋‹ต ๊ตฌ์กฐ

{
  "content": [
    {
      "id": 1,
      "title": "๋…์„œ ๊ธฐ๋ก ์ œ๋ชฉ",
      "content": "๋…์„œ ๋ฉ”๋ชจ ๋‚ด์šฉ"
    }
  ],
  "totalPages": 3,
  "totalElements": 25,
  "number": 0,
  "size": 10
}

4. Page vs Slice ์„ ํƒ ๊ณผ์ •

// Slice<T> — count ์ฟผ๋ฆฌ ์—†์Œ, ๋‹ค์Œ ํŽ˜์ด์ง€ ์กด์žฌ ์—ฌ๋ถ€๋งŒ ๋ฐ˜ํ™˜ (๋ฌดํ•œ ์Šคํฌ๋กค)
Slice<Post> findAllByUserId(Long userId, Pageable pageable);

// Page<T> — count ์ฟผ๋ฆฌ ๋ฐœ์ƒ, ์ „์ฒด ํŽ˜์ด์ง€ ์ˆ˜/ํ˜„์žฌ ํŽ˜์ด์ง€ ์ •๋ณด ๋ฐ˜ํ™˜ (ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ UI)
Page<Post> findAllByUserId(Long userId, Pageable pageable);

๋‘ ๋ฐฉ์‹์˜ ์ฐจ์ด๋Š” count ์ฟผ๋ฆฌ ์œ ๋ฌด๋‹ค. Slice<T>๋Š” ๋‹ค์Œ ํŽ˜์ด์ง€ ์กด์žฌ ์—ฌ๋ถ€(hasNext())๋งŒ ๋ฐ˜ํ™˜ํ•˜๊ณ , Page<T>๋Š” ์ „์ฒด ํŽ˜์ด์ง€ ์ˆ˜์™€ ์ „์ฒด ๋ฐ์ดํ„ฐ ๊ฐœ์ˆ˜๊นŒ์ง€ ๋ฐ˜ํ™˜ํ•œ๋‹ค.

Slice<T>๋Š” count ์ฟผ๋ฆฌ๊ฐ€ ์—†์–ด์„œ ์„ฑ๋Šฅ์ƒ ์œ ๋ฆฌํ•˜์ง€๋งŒ, ๋ฌดํ•œ ์Šคํฌ๋กค UI์— ์ ํ•ฉํ•œ ๋ฐฉ์‹์ด๋‹ค.

๋ฐ˜๋ฉด ์ด ํ”„๋กœ์ ํŠธ๋Š” ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ ๊ธฐ๋ฐ˜ UI๊ฐ€ ๋” ์ ํ•ฉํ•˜๋‹ค๊ณ  ํŒ๋‹จํ•ด ์ „์ฒด ํŽ˜์ด์ง€ ์ •๋ณด๋ฅผ ์ œ๊ณตํ•˜๋Š” Page<T>๋ฅผ ์„ ํƒํ–ˆ๋‹ค. 

 

728x90