ํฌ์ŠคํŠธ

๐Ÿ“š Frontend ๊ฐœ๋ฐœ์ž์˜ Pagination ์ƒ๊ฐ ํ™•์žฅํ•˜๊ธฐ

๐Ÿ“š Frontend ๊ฐœ๋ฐœ์ž์˜ Pagination ์ƒ๊ฐ ํ™•์žฅํ•˜๊ธฐ

์›น/์•ฑ ๊ฐœ๋ฐœ์—์„œ ํ•„์ˆ˜์ ์ธ ํŽ˜์ด์ง€๋„ค์ด์…˜(Pagination) ์€ ๋Œ€๋Ÿ‰์˜ ๋ฐ์ดํ„ฐ๋ฅผ ํšจ์œจ์ ์œผ๋กœ ๊ด€๋ฆฌํ•˜๊ธฐ ์œ„ํ•œ ํ•ต์‹ฌ ๊ธฐ์ˆ ์ž…๋‹ˆ๋‹ค. Frontend ๊ฐœ๋ฐœ์ž์ธ ์ €๋Š”, Offset ๋ฐฉ์‹๊ณผ Cursor ๋ฐฉ์‹์˜ ๋‚ด๋ถ€ ๋™์ž‘ ๋ฐฉ์‹ ์ฐจ์ด๋ฅผ ๊นŠ์ด ๊ณ ๋ฏผํ•˜๋ฉฐ ๊ฐœ๋ฐœํ•ด ๋ณธ ๊ฒฝํ—˜์€ ์—†์—ˆ์Šต๋‹ˆ๋‹ค. ์ตœ๊ทผ GraphQL๊ณผ Apollo Client ํ”„๋กœ์ ํŠธ์—์„œ Cursor ๋ฐฉ์‹ ํŽ˜์ด์ง€๋„ค์ด์…˜(Pagination) ์„ ์ฒ˜์Œ์œผ๋กœ ๋ณธ๊ฒฉ์ ์œผ๋กœ ์‚ฌ์šฉํ•˜๊ฒŒ ๋˜์—ˆ๊ณ , ์ด๋ฒˆ ๊ธ€์—์„œ๋Š” ๊ฐ ๋ฐฉ์‹์— ๋Œ€ํ•œ Frontend ๊ฐœ๋ฐœ์ž ์ž…์žฅ์—์„œ์˜ ๋‹ค์–‘ํ•œ ์Šคํƒ์—์„œ์˜ ์‚ฌ์šฉ๋ฒ•์„ ์ œ๊ณตํ•˜๊ธฐ ์œ„ํ•ด, Cursor ๋ฐฉ์‹ ์˜ˆ์‹œ๋Š” GraphQL + Apollo Client ํ™˜๊ฒฝ์—์„œ, Offset ๋ฐฉ์‹ ์˜ˆ์‹œ๋Š” ์ผ๋ฐ˜์ ์ธ Fetch API ๋˜๋Š” Axios๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ฝ”๋“œ ์˜ˆ์‹œ๋ฅผ ์ž‘์„ฑํ•ด ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด ๊ฐ ๊ธฐ์ˆ  ์Šคํƒ๋ณ„ ํŽ˜์ด์ง€๋„ค์ด์…˜ ๊ตฌํ˜„ ๋ฐฉ์‹๊ณผ ์ฐจ์ด์ ์„ ๋ณด์—ฌ๋“œ๋ฆฌ๊ณ ์ž ํ•ฉ๋‹ˆ๋‹ค.


1. ํŽ˜์ด์ง€๋„ค์ด์…˜์˜ ํ•„์š”์„ฑ๊ณผ ๊ธฐ๋ณธ ๊ฐœ๋…

๐Ÿ“Œ ์™œ ํŽ˜์ด์ง€๋„ค์ด์…˜์ด ํ•„์š”ํ• ๊นŒ์š”?

  • ์›น์‚ฌ์ดํŠธ ์„ฑ๋Šฅ ํ–ฅ์ƒ ๐Ÿš€: ๋Œ€๋Ÿ‰ ๋ฐ์ดํ„ฐ ๋กœ๋”ฉ์œผ๋กœ ์ธํ•œ ์›น์‚ฌ์ดํŠธ/์•ฑ ์„ฑ๋Šฅ ์ €ํ•˜ ๋ฐฉ์ง€ (์‚ฌ์šฉ์ž ์ดํƒˆ ๋ฐฉ์ง€! ๐Ÿƒโ€โ™€๏ธ๐Ÿ’จ), ํŽ˜์ด์ง€ ๋‹จ์œ„๋กœ ๋ถˆ๋Ÿฌ์˜ค๋ฉด ๋น ๋ฅด๊ฒŒ ์‘๋‹ตํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ์‚ฌ์šฉ์ž ๊ฒฝํ—˜ ๊ฐœ์„  ๐Ÿ˜Š: ์ •๋ณด ๊ณผ๋ถ€ํ•˜ ๋ฐฉ์ง€, ์‚ฌ์šฉ์ž๊ฐ€ ํ•œ๋ˆˆ์— ๋ณด๊ธฐ ์‰ฝ๊ฒŒ ๋ฐ์ดํ„ฐ๋ฅผ ๋‚˜๋ˆ„์–ด ์ œ๊ณตํ•˜์—ฌ ์ •๋ณด ํƒ์ƒ‰์ด ํŽธ๋ฆฌํ•ฉ๋‹ˆ๋‹ค. (UX ๋””์ž์ธ์˜ ๊ธฐ๋ณธ)
  • ์„œ๋ฒ„ ๋ถ€ํ•˜ ๊ฐ์†Œ(=๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํšจ์œจ) ๐Ÿ’พ: ํ•œ ๋ฒˆ์— ์ ์€ ๋ฐ์ดํ„ฐ๋งŒ ์ฒ˜๋ฆฌํ•˜๋ฏ€๋กœ ์„œ๋ฒ„์˜ ๋ถ€๋‹ด์ด ์ค„์–ด๋“ญ๋‹ˆ๋‹ค.(์ฟผ๋ฆฌ ์„ฑ๋Šฅ ํ–ฅ์ƒ, ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋ถ€ํ•˜ ๊ฐ์†Œ)

๐Ÿ“ ํŽ˜์ด์ง€๋„ค์ด์…˜ ๋ฐฉ์‹ ๊ฐœ์š”

