๐ Frontend ๊ฐ๋ฐ์์ Pagination ์๊ฐ ํ์ฅํ๊ธฐ
์น/์ฑ ๊ฐ๋ฐ์์ ํ์์ ์ธ ํ์ด์ง๋ค์ด์ (Pagination) ์ ๋๋์ ๋ฐ์ดํฐ๋ฅผ ํจ์จ์ ์ผ๋ก ๊ด๋ฆฌํ๊ธฐ ์ํ ํต์ฌ ๊ธฐ์ ์ ๋๋ค. Frontend ๊ฐ๋ฐ์์ธ ์ ๋, Offset ๋ฐฉ์๊ณผ Cursor ๋ฐฉ์์ ๋ด๋ถ ๋์ ๋ฐฉ์ ์ฐจ์ด๋ฅผ ๊น์ด ๊ณ ๋ฏผํ๋ฉฐ ๊ฐ๋ฐํด ๋ณธ ๊ฒฝํ์ ์์์ต๋๋ค. ์ต๊ทผ GraphQL๊ณผ Apollo Client ํ๋ก์ ํธ์์ Cursor ๋ฐฉ์ ํ์ด์ง๋ค์ด์ (Pagination) ์ ์ฒ์์ผ๋ก ๋ณธ๊ฒฉ์ ์ผ๋ก ์ฌ์ฉํ๊ฒ ๋์๊ณ , ์ด๋ฒ ๊ธ์์๋ ๊ฐ ๋ฐฉ์์ ๋ํ Frontend ๊ฐ๋ฐ์ ์ ์ฅ์์์ ๋ค์ํ ์คํ์์์ ์ฌ์ฉ๋ฒ์ ์ ๊ณตํ๊ธฐ ์ํด, Cursor ๋ฐฉ์ ์์๋ GraphQL + Apollo Client ํ๊ฒฝ์์, Offset ๋ฐฉ์ ์์๋ ์ผ๋ฐ์ ์ธ Fetch API ๋๋ Axios๋ฅผ ์ฌ์ฉํ์ฌ ์ฝ๋ ์์๋ฅผ ์์ฑํด ๋ณด๊ฒ ์ต๋๋ค. ์ด๋ฅผ ํตํด ๊ฐ ๊ธฐ์ ์คํ๋ณ ํ์ด์ง๋ค์ด์ ๊ตฌํ ๋ฐฉ์๊ณผ ์ฐจ์ด์ ์ ๋ณด์ฌ๋๋ฆฌ๊ณ ์ ํฉ๋๋ค.
1. ํ์ด์ง๋ค์ด์ ์ ํ์์ฑ๊ณผ ๊ธฐ๋ณธ ๊ฐ๋
๐ ์ ํ์ด์ง๋ค์ด์ ์ด ํ์ํ ๊น์?
- ์น์ฌ์ดํธ ์ฑ๋ฅ ํฅ์ ๐: ๋๋ ๋ฐ์ดํฐ ๋ก๋ฉ์ผ๋ก ์ธํ ์น์ฌ์ดํธ/์ฑ ์ฑ๋ฅ ์ ํ ๋ฐฉ์ง (์ฌ์ฉ์ ์ดํ ๋ฐฉ์ง! ๐โโ๏ธ๐จ), ํ์ด์ง ๋จ์๋ก ๋ถ๋ฌ์ค๋ฉด ๋น ๋ฅด๊ฒ ์๋ตํ ์ ์์ต๋๋ค.
- ์ฌ์ฉ์ ๊ฒฝํ ๊ฐ์ ๐: ์ ๋ณด ๊ณผ๋ถํ ๋ฐฉ์ง, ์ฌ์ฉ์๊ฐ ํ๋์ ๋ณด๊ธฐ ์ฝ๊ฒ ๋ฐ์ดํฐ๋ฅผ ๋๋์ด ์ ๊ณตํ์ฌ ์ ๋ณด ํ์์ด ํธ๋ฆฌํฉ๋๋ค. (UX ๋์์ธ์ ๊ธฐ๋ณธ)
- ์๋ฒ ๋ถํ ๊ฐ์(=๋ฐ์ดํฐ๋ฒ ์ด์ค ํจ์จ) ๐พ: ํ ๋ฒ์ ์ ์ ๋ฐ์ดํฐ๋ง ์ฒ๋ฆฌํ๋ฏ๋ก ์๋ฒ์ ๋ถ๋ด์ด ์ค์ด๋ญ๋๋ค.(์ฟผ๋ฆฌ ์ฑ๋ฅ ํฅ์, ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ถํ ๊ฐ์)
๐ ํ์ด์ง๋ค์ด์ ๋ฐฉ์ ๊ฐ์
| ํน์ง | Offset-based Pagination | Cursor 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๊น์ง ๋ชจ๋ ์ดํด๋ณด์์ต๋๋ค.
ํ๋ก ํธ์๋์ ๋ฐฑ์๋ ๊ฐ๋ฐ์ ๋ชจ๋์๊ฒ ์ ์ฉํ ์ ๋ณด๊ฐ ๋์๊ธธ ๋ฐ๋ผ๋ฉฐ, ์ง๋ฌธ์ด๋ ์ถ๊ฐ์ ์ธ ์๊ฒฌ์ด ์๋ค๋ฉด ๋๊ธ๋ก ๋จ๊ฒจ์ฃผ์๊ณ , ๋์์ด ๋์
จ๋ค๋ฉด ๊ณต์ ๋ถํ๋๋ฆฝ๋๋ค. ๐