ํŠน์ง•Offset-based PaginationCursor Pagination
Pagination ๋ฐฉ์‹๐Ÿ”ข Offset & Limit ๊ธฐ๋ฐ˜๐Ÿ“ Cursor ๊ธฐ๋ฐ˜
ํŽ˜์ด์ง€ ํƒ์ƒ‰โžก๏ธ ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ ์ง์ ‘ ์ด๋™ ์šฉ์ดโžก๏ธ ์ˆœ์ฐจ์  ํƒ์ƒ‰์— ์ตœ์ ํ™” (โ€œ๋‹ค์Œ/์ด์ „โ€)
๊ตฌํ˜„ ๋‚œ์ด๋„๐Ÿงฑ ๋น„๊ต์  ์‰ฌ์›€๐Ÿงฑ ์•ฝ๊ฐ„ ๋” ๋ณต์žก (์ดˆ๊ธฐ ์„ค์ • ๋ฐ cursor ๊ด€๋ฆฌ)
๋Œ€์šฉ๋Ÿ‰ ๋ฐ์ดํ„ฐ ์„ฑ๋Šฅ๐ŸŒ Offset ์ฆ๊ฐ€ ์‹œ ์„ฑ๋Šฅ ์ €ํ•˜ ๊ฐ€๋Šฅ์„ฑ ๋†’์Œ๐Ÿš€ ์„ฑ๋Šฅ ์šฐ์ˆ˜, ๋Œ€์šฉ๋Ÿ‰ ๋ฐ์ดํ„ฐ์— ๊ฐ•์ 
๋ฐ์ดํ„ฐ ์ผ๊ด€์„ฑโš ๏ธ ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ ์‹œ ๋ถˆ์ผ์น˜ ๊ฐ€๋Šฅ์„ฑ ์กด์žฌ๐Ÿ‘ ๋ฐ์ดํ„ฐ ์ผ๊ด€์„ฑ ๋†’์Œ (ํŠนํžˆ ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ)
์ด Item Countโœ… ์ด ์•„์ดํ…œ ์ˆ˜ ๊ณ„์‚ฐ ํ•„์š” (๋ณ„๋„ ์ฟผ๋ฆฌ)Optional (ํ•„์ˆ˜ ์•„๋‹˜, ์„ฑ๋Šฅ ์ตœ์ ํ™” ๊ฐ€๋Šฅ)
์ฃผ์š” ์‚ฌ์šฉ์ฒ˜๐Ÿข ์ผ๋ฐ˜์ ์ธ ๋ชฉ๋ก, ๊ด€๋ฆฌ์ž ํŽ˜์ด์ง€, ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๐Ÿ“ฑ ์†Œ์…œ ๋ฏธ๋””์–ด ํ”ผ๋“œ, ๋ฌดํ•œ ์Šคํฌ๋กค, ์‹ค์‹œ๊ฐ„ ์ŠคํŠธ๋ฆฌ๋ฐ
๊ฐœ๋ฐœ ํŒ- Indexing ์ตœ์ ํ™” (์„ฑ๋Šฅ ๊ฐœ์„ )- Cursor ์ปฌ๋Ÿผ ์‹ ์ค‘ํ•œ ์„ ํƒ (๊ณ ์œ ์„ฑ, ์•ˆ์ •์„ฑ)
ย - ์บ์‹ฑ ์ „๋žต ํ™œ์šฉ (DB ๋ถ€ํ•˜ ๊ฐ์†Œ)- Cursor ์•”ํ˜ธํ™”/๋ณด์•ˆ ๊ณ ๋ ค (๋ฏผ๊ฐ ์ •๋ณด ๋ณดํ˜ธ)

2. Offset Pagination (ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ ๋ฐฉ์‹) ์‹ฌํ™” ๋ถ„์„

2.1 ๊ธฐ๋ณธ ๊ฐœ๋… ๋ฐ ๋™์ž‘ ์›๋ฆฌ

Offset Pagination ์€ ์šฐ๋ฆฌ๊ฐ€ ํ”ํžˆ ์ ‘ํ•˜๋Š” โ€œํŽ˜์ด์ง€ ๋ฒˆํ˜ธโ€ ๋ฐฉ์‹์„ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
์˜ˆ๋ฅผ ๋“ค์–ด, ํ•œ ํŽ˜์ด์ง€์— 10๊ฐœ์˜ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณด์—ฌ์ค„ ๋•Œ, 3ํŽ˜์ด์ง€๋Š” OFFSET = (3 - 1) * 10 = 20 ์œผ๋กœ ๊ณ„์‚ฐํ•˜์—ฌ ๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ต๋‹ˆ๋‹ค.
ํ”„๋ก ํŠธ์—”๋“œ์—์„œ๋Š” ์‚ฌ์šฉ์ž๊ฐ€ โ€œ3ํŽ˜์ด์ง€โ€ ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜๋ฉด ํ•ด๋‹น ํŽ˜์ด์ง€์˜ ๋ฐ์ดํ„ฐ๋ฅผ ์š”์ฒญํ•˜๊ณ , ๋ฐฑ์—”๋“œ์—์„œ๋Š” SQL์˜ LIMIT๊ณผ OFFSET์„ ํ™œ์šฉํ•˜์—ฌ ๋ฐ์ดํ„ฐ๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.

2.2 ๋ฐฑ์—”๋“œ SQL ์˜ˆ์ œ

์˜ˆ์ œ 1: ๊ธฐ๋ณธ SQL ์ฟผ๋ฆฌ (๊ณ ์ • OFFSET)

1
2
3
4
5
-- products ํ…Œ์ด๋ธ”์—์„œ 3๋ฒˆ์งธ ํŽ˜์ด์ง€(ํ•œ ํŽ˜์ด์ง€์— 10๊ฐœ)๋ฅผ ์กฐํšŒํ•˜๋Š” ์˜ˆ์ œ
SELECT id, name, created_at
FROM products
ORDER BY id ASC
LIMIT 10 OFFSET 20;

์„ค๋ช…:

  • LIMIT 10: ํ•œ ํŽ˜์ด์ง€๋‹น 10๊ฐœ์˜ ๋ฐ์ดํ„ฐ๋ฅผ ์กฐํšŒ
  • OFFSET 20: (3 - 1) * 10 = 20 โ†’ 21๋ฒˆ์งธ ๋ฐ์ดํ„ฐ๋ถ€ํ„ฐ ๊ฐ€์ ธ์˜ด

์˜ˆ์ œ 2: ๋™์  OFFSET ๊ณ„์‚ฐ SQL ์ฟผ๋ฆฌ

-- ? placeholder ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋™์ ์œผ๋กœ OFFSET ๊ณ„์‚ฐ (MySQL Prepared Statement)
-- Parameterized Query: ? placeholder ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ SQL Injection ๊ณต๊ฒฉ์„ ๋ฐฉ์ง€
SELECT id, name, created_at
FROM products
ORDER BY id ASC
LIMIT ?   --  ํŽ˜์ด์ง€๋‹น ์•„์ดํ…œ ์ˆ˜
OFFSET ((? - 1) * ?);  -- ์‹œ์ž‘ Offset

์„ค๋ช…:
์‚ฌ์šฉ์ž๊ฐ€ ์š”์ฒญํ•œ ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ์™€ ํŽ˜์ด์ง€ ํฌ๊ธฐ์— ๋”ฐ๋ผ OFFSET์ด ์ž๋™ ๊ณ„์‚ฐ๋ฉ๋‹ˆ๋‹ค.

์˜ˆ์ œ 3: ์ „์ฒด ์•„์ดํ…œ ์ˆ˜ ์กฐํšŒ (ํŽ˜์ด์ง€ ์ˆ˜ ๊ณ„์‚ฐ์šฉ)

1
SELECT COUNT(*) FROM products;

์„ค๋ช…:
์ „์ฒด ๋ฐ์ดํ„ฐ ๊ฐœ์ˆ˜๋ฅผ ์กฐํšŒํ•˜์—ฌ, ์ด ํŽ˜์ด์ง€ ์ˆ˜ ๋“ฑ์„ ๊ณ„์‚ฐํ•  ๋•Œ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.

2.3 ํ”„๋ก ํŠธ์—”๋“œ JavaScript ์˜ˆ์ œ

์˜ˆ์ œ 4: fetch API๋ฅผ ์ด์šฉํ•œ ํŽ˜์ด์ง€๋ณ„ ๋ฐ์ดํ„ฐ ์š”์ฒญ

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const fetchProductsByPage = async (page, pageSize) => {
  try {
    const params = new URLSearchParams({
      limit: pageSize,
      offset: (page - 1) * pageSize,  // offset ๊ณ„์‚ฐ์„ ์—ฌ๊ธฐ์„œ ์ˆ˜ํ–‰
    });

    const response = await fetch(`/api/products?${params.toString()}`);

    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    
    const data = await response.json();
    
    console.log(`Products on page ${page}:`, data.products);
    console.log("Total product count:", data.totalCount);
    return data;
  } catch (error) {
    console.error("Error fetching products:", error);
    return { products: [], totalCount: 0 }; // ๋” ์ ์ ˆํ•œ ๋นˆ ๊ฐ์ฒด ๋ฐ˜ํ™˜.
  }
};

// ์˜ˆ์‹œ: 2ํŽ˜์ด์ง€, ํ•œ ํŽ˜์ด์ง€๋‹น 10๊ฐœ ์•„์ดํ…œ
const products = await fetchProductsByPage(2, 10);

์„ค๋ช…:

  • offset ๊ณ„์‚ฐ์œผ๋กœ ์›ํ•˜๋Š” ํŽ˜์ด์ง€์˜ ์‹œ์ž‘์ ์„ ๊ตฌํ•ฉ๋‹ˆ๋‹ค.
  • API ์š”์ฒญ ํ›„ ๋ฐ›์€ ๋ฐ์ดํ„ฐ๋ฅผ ํ™”๋ฉด์— ์ถœ๋ ฅํ•ฉ๋‹ˆ๋‹ค.

2.4 ์‹ค์ œ ๋ฐ์ดํ„ฐ ์˜ˆ์‹œ (Mock Data)

์˜ˆ์ œ 5: ๐Ÿ’พ Offset Pagination Mock Data & SQL ์ฟผ๋ฆฌ ์‹คํ–‰ ์˜ˆ์‹œ

Mock Data (products ํ…Œ์ด๋ธ”, MySQL):

-- products ํ…Œ์ด๋ธ” ์ƒ์„ฑ ๋ฐ Mock Data ์‚ฝ์ž… SQL (MySQL)
CREATE TABLE products (
      id INT AUTO_INCREMENT PRIMARY KEY,
      name VARCHAR(255) NOT NULL,
      price INTEGER NOT NULL,
      created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

INSERT INTO products (name, price) VALUES
    ('Product A', 10), ('Product B', 20), ('Product C', 15), ('Product D', 25), ('Product E', 30),
    ('Product F', 18), ('Product G', 22), ('Product H', 12), ('Product I', 28), ('Product J', 35),
    ('Product K', 17), ('Product L', 21), ('Product M', 13), ('Product N', 29), ('Product O', 32),
    ('Product P', 19), ('Product Q', 26), ('Product R', 14), ('Product S', 31), ('Product T', 23);

โ€“ โœ… Offset Pagination ์ฟผ๋ฆฌ (์‹คํ–‰ ์˜ˆ์‹œ)

SELECT id, name, price, created_at
FROM products
ORDER BY id ASC
LIMIT 5   -- ํŽ˜์ด์ง€๋‹น 5๊ฐœ
OFFSET 5  -- 2ํŽ˜์ด์ง€ ์‹œ์ž‘์  ( (2-1) * 5 = 5 )
;

| id | name | price | created_at | |โ€”โ€”|โ€”โ€”โ€”โ€”โ€“|โ€”โ€”-|โ€”โ€”โ€”โ€”โ€”โ€”โ€”-| | 6 | Product F | 18 | 2025-02-27 15:32:49 | | 7 | Product G | 22 | 2025-02-27 15:32:49 | | 8 | Product H | 12 | 2025-02-27 15:32:49 | | 9 | Product I | 28 | 2025-02-27 15:32:49 | | 10 | Product J | 35 | 2025-02-27 15:32:49 |

โ€“ โœ… ์ด ์•„์ดํ…œ ์ˆ˜ ์ฟผ๋ฆฌ (์‹คํ–‰ ์˜ˆ์‹œ) SELECT COUNT(*) FROM products;

COUNT(*)
20

2.5 ์žฅ๋‹จ์ 

๐Ÿ‘ ์žฅ์ 

  • ๐Ÿงฑ ๊ตฌํ˜„ ์šฉ์ด์„ฑ: SQL ์ฟผ๋ฆฌ ์ž‘์„ฑ์ด ๊ฐ„๋‹จํ•˜๊ณ , ํ”„๋ ˆ์ž„์›Œํฌ/ORM ์ง€์›๋„ ํ’๋ถ€ํ•˜์—ฌ ๊ฐœ๋ฐœ ์ƒ์‚ฐ์„ฑ ํ–ฅ์ƒ (๋น ๋ฅธ ๊ฐœ๋ฐœ, ์œ ์ง€๋ณด์ˆ˜ ์šฉ์ด)
  • ๐Ÿงญ ์ง๊ด€์ ์ธ ํŽ˜์ด์ง€ ํƒ์ƒ‰: ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ๋ฅผ ํ†ตํ•ด ํŠน์ • ํŽ˜์ด์ง€๋กœ ๋ฐ”๋กœ ์ด๋™ํ•˜๋Š” ๊ธฐ๋Šฅ ๊ตฌํ˜„์ด ์‰ฌ์›€ (์‚ฌ์šฉ์ž ํŽธ์˜์„ฑ, ๊ด€๋ฆฌ์ž ๊ธฐ๋Šฅ์— ์ ํ•ฉ)
  • ๐Ÿ› ๏ธ ๋””๋ฒ„๊น… ์šฉ์ด: ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ, OFFSET ๊ฐ’์„ ํ†ตํ•ด ํŠน์ • ํŽ˜์ด์ง€ ๋ฐ์ดํ„ฐ ํ™•์ธ ๋ฐ ๋ฌธ์ œ ๋ถ„์„ ์šฉ์ด (๊ฐœ๋ฐœ/ํ…Œ์ŠคํŠธ ํšจ์œจ ์ฆ๋Œ€)

๐Ÿ‘Ž ๋‹จ์ 

  • ๐ŸŒ ์„ฑ๋Šฅ ๋ฌธ์ œ (Deep Offset): OFFSET ๊ฐ’์ด ์ปค์งˆ์ˆ˜๋ก ์ฟผ๋ฆฌ ์„ฑ๋Šฅ์ด ๊ธ‰๊ฒฉํžˆ ์ €ํ•˜๋  ์ˆ˜ ์žˆ์Œ (Full Table Scan ๋ฐœ์ƒ ๊ฐ€๋Šฅ์„ฑ, ํŠนํžˆ ๋Œ€์šฉ๋Ÿ‰ ๋ฐ์ดํ„ฐ์—์„œ ์‹ฌ๊ฐ)
  • โš ๏ธ ๋ฐ์ดํ„ฐ ๋ถˆ์ผ์น˜ (Data Race): ํŽ˜์ด์ง€ ์ด๋™ ์ค‘ ๋ฐ์ดํ„ฐ ์‚ฝ์ž…/์‚ญ์ œ ๋ฐœ์ƒ ์‹œ, ํŽ˜์ด์ง€ ๋ฐ์ดํ„ฐ ์ค‘๋ณต ๋˜๋Š” ๋ˆ„๋ฝ ๋ฌธ์ œ ๋ฐœ์ƒ ๊ฐ€๋Šฅ์„ฑ (ํŠนํžˆ ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ์ด ์žฆ์€ ๊ฒฝ์šฐ)
  • ๐Ÿ“Š ์ด ์•„์ดํ…œ ์ˆ˜ ์ฟผ๋ฆฌ: ์ด ํŽ˜์ด์ง€ ์ˆ˜ ๊ณ„์‚ฐ์„ ์œ„ํ•ด ๋ณ„๋„์˜ COUNT(*) ์ฟผ๋ฆฌ ํ•„์š” (DB ๋ถ€ํ•˜ ์ฆ๊ฐ€, ์•ฝ๊ฐ„์˜ ์„ฑ๋Šฅ ์˜ค๋ฒ„ํ—ค๋“œ)

2.6 โš ๏ธ๋‹จ์ : Offset Pagination, ์™œ ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ์— ์ทจ์•ฝํ• ๊นŒ?

1. Offset Pagination ๊ธฐ๋ณธ ์ž‘๋™ ์›๋ฆฌ (์ฑ… ๋ชฉ๋ก ๋น„์œ )

Offset ๋ฐฉ์‹ ํŽ˜์ด์ง€๋„ค์ด์…˜์€ ๋งˆ์น˜ ๋„์„œ๊ด€์—์„œ โ€œ๋ชฉ๋ก์˜ ํŠน์ • ์ˆœ๋ฒˆ๋ถ€ํ„ฐ ๋ช‡ ๊ถŒ์˜ ์ฑ…์„ ๊ฐ€์ ธ์™€ ์ฃผ์„ธ์š”!โ€ ๋ผ๊ณ  ์š”์ฒญํ•˜๋Š” ๊ฒƒ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

  • ์š”์ฒญ ์˜ˆ์‹œ: โ€œ๋ชฉ๋ก์—์„œ 11๋ฒˆ์งธ๋ถ€ํ„ฐ 10๊ถŒ ๋ณด์—ฌ์ค˜โ€
  • ์‹œ์Šคํ…œ: OFFSET 10 (์ฒ˜์Œ 10๊ถŒ ๊ฑด๋„ˆ๋›ฐ๊ธฐ)๊ณผ LIMIT 10์„ ์ ์šฉํ•ด์„œ, ๋ชฉ๋ก ์ƒ 11๋ฒˆ๋ถ€ํ„ฐ 20๋ฒˆ ์ฑ…์„ ์„ ํƒ

์—ฌ๊ธฐ์„œ ํ•ต์‹ฌ์€ OFFSET์ด ๋ฐ์ดํ„ฐ ๋ชฉ๋ก์˜ ์ˆœ๋ฒˆ (์ ˆ๋Œ€์ ์ธ ์œ„์น˜) ์— ์˜์กดํ•œ๋‹ค๋Š” ์ ์ž…๋‹ˆ๋‹ค.

2. ๋ฐ์ดํ„ฐ ์‚ญ์ œ ์‹œ๋‚˜๋ฆฌ์˜ค: ์‚ฌ๋ผ์ง„ ์ฑ…, ๊ผฌ์—ฌ๋ฒ„๋ฆฐ ํŽ˜์ด์ง€ ๐Ÿ˜ต

์—ฌ๋Ÿฌ๋ถ„์ด 2ํŽ˜์ด์ง€ (11๋ฒˆ๋ถ€ํ„ฐ 20๋ฒˆ ์ฑ…)๋ฅผ ์‹ ๋‚˜๊ฒŒ ์ฝ๊ณ  ์žˆ๋Š”๋ฐ, ๊ฐ‘์ž๊ธฐ ๋„์„œ๊ด€ 1ํŽ˜์ด์ง€ (1๋ฒˆ๋ถ€ํ„ฐ 10๋ฒˆ ์ฑ…) ์—์„œ ์ฑ… 3๊ถŒ์ด ์‚ฌ๋ผ์ง„ ๊ฒ๋‹ˆ๋‹ค (๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ๋ฐ์ดํ„ฐ ์‚ญ์ œ ๋ฐœ์ƒ)

๐Ÿ“š ์ฑ…์žฅ ์ƒํ™ฉ ๋ณ€ํ™”:

  • ์‚ญ์ œ ์ „: 1๋ฒˆ, 2๋ฒˆ, 3๋ฒˆ, 4๋ฒˆ, 5๋ฒˆ, 6๋ฒˆ, 7๋ฒˆ, 8๋ฒˆ, 9๋ฒˆ, 10๋ฒˆ, 11๋ฒˆ, 12๋ฒˆ, 13๋ฒˆ, 14๋ฒˆ, 15๋ฒˆ, 16๋ฒˆ, 17๋ฒˆ, 18๋ฒˆ, 19๋ฒˆ, 20๋ฒˆ, โ€ฆ
  • ์‚ญ์ œ ํ›„: 1๋ฒˆ, 2๋ฒˆ, 4๋ฒˆ, 6๋ฒˆ, 8๋ฒˆ, 9๋ฒˆ, 10๋ฒˆ, ๊ธฐ์กด 11๋ฒˆ (์ด์ œ 8๋ฒˆ!), ๊ธฐ์กด 12๋ฒˆ (์ด์ œ 9๋ฒˆ!), ๊ธฐ์กด 13๋ฒˆ (์ด์ œ 10๋ฒˆ!), ๊ธฐ์กด 14๋ฒˆ (์ด์ œ 11๋ฒˆ!), โ€ฆ , ๊ธฐ์กด 20๋ฒˆ (์ด์ œ 17๋ฒˆ!), โ€ฆ

โš ๏ธ ๋ฌธ์ œ ๋ฐœ์ƒ:

  • ์›๋ž˜ 2ํŽ˜์ด์ง€์˜ 11๋ฒˆ, 12๋ฒˆ, 13๋ฒˆ ์ฑ…๋“ค์ด ์•ž์ชฝ ์ฑ…์ด ์‚ญ์ œ๋˜๋ฉด์„œ ์ˆœ๋ฒˆ์ด ๊ผฌ์—ฌ๋ฒ„๋ฆฝ๋‹ˆ๋‹ค.
  • ๋‹ค์Œ์— ๊ฐ™์€ OFFSET 10, LIMIT 10 ์œผ๋กœ 2ํŽ˜์ด์ง€๋ฅผ ๋‹ค์‹œ ์š”์ฒญํ•˜๋ฉด, ์‹œ์Šคํ…œ์€ ์ด์ œ ์ƒˆ๋กญ๊ฒŒ 11๋ฒˆ๋ถ€ํ„ฐ 20๋ฒˆ ์ฑ…์„ ๊ฐ€์ ธ์˜ค๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.
  • ํ•˜์ง€๋งŒ ์ด โ€œ์ƒˆ๋กœ์šด 2ํŽ˜์ด์ง€โ€ ๋Š” ์‚ญ์ œ ์ „ 2ํŽ˜์ด์ง€์˜ ๋’ท๋ถ€๋ถ„ ๋ฐ์ดํ„ฐ (์›๋ž˜ 11๋ฒˆ ์ดํ›„ ์ฑ…๋“ค) ์™€ ์ค‘๋ณต๋˜๊ฑฐ๋‚˜, ์•„์˜ˆ ์—‰๋šฑํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณด์—ฌ์ฃผ๊ฒŒ ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

3. โž• ๋ฐ์ดํ„ฐ ์ถ”๊ฐ€ ์‹œ๋‚˜๋ฆฌ์˜ค: ๋Š˜์–ด๋‚œ ์ฑ…, ๋ฐ€๋ ค๋ฒ„๋ฆฐ ํŽ˜์ด์ง€ ๐Ÿคฏ

์ด๋ฒˆ์—๋Š” ๋ฐ˜๋Œ€๋กœ 2ํŽ˜์ด์ง€๋ฅผ ๋ณด๊ณ  ์žˆ๋Š”๋ฐ, 1ํŽ˜์ด์ง€ ์•ž์— ์ƒˆ ์ฑ… 3๊ถŒ์ด โ€œ๋ฟ…!โ€ ํ•˜๊ณ  ๋‚˜ํƒ€๋‚ฌ์–ด์š” (๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ๋ฐ์ดํ„ฐ ์ถ”๊ฐ€ ๋ฐœ์ƒ)

๐Ÿ“š ์ฑ…์žฅ ์ƒํ™ฉ ๋ณ€ํ™”:

  • ์ถ”๊ฐ€ ์ „: 1๋ฒˆ, 2๋ฒˆ, 3๋ฒˆ, 4๋ฒˆ, 5๋ฒˆ, 6๋ฒˆ, 7๋ฒˆ, 8๋ฒˆ, 9๋ฒˆ, 10๋ฒˆ, 11๋ฒˆ, 12๋ฒˆ, 13๋ฒˆ, 14๋ฒˆ, 15๋ฒˆ, 16๋ฒˆ, 17๋ฒˆ, 18๋ฒˆ, 19๋ฒˆ, 20๋ฒˆ, โ€ฆ
  • ์ถ”๊ฐ€ ํ›„: ์ƒˆ๋กœ์šด ์ฑ… A, ์ƒˆ๋กœ์šด ์ฑ… B, ์ƒˆ๋กœ์šด ์ฑ… C, 1๋ฒˆ, 2๋ฒˆ, 3๋ฒˆ, 4๋ฒˆ, 5๋ฒˆ, 6๋ฒˆ, 7๋ฒˆ, 8๋ฒˆ, 9๋ฒˆ, 10๋ฒˆ, ๊ธฐ์กด 11๋ฒˆ (์ด์ œ 14๋ฒˆ!), ๊ธฐ์กด 12๋ฒˆ (์ด์ œ 15๋ฒˆ!), ๊ธฐ์กด 13๋ฒˆ (์ด์ œ 16๋ฒˆ!), ๊ธฐ์กด 14๋ฒˆ (์ด์ œ 17๋ฒˆ!), โ€ฆ , ๊ธฐ์กด 20๋ฒˆ (์ด์ œ 23๋ฒˆ!), โ€ฆ

โš ๏ธ ๋ฌธ์ œ ๋ฐœ์ƒ:

  • ์ด๋ฒˆ์—๋Š” 2ํŽ˜์ด์ง€์˜ 11๋ฒˆ, 12๋ฒˆ, 13๋ฒˆ ์ฑ…๋“ค์ด ์•ž์ชฝ์— ์ฑ…์ด ์ถ”๊ฐ€๋˜๋ฉด์„œ ์ˆœ๋ฒˆ์ด ๋’ค๋กœ ๋ฐ€๋ ค๋ฒ„๋ฆฝ๋‹ˆ๋‹ค.
  • ๋‹ค์‹œ ๊ฐ™์€ OFFSET 10, LIMIT 10 ์œผ๋กœ 2ํŽ˜์ด์ง€๋ฅผ ์š”์ฒญํ•˜๋ฉด, ์‹œ์Šคํ…œ์€ ์—ฌ์ „ํžˆ ์ƒˆ๋กญ๊ฒŒ 11๋ฒˆ๋ถ€ํ„ฐ 20๋ฒˆ ์ฑ…์„ ๊ฐ€์ ธ์˜ค๋ ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค.
  • ํ•˜์ง€๋งŒ ์ด โ€œ์ƒˆ๋กœ์šด 2ํŽ˜์ด์ง€โ€๋Š” ์ถ”๊ฐ€ ์ „ 2ํŽ˜์ด์ง€์˜ ์ผ๋ถ€ ๋ฐ์ดํ„ฐ (์›๋ž˜ 11๋ฒˆ, 12๋ฒˆ, 13๋ฒˆ ์ฑ…) ๋ฅผ ๋‹ค์‹œ ๋ณด์—ฌ์ฃผ๊ฑฐ๋‚˜, ํŽ˜์ด์ง€๊ฐ€ ๋ฐ€๋ฆฌ๋ฉด์„œ ๋ฐ์ดํ„ฐ๊ฐ€ ์ค‘๋ณต๋˜์–ด ๋ณด์ด๋Š” ํ˜„์ƒ์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

4. ๐Ÿ”‘ ํ•ต์‹ฌ ์š”์•ฝ: ์ ˆ๋Œ€ ์œ„์น˜ ์˜์กด์˜ ํ•จ์ •

Offset ๋ฐฉ์‹ ํŽ˜์ด์ง€๋„ค์ด์…˜์˜ ๋ฌธ์ œ์ ์€ ๊ฒฐ๊ตญ OFFSET ์ด๋ผ๋Š” โ€˜์ ˆ๋Œ€์ ์ธ ์œ„์น˜โ€™ ์— ์ง€๋‚˜์น˜๊ฒŒ ์˜์กดํ•œ๋‹ค๋Š” ๋ฐ ์žˆ์Šต๋‹ˆ๋‹ค.

  • ์ ˆ๋Œ€ ์œ„์น˜ ์˜์กด: Offset ๋ฐฉ์‹์€ ๋ฐ์ดํ„ฐ ๋ชฉ๋ก์—์„œ ์ •ํ•ด์ง„ ์ˆœ๋ฒˆ์„ ๊ธฐ์ค€์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค.
  • ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ์— ๋ฏผ๊ฐ: ์ฑ…(๋ฐ์ดํ„ฐ)์ด ์ถ”๊ฐ€๋˜๊ฑฐ๋‚˜ ์‚ญ์ œ๋˜๋ฉด ์ˆœ๋ฒˆ์ด ๋ฐ”๋€Œ๋ฉด์„œ, ์ด์ „์— ๊ณ„์‚ฐ๋œ OFFSET ๊ฐ’์ด ๋” ์ด์ƒ ์ •ํ™•ํ•œ ์œ„์น˜๋ฅผ ๊ฐ€๋ฆฌํ‚ค์ง€ ์•Š๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.
  • ๊ฒฐ๊ณผ ๋ฌธ์ œ: ์ด๋กœ ์ธํ•ด ์‚ฌ์šฉ์ž๊ฐ€ ๊ธฐ๋Œ€ํ•œ ๋ฐ์ดํ„ฐ ๊ตฌ๊ฐ„๊ณผ ์‹ค์ œ ๋ฐ˜ํ™˜๋˜๋Š” ๋ฐ์ดํ„ฐ ๊ตฌ๊ฐ„ ์‚ฌ์ด์— ์ค‘๋ณต ๋˜๋Š” ๋ˆ„๋ฝ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒƒ์ด์ฃ .

3. Cursor Pagination (cursor ๋ฐฉ์‹)

3.1 ๊ธฐ๋ณธ ๊ฐœ๋… ๋ฐ ๋™์ž‘ ์›๋ฆฌ

Cursor Pagination ์€ โ€œ์ฑ…๊ฐˆํ”ผโ€(CreepHyp - ๆ ž ๋…ธ๋ž˜ ์ข‹์•„์š”)์ฒ˜๋Ÿผ, ๋งˆ์ง€๋ง‰์œผ๋กœ ์กฐํšŒํ•œ ํ•ญ๋ชฉ์˜ ๊ณ ์œ  ๊ฐ’(์˜ˆ: id๋‚˜ created_at)์„ ๊ธฐ์ค€์œผ๋กœ ๋‹ค์Œ ํŽ˜์ด์ง€ ๋ฐ์ดํ„ฐ๋ฅผ ์š”์ฒญํ•˜๋Š” ๋ฐฉ์‹์ž…๋‹ˆ๋‹ค.
๋งˆ์น˜ ์ฑ…๊ฐˆํ”ผ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ฝ๋˜ ํŽ˜์ด์ง€๋ฅผ ๊ธฐ์–ตํ•˜๊ณ , ๋‹ค์Œ ๋‚ด์šฉ์„ ์ด์–ด์„œ ์ฝ๋Š” ๊ฒƒ๊ณผ ์œ ์‚ฌํ•ฉ๋‹ˆ๋‹ค.
ํŠนํžˆ ๋Œ€์šฉ๋Ÿ‰ ๋ฐ์ดํ„ฐ์…‹ ๋˜๋Š” ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ ์ŠคํŠธ๋ฆฌ๋ฐ ํ™˜๊ฒฝ์—์„œ ๋›ฐ์–ด๋‚œ ์„ฑ๋Šฅ๊ณผ ๋ฐ์ดํ„ฐ ์ผ๊ด€์„ฑ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ์ด ๋ฐฉ์‹์€ ๋ฐ์ดํ„ฐ๊ฐ€ ์ถ”๊ฐ€๋˜๊ฑฐ๋‚˜ ์‚ญ์ œ๋˜์–ด๋„ ์ผ๊ด€์„ฑ์„ ์œ ์ง€ํ•  ์ˆ˜ ์žˆ์–ด ๋Œ€์šฉ๋Ÿ‰ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ์— ๋งค์šฐ ์œ ๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

3.2 ๋ฐฑ์—”๋“œ SQL ์˜ˆ์ œ

์˜ˆ์ œ 6: ๊ธฐ๋ณธ SQL ์ฟผ๋ฆฌ (cursor ์—†์ด ์ฒซ ํŽ˜์ด์ง€)

-- ์ฒซ ํŽ˜์ด์ง€๋Š” cursor ์—†์ด LIMIT๋งŒ ์‚ฌ์šฉ
SELECT id, name, created_at
FROM products
ORDER BY id ASC
LIMIT ?;

์„ค๋ช…:
์ฒซ ํŽ˜์ด์ง€ ์š”์ฒญ ์‹œ cursor๊ฐ€ ์—†์œผ๋ฏ€๋กœ ๋‹จ์ˆœํžˆ LIMIT๋งŒ ์ ์šฉํ•ฉ๋‹ˆ๋‹ค.

์˜ˆ์ œ 7: SQL ์ฟผ๋ฆฌ (cursor ์‚ฌ์šฉํ•˜์—ฌ ๋‹ค์Œ ํŽ˜์ด์ง€ ์กฐํšŒ)

-- ๋งˆ์ง€๋ง‰์œผ๋กœ ์กฐํšŒํ•œ id (์˜ˆ: 5)๋ฅผ cursor๋กœ ์‚ฌ์šฉํ•˜์—ฌ ๋‹ค์Œ 5๊ฐœ ๋ฐ์ดํ„ฐ๋ฅผ ์กฐํšŒ
SELECT id, name, created_at
FROM products
WHERE id > ?
ORDER BY id ASC
LIMIT ?;

์„ค๋ช…:
:last_id ๊ฐ’ ์ดํ›„์˜ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์™€, ์ˆœ์ฐจ์ ์œผ๋กœ ํŽ˜์ด์ง€๋ฅผ ์ด๋™ํ•ฉ๋‹ˆ๋‹ค.

3.3 ํ”„๋ก ํŠธ์—”๋“œ JavaScript ์˜ˆ์ œ

์˜ˆ์ œ 8: Graphql + Apollo๋ฅผ ์ด์šฉํ•œ Cursor ๊ธฐ๋ฐ˜ ๋ฐ์ดํ„ฐ ์š”์ฒญ

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
import { useLazyQuery } from '@apollo/client';


// GraphQL Query ์ •์˜: cursor ๊ธฐ๋ฐ˜ ์ƒํ’ˆ ๋ชฉ๋ก ์กฐํšŒ
const GET_PRODUCTS_CURSOR = gql`
  query GetProducts($limit: Int!, $cursor: String) {
    products(limit: $limit, after: $cursor) {
      edges {
        node {
          id
          name
          price
          createdAt
          __typename // Apollo Client ์บ์‹ฑ์— ์ค‘์š”
        }
        cursor
      }
      pageInfo {
        hasNextPage // ๋‹ค์Œ ํŽ˜์ด์ง€ ์กด์žฌ ์—ฌ๋ถ€
        endCursor   // ๋งˆ์ง€๋ง‰ ์•„์ดํ…œ cursor
      }
    }
  }
`;

let nextCursor = null; // ๋‹ค์Œ ํŽ˜์ด์ง€ cursor๋ฅผ ์ €์žฅ (์ดˆ๊ธฐ๊ฐ’ null)
                       
// cursor ๊ธฐ๋ฐ˜ ํŽ˜์ด์ง€๋„ค์ด์…˜ ์ปค์Šคํ…€ ํ›…
const useProductsCursorPagination = (pageSize) => {

  const [fetchProducts, { data, loading, error }] = useLazyQuery(GET_PRODUCTS_CURSOR); // useLazyQuery ํ›… ์‚ฌ์šฉ: ์ˆ˜๋™ ํŠธ๋ฆฌ๊ฑฐ

  const fetchedProductsData = data?.products?.edges?.map(({ node }) => node) || []; // ์ƒํ’ˆ ๋ฐ์ดํ„ฐ ์ถ”์ถœ
  const hasNextPage = data?.products?.pageInfo?.hasNextPage || false; // ๋‹ค์Œ ํŽ˜์ด์ง€ ์กด์žฌ ์—ฌ๋ถ€ ์ถ”์ถœ
  nextCursor = data?.products?.pageInfo?.endCursor || null; // ๋‹ค์Œ cursor ์—…๋ฐ์ดํŠธ: pageInfo์˜ endCursor ์‚ฌ์šฉ

  // ์ดˆ๊ธฐ ์ƒํ’ˆ ๋กœ๋“œ ํ•จ์ˆ˜ (์ฒซ ํŽ˜์ด์ง€): cursor ์—†์ด
  const loadInitialProducts = async () => {
    try {
      await fetchProducts({ variables: { limit: pageSize, cursor: null } }); // cursor ์—†์ด ์ฒซ ํŽ˜์ด์ง€ ์š”์ฒญ
    } catch (fetchError) {
      console.error("์ดˆ๊ธฐ ์ƒํ’ˆ ๋กœ๋“œ ์˜ค๋ฅ˜:", fetchError);
    }
  };

  // ๋‹ค์Œ ํŽ˜์ด์ง€ ๋กœ๋“œ ํ•จ์ˆ˜
  const loadNextPage = async () => {
    if (hasNextPage && nextCursor) { // ๋‹ค์Œ ํŽ˜์ด์ง€๊ฐ€ ์žˆ๊ณ  cursor๊ฐ€ ์žˆ์„ ๋•Œ๋งŒ ์š”์ฒญ
      try {
        await fetchProducts({ variables: { limit: pageSize, cursor: nextCursor } }); // cursor ์‚ฌ์šฉํ•˜์—ฌ ๋‹ค์Œ ํŽ˜์ด์ง€ ์š”์ฒญ
      } catch (fetchError) {
        console.error("๋‹ค์Œ ํŽ˜์ด์ง€ ์ƒํ’ˆ ๋กœ๋“œ ์˜ค๋ฅ˜:", fetchError);
      }
    } else {
      console.log("๋” ์ด์ƒ ํŽ˜์ด์ง€ ์—†์Œ ๋˜๋Š” ๋‹ค์Œ cursor ์—†์Œ.");
      // UI ์ฒ˜๋ฆฌ: "๋‹ค์Œ ํŽ˜์ด์ง€" ๋ฒ„ํŠผ ๋น„ํ™œ์„ฑํ™” ๋“ฑ
    }
  };

  // ํ›… ๋ฐ˜ํ™˜๊ฐ’: ๋กœ๋”ฉ, ์—๋Ÿฌ, ์ƒํ’ˆ, ํŽ˜์ด์ง€ ์ •๋ณด, ๋กœ๋“œ ํ•จ์ˆ˜
  return {
    loading,
    error,
    products: fetchedProductsData,
    hasNextPage,
    loadInitialProducts,
    loadNextPage
  };
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46

import React, { useEffect } from 'react';

const ProductListWithCursorPagination = ({ pageSize = 10 }) => { // pageSize prop์œผ๋กœ ์„ค์ • ๊ฐ€๋Šฅ
  const {
    loading,
    error,
    products,
    hasNextPage,
    loadInitialProducts,
    loadNextPage
  } = useProductsCursorPagination(pageSize); 

  useEffect(() => { 
    loadInitialProducts();
  }, []);

  if (loading) return <p>Loading products...</p>
  if (error) return <p>Error : {error.message}</p>

  return (
    <div>
      <h2>Product List (Cursor Pagination with GraphQL & Apollo)</h2>
      <ul>
        {products.map(product => (
          <li key={product.id}>
            {product.name} - Price: ${product.price} - Created At: {product.createdAt}
          </li>
        ))}
      </ul>

      {hasNextPage && ( // ๋‹ค์Œ ํŽ˜์ด์ง€ ๋ฒ„ํŠผ ๋ Œ๋”๋ง: hasNextPage๊ฐ€ true์ผ ๋•Œ๋งŒ
        <button onClick={loadNextPage} disabled={loading}>
          {loading ? 'Loading next page...' : 'Load Next Page'}
        </button>
      )}

      {!hasNextPage && products.length > 0 && <p>No more products.</p>} {/* ๋งˆ์ง€๋ง‰ ํŽ˜์ด์ง€ ํ‘œ์‹œ */}
      {products.length === 0 && !loading && !error && <p>No products available.</p>} {/* ์ƒํ’ˆ ์—†์„ ๋•Œ ํ‘œ์‹œ */}
    </div>
  );
};

export default ProductListWithCursorPagination;

์„ค๋ช…:

  • cursor๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ URL์— ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ์ถ”๊ฐ€ํ•˜์—ฌ API ์š”์ฒญ์„ ๋ณด๋ƒ…๋‹ˆ๋‹ค.
  • ์„œ๋ฒ„ ์‘๋‹ต์—์„œ nextCursor ๊ฐ’์„ ๋ฐ›์•„ ๋‹ค์Œ ํŽ˜์ด์ง€ ์š”์ฒญ์— ํ™œ์šฉํ•ฉ๋‹ˆ๋‹ค.

3.4 ์žฅ๋‹จ์ 

๐Ÿ‘ ์žฅ์ 

  • ๐Ÿš€ ์••๋„์ ์ธ ์„ฑ๋Šฅ (๋Œ€์šฉ๋Ÿ‰ ๋ฐ์ดํ„ฐ): Offset ๊ธฐ๋ฐ˜ Pagination ๊ณผ ๋‹ฌ๋ฆฌ, ๋ฐ์ดํ„ฐ์…‹ ํฌ๊ธฐ์— ๊ด€๊ณ„์—†์ด ์ผ์ •ํ•œ ์ฟผ๋ฆฌ ์„ฑ๋Šฅ์„ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค. ํŠนํžˆ ๋Œ€์šฉ๋Ÿ‰ ๋ฐ์ดํ„ฐ, ๋ฌดํ•œ ์Šคํฌ๋กค์— ์ตœ์ ํ™” (์„œ๋น„์Šค ํ™•์žฅ์„ฑ ํ™•๋ณด)
  • ๐Ÿ‘ ๋ฐ์ดํ„ฐ ์ผ๊ด€์„ฑ: ํŽ˜์ด์ง€ ์ด๋™ ์ค‘ ๋ฐ์ดํ„ฐ ์‚ฝ์ž…/์‚ญ์ œ๊ฐ€ ๋ฐœ์ƒํ•ด๋„ ๋ฐ์ดํ„ฐ ์ค‘๋ณต/๋ˆ„๋ฝ ๋ฌธ์ œ ๋ฐœ์ƒ ๊ฐ€๋Šฅ์„ฑ์ด ๋งค์šฐ ๋‚ฎ์Šต๋‹ˆ๋‹ค. (์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ ์ŠคํŠธ๋ฆฌ๋ฐ, ์ฑ„ํŒ… ๋“ฑ์— ์ ํ•ฉ)
  • โœจ ํ™•์žฅ์„ฑ: ์„œ๋ฒ„๋Š” cursor๋งŒ ๊ด€๋ฆฌํ•˜๋ฉด ๋˜๋ฏ€๋กœ, ํด๋ผ์ด์–ธํŠธ ์ƒํƒœ (ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ ๋“ฑ) ๋ฅผ ๊ด€๋ฆฌํ•  ํ•„์š”๊ฐ€ ์—†์–ด ์„œ๋ฒ„ ํ™•์žฅ์„ฑ์ด ๋›ฐ์–ด๋‚ฉ๋‹ˆ๋‹ค. (MSA ํ™˜๊ฒฝ์— ์ ํ•ฉ)

๐Ÿ‘Ž ๋‹จ์ 

  • ๐Ÿงฑ ๊ตฌํ˜„ ๋ณต์žก๋„ ์ฆ๊ฐ€: Cursor ์ƒ์„ฑ, ๊ด€๋ฆฌ, ํด๋ผ์ด์–ธํŠธ ์ „๋‹ฌ ๋“ฑ ๊ตฌํ˜„ ๋ณต์žก๋„๊ฐ€ Offset-based Pagination ๋ณด๋‹ค ์ƒ๋Œ€์ ์œผ๋กœ ๋†’์Šต๋‹ˆ๋‹ค. (์ดˆ๊ธฐ ๊ฐœ๋ฐœ ๋ฐ ์œ ์ง€๋ณด์ˆ˜ ๋‚œ์ด๋„ ์ฆ๊ฐ€)
  • ๐Ÿงญ ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ ํƒ์ƒ‰ ์ œํ•œ: ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ ๊ธฐ๋ฐ˜ ์ง์ ‘ ์ด๋™์ด ๋ถˆ๊ฐ€๋Šฅํ•˜๊ฑฐ๋‚˜ ๊ตฌํ˜„์ด ๋งค์šฐ ์–ด๋ ต์Šต๋‹ˆ๋‹ค. ์ˆœ์ฐจ์ ์ธ โ€œ๋‹ค์Œ ํŽ˜์ด์ง€โ€ ํƒ์ƒ‰๋งŒ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. (์ผ๋ฐ˜์ ์ธ ์›น ํŽ˜์ด์ง€ Pagination UI ์™€๋Š” ๋‹ค๋ฆ„)
  • ๐Ÿ”‘ cursor ๊ด€๋ฆฌ ๋ฐ ๋ณด์•ˆ: cursor ์œ ํšจ ๊ธฐ๊ฐ„ ๊ด€๋ฆฌ, cursor ์ •๋ณด ๋…ธ์ถœ ๋ฐฉ์ง€ ๋“ฑ cursor ๊ด€๋ฆฌ ๋ฐ ๋ณด์•ˆ ์ด์Šˆ๋ฅผ ๊ณ ๋ คํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. (๋ณด์•ˆ ๋ฐ ์•ˆ์ •์„ฑ ์ค‘์š”)

3.5 โš ๏ธ๋‹จ์ : Cursor Pagination, ์™œ ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ ํƒ์ƒ‰์ด ์–ด๋ ค์šธ๊นŒ?

Cursor ๊ฐ’ ์ž์ฒด์—๋Š” ์ „์ฒด ๋ฐ์ดํ„ฐ์—์„œ ๋ช‡ ๋ฒˆ์งธ ํŽ˜์ด์ง€์ธ์ง€, ์ด ํŽ˜์ด์ง€ ์ˆ˜๊ฐ€ ๋ช‡ ํŽ˜์ด์ง€์ธ์ง€์™€ ๊ฐ™์€ ์ •๋ณด๊ฐ€ ๋‹ด๊ฒจ์žˆ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. Cursor๋Š” ๋‹จ์ˆœํžˆ โ€œ๋‹ค์Œ ๋ฐ์ดํ„ฐ ๋ฌถ์Œ์„ ๊ฐ€์ ธ์˜ค๊ธฐ ์œ„ํ•œ ํ‹ฐ์ผ“โ€ ๊ฐ™์€ ์—ญํ• ์„ ํ•  ๋ฟ์ด์—์š”. ํŠน์ • ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ๋กœ ์ง์ ‘ ์ด๋™ํ•˜๋Š” ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•˜๋ ค๋ฉด, ์„œ๋ฒ„์—์„œ ์ „์ฒด ๋ฐ์ดํ„ฐ์…‹์„ ์Šค์บ”ํ•˜์—ฌ ํ•ด๋‹น ํŽ˜์ด์ง€์— ํ•ด๋‹นํ•˜๋Š” Cursor ๊ฐ’์„ ๋ณ„๋„๋กœ ๊ณ„์‚ฐํ•ด์•ผ ํ•˜๋Š” ๋ณต์žกํ•œ ๊ณผ์ •์ด ํ•„์š”ํ•˜๋ฉฐ, ์ด๋Š” Cursor Pagination์˜ ์žฅ์ ์ธ ์„ฑ๋Šฅ ํšจ์œจ์„ฑ์„ ํฌ๊ฒŒ ๋–จ์–ด๋œจ๋ฆฝ๋‹ˆ๋‹ค


4. Offset Pagination vs. Cursor Pagination: ์–ธ์ œ ์–ด๋–ค ๋ฐฉ์‹์„ ์„ ํƒํ• ๊นŒ?

๐Ÿค” ์–ด๋–ค Pagination ๋ฐฉ์‹์„ ์„ ํƒํ•ด์•ผ ํ• ๊นŒ์š”?

Pagination ๋ฐฉ์‹ ์„ ํƒ์— ์ •๋‹ต์€ ์—†์Šต๋‹ˆ๋‹ค. ์‹ค์ œ๋กœ UI/UX, ๊ธฐ์กด ์‹œ์Šคํ…œ, ํ”„๋กœ์ ํŠธ ์ œ์•ฝ ๋“ฑ ๋‹ค์–‘ํ•œ ์ด์œ ๋กœ ์—ฌ๋Ÿฌ๋ถ„์ด ์ž์œ ๋กญ๊ฒŒ ์„ ํƒํ•˜๊ธฐ ์–ด๋ ค์šธ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋ฐ์ดํ„ฐ ๊ทœ๋ชจ์™€ ๋ณ€๊ฒฝ ๋นˆ๋„, ์š”๊ตฌ ์‚ฌํ•ญ, ๊ฐœ๋ฐœ ๋ฆฌ์†Œ์Šค, ์‚ฌ์šฉ์ž ๊ฒฝํ—˜ ๋“ฑ์„ ์ข…ํ•ฉ์ ์œผ๋กœ ๊ณ ๋ คํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๊ฒฐ๊ตญ, ์—ฌ๋Ÿฌ ์ œ์•ฝ ์กฐ๊ฑด์„ ๊ณ ๋ คํ•˜์—ฌ ์ƒํ™ฉ์— ๋งž๋Š” ์ตœ์ ์˜ ๋ฐฉ์‹์„ ์„ ํƒํ•˜๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค.


๐ŸŽ‰ ๋งˆ๋ฌด๋ฆฌ

Offset Pagination๊ณผ Cursor Pagination์˜ ๊ฐœ๋…๋ถ€ํ„ฐ ์žฅ๋‹จ์ , ๊ตฌ์ฒด์ ์ธ SQL ๋ฐ JavaScript ์˜ˆ์ œ, ๊ทธ๋ฆฌ๊ณ  ์‹ค์ œ Mock Data๊นŒ์ง€ ๋ชจ๋‘ ์‚ดํŽด๋ณด์•˜์Šต๋‹ˆ๋‹ค.
ํ”„๋ก ํŠธ์—”๋“œ์™€ ๋ฐฑ์—”๋“œ ๊ฐœ๋ฐœ์ž ๋ชจ๋‘์—๊ฒŒ ์œ ์šฉํ•œ ์ •๋ณด๊ฐ€ ๋˜์—ˆ๊ธธ ๋ฐ”๋ผ๋ฉฐ, ์งˆ๋ฌธ์ด๋‚˜ ์ถ”๊ฐ€์ ์ธ ์˜๊ฒฌ์ด ์žˆ๋‹ค๋ฉด ๋Œ“๊ธ€๋กœ ๋‚จ๊ฒจ์ฃผ์‹œ๊ณ , ๋„์›€์ด ๋˜์…จ๋‹ค๋ฉด ๊ณต์œ  ๋ถ€ํƒ๋“œ๋ฆฝ๋‹ˆ๋‹ค. ๐Ÿ˜Š

์ด ๊ธฐ์‚ฌ๋Š” ์ €์ž‘๊ถŒ์ž์˜ CC BY 4.0 ๋ผ์ด์„ผ์Šค๋ฅผ ๋”ฐ๋ฆ…๋‹ˆ๋‹ค.