๐๐ป ์ฌ๊ธฐ์๋ ํ์ด์ง๋ค์ด์
์ ๊ตฌํํ ๋ ํธ์ถ๋๋ API์ ํจ๊ป ํ์ด์ง๋ค์ด์
๊ตฌ์กฐ๋ฅผ ์ดํด ๋ด
๋๋ค.
Here, we examine the pagination structure along with the API called when implementing pagination.
๐๐ป ์ด์ ํฌ์คํธ์์ ClientList.tsx์์ TimelineList.tsx๋ก ์ด๊ธฐ๋ฐ์ดํฐ๋ฅผ ์ ๋ฌํ๋ฉด ์ด ๋ฐ์ดํฐ๊ฐ useCursonPaginationํ
์ ๋ค์ด๊ฐ๋ค๊ฐ ๋์ต๋๋ค.
As mentioned in the previous post, when initial data is passed from ClientList.tsx to TimelineList.tsx, it is processed by the useCursorPagination hook.
๐๐ป TimlineList.tsx๋ฅผ ํตํด ํ
๋ด๋ถ์ ์ ๋ฌํ ์ฝ๋ฐฑํจ์์ธ fetcher๊ฐ ํ
๋ด๋ถ์์ ์คํ๋๊ณ API์ ํต์ ํ๊ณ ๋ฐ์ดํฐ๋ฅผ ๋ฐ์์์ ๊ธฐ์กด ๋ฐ์ดํฐ ๋ค์ ๋ถ์ฌ์ค๋๋ค.
The fetcher callback function, passed into the hook via TimelineList.tsx, executes within the hook, communicates with the API, retrieves data, and appends it to the existing data.
๐ ์ฝ๋ฐฑํจ์ fetcher์ API ํต์ / API communication using the fetcher callback function
โ๏ธ app/api/v1/timelines/[type]/route.ts
โ๏ธ API์ ํต์ ํ ๋ ์ฌ์ฉ๋๋ URL ํ์ / URL format used when communicating with an API
GET /api/v1/timelines/home?limit=20&max_id=59d38066-6cc2-4c2b-b06e-710f7293fda6
GET /api/v1/timelines/home?limit=20&max_id=41b5b137-d56c-4e36-a995-3dbed5ef061c
GET /api/v1/timelines/home?limit=20&max_id=a864ab3b-a090-4575-a7b1-0bf5c075f73b
โ๏ธ TimelineList.tsx์ fetcher์ฝ๋ฐฑํจ์๋ ์๋์ ๊ฐ์ด API์ ํต์ ์ ํฉ๋๋ค.
The fetcher callback function in TimelineList.tsx communicates with the API as follows.
if (tab === 'home') {
if (effectiveUser) {
url = `/api/v1/timelines/home?${params.toString()}`;
} else {
params.set('local', 'true');
url = `/api/v1/timelines/public?${params.toString()}`;
}
} else if (tab === 'local') {
params.set('local', 'true');
url = `/api/v1/timelines/public?${params.toString()}`;
} else {
url = `/api/v1/timelines/public?${params.toString()}`;
}
1)์ด params.toString()์ด ‘limit=20&max_id=a864ab3b-a…’ ์ด ๋ถ๋ถ์
๋๋ค.
This params.toString() corresponds to the part that looks like 'limit=20&max_id=a864ab3b-a...'.
โ๏ธ API๋ด๋ถ์์ ๋ค์๊ณผ ๊ฐ์ ๊ณผ์ ์ผ๋ก ์ ๋ฌ ๋ฐ์ ํ๋ผ๋ฉํฐ๋ฅผ ์ฒ๋ฆฌํฉ๋๋ค.
The API processes the received parameters internally through the following steps.
1)๋ฐ์ดํฐ ๊ฒ์ / Data Retrieval
let filteredRows: any[] = [];
if (type === 'home') {
// โ
myapp61+ - LIMIT 200 ์ถ๊ฐ / Add LIMIT 200
const myPosts = db.prepare(`SELECT * FROM posts WHERE username =? LIMIT 200`).all(viewer) as any[];
const inboxPosts = db.prepare(`SELECT * FROM inbox_posts WHERE username =? LIMIT 200`).all(viewer) as any[];as any[];
... ...
} else if (type === 'public') {
... ...
} else { ... ...}
A. ๊ฐ ํญ์ ๋ฐ๋ผ์ ํ์ํ ๋ฐ์ดํฐ๋ฅผ ๊ฒ์ํฉ๋๋ค
Search for the necessary data based on each tab.
B. API์์ ํญ์ URL์์ … timelines/public.. ,…timeline/home…์์ public,home ๋ถ๋ถ์
๋๋ค.
In the API, the “tabs” refer to the public and home segments found in URL paths like .../timelines/public... and .../timeline/home....
2)max_id,limit
if (max_id) {
// ํ์ฌ๋ ํ์์คํฌํ๊ฐ ์๋ UUID๋ค์ด๊ฐ๋ ๊ตฌ์กฐ๋ผ maxTime์ ํญ์ 0.
// Currently, since the structure uses a UUID instead of a timestamp, maxTime is always 0.
let maxTime = parseDate(max_id);
// max_id๊ฐ UUID์ธ ๊ฒฝ์ฐ created_at๊ฒ์ / Search created_at when max_id is a UUID
if (maxTime === 0) {
const refRow = filteredRows.find((r: any) => String(r.id) === String(max_id));
if (refRow) {
// maxTime์ ๋ ์ง ๊ฐ ์ ์ฅ ์ด์ 0์ด ์๋ / Storing the date value in maxTime; it is no longer zero.
maxTime = parseDate(refRow.created_at);
} else {
// filteredRows์ ์์ผ๋ฉด DB์์ ์ง์ ์กฐํ / If not found in filteredRows, query the database directly.
try {
const refDb = db.prepare(`SELECT created_at FROM posts WHERE id=?`).get(max_id) as any;
if (refDb) maxTime = parseDate(refDb.created_at);
else {
const refInbox = db.prepare(`SELECT created_at FROM inbox_posts WHERE id=?`).get(max_id) as any;
if (refInbox) maxTime = parseDate(refInbox.created_at);
}
} catch {}
}
}
// ์ด์ ๊ธ ๊ฒ์๋ก์ง / Previous Post Search Logic
// ์ ๋ฌ๋ฐ์ max_id์ ์ผ์นํ๋ ๊ธ๋ณด๋ค ์ด์ ๊ธ์ ๋ฐฐ์ด์์ ๊ฒ์ / Search the array for posts preceding the post matching the provided max_id.
if (maxTime > 0) {
filteredRows = filteredRows.filter((r) => parseDate(r.created_at) < maxTime);
}
}
// ํ๋ผ๋ฉํฐ๋ก ์ ๋ฌ๋ฐ์ limit=20๋ก ๋ฐ์ดํฐ 20๊ฐ๋ง ์ ํ
// Select only 20 items using the limit=20 parameter
const hasMore = filteredRows.length > limit;
const sliced = filteredRows.slice(0, limit);
const statuses = sliced.map((row: any) => { ... ...
return { ... ...
});
}
A.max_id์ ๊ฐ์ ๊ธ์ created_at(๊ธ ์์ฑํ ๋ ์)๋ฅผ ๊ฒ์ํด์ ์ด created_at๋ณด๋ค ์ด์ ๋ฐ์ดํฐ๋ง ์ถ์ถํฉ๋๋ค.
Retrieve the created_at (post creation date) associated with the max_id, and extract only the data from before that timestamp.
B. ํ์ด์ง๋ค์ด์
์ด ์ ์ฉ๋๊ธฐ ์ ์ ์ฒซ ๊ฒ์๋ฌผ 20๊ฐ๋ order by desc๋ก ๊ฐ์ ธ์ค๊ธฐ ๋๋ฌธ์ ์ต์ ๋ฐ์ดํฐ๋ถํฐ ์ด์ ๋ฐ์ดํฐ 20๊ฐ๋ฅผ ๊ฐ์ ธ์ต๋๋ค.
Before pagination is applied, the first 20 posts are retrieved using ORDER BY DESC, so the 20 most recent items are fetched.
C.์ฌ๊ธฐ์ ๋ง์ง๋ง์ ๋ณธ ๊ธ์ ์์ด๋๊ฐ max_id๊ฐ ๋๊ณ ์ด max_id์ ๊ฐ์ ๊ธ์ idํ๋์ ์๋ crated_at(๊ธ์ด ์์ฑ๋ ๋ ์ง)์ด์ ์ ๋ฐ์ดํฐ 20๊ฐ๋ฅผ ๊ฐ์ ธ์์ ๋ฆฌํดํฉ๋๋ค.
Here, the ID of the last post viewed becomes the max_id; the system then retrieves and returns 20 records that precede the created_at timestamp associated with that max_id.
๐ ๋ฐ์ดํฐ ์ฒ๋ฆฌ๊ณผ์ ์์ฝ / Summary of the Data Processing Workflow
โ๏ธ API๋ถ๋ถ์ ๋ฐ์ดํฐ ์ฒ๋ฆฌ ๊ณผ์ ์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
The data processing flow for the API component is as follows.
1)์ต์ ๋ฐ์ดํฐ ๊ฒ์
Search for the latest data
2)max_id๊ฐ ์์ผ๋ฉด max_id๊ฐ์ id๊ฐ ์๋ ํ๋์ created_at์ ๊ฒ์
If max_id exists, search for the created_at of fields with an ID matching max_id.
3)limit์๋(20๊ฐ) ๋งํผ๋ง ๋ฐ์ดํฐ๋ฅผ ์๋ผ์ ๋ฆฌํด
Slice the data up to the specified limit (20 items) and return it.
๐๋ถ์คํธ ์นด์ดํฐ(์ฝ๋์์ ) / Boost Counter (Code Modification)
const allAnnounces = viewer ? (db.prepare(`SELECT * FROM announces WHERE username =?`).all(viewer) as any[]) : [];
... ...
const announcesByObject = new Map<string, number>();
for (const a of allAnnounces) {
/*
announces ๊ฐฏ์ ์ธ๊ธฐ
- [fullurl,'/' ๊ธฐ์ค์ผ๋ก ์ ์ฒด ๋ถ๋ฆฌํ ๋ง์ง๋ง ์์๋ง ์ ํ] => [https://fllurl/adc.abc]
- allAnnounces๊ฐ 10๊ฐ๋ฉด for๊ฐ 20๋ฒ ๋๊ฒ ๋์ด ์์ k๊ฐ ๋ฌธ์์ด์ด๋ผ a.object1ํ,a.object.split๊ฐ 2ํ ์ด๋ ๊ฒ for๊ฐ ์คํ๋จ.
*/
for (const k of [a.object, a.object.split('/').pop() || '']) { if (k) announcesByObject.set(k, (announcesByObject.get(k) || 0) + 1); }
}
A.๊ธฐ๋ณธ์ ์ผ๋ก ์ ์ฒด ๋ถ์คํธ(allAnnounces)์์ announcedByObject Map์ fullURL๊ณผ ‘/’๊ธฐ์ค์ผ๋ก ๋ถ๋ฆฌ๋ ๋ง์ง๋งurl๊ฐ(split(‘/’).pop())์ ๋ชจ๋ ์ ์ฅ
Basically, for all boosts (allAnnounces), store both the fullURL and the last URL segment (obtained via split('/').pop()) in the announcedByObject map.
B. cont k of [a.object, a.object.split(‘/’).pop()]์ k์ ๋ฐฐ์ด ๊ฐ์ด ๋ฌธ์์ด๋ก ๊ฐ๊ฐ ๋ฐ๋ก ํ ๋น๋์ด ๋ฐ์ดํฐ๊ฐ 10๊ฐ๋ฉด for๊ฐ 20๋ฒ ์คํ๋๊ฒ ๋์ด ์์.
In cont k of [a.object, a.object.split('/').pop()], the array values โโare assigned to k individually as strings; consequently, if there are 10 data items, the for loop executes 20 times.
C.announcesByObject.set(k, (announcesByObject.get(k) || 0) + 1); } ์ฌ๊ธฐ์ URL๊ณผ ์ซ์๋ฅผ Map์ ์
๋ ฅํจ
announcesByObject.set(k, (announcesByObject.get(k) || 0) + 1); } Here, the URL and the number are stored in the Map.
# ์ ์ฒด ๋ฐ์ดํฐ๊ฐ ์๋์ ๊ฐ๋ค๋ฉด... / If the entire dataset is as follows...
allAnnounces = [
{object: "https://server.com/posts/abc123"},
{object: "https://server.com/posts/abc123"},
{object: "abc123"}
]
# ์ฒซ๋ฐ์ดํฐ ์์ / Example of initial data
1. a.object
= "https://myserver.com/posts/abc123" (์ ์ฒด URL / Full URL)
2. a.object.split('/').pop()
= "https://server.com/posts/abc123".split('/')
= ["https:", "", "server.com", "posts", "abc123"]
.pop() = ๋ง์ง๋ง ์์/Last element, ID only = "abc123" (id๋ง/ID only)
3.๊ทธ๋ฌ๋ฉด ์๋์ ๊ฐ์ด ๋ฐฐ์ด์ ์ ์ฅ๋จ / Then, it is stored in an array as shown below.
[ a.object, a.object.split('/').pop() || '' ]
= [ "https://myserver.com/posts/abc123", "abc123" ]
4.for ๋ฌธ์ด ์คํ๋๋ฉด์ const k๋ ๋ฌธ์์ด์ด๊ธฐ ๋๋ฌธ์ k = "https://myserver.com/posts/abc123" ์ด๋ ๊ฒ ํ๋ฒ์คํํ๊ณ ๋๋ฒ์งธ๋ก k = "abc123" ์ด๋ ๊ฒ ํ ๋น๋์ด ์คํ๋จ
As the `for` loop executes, since `k` is a string, it first runs with `k = "https://myserver.com/posts/abc123"`, and then, in the second iteration, it runs with `k = "abc123"` assigned to it.
5.announcesByObject.set(k, (announcesByObject.get(k) || 0) + 1);
- ์ฌ๊ธฐ์ announcesByObject๋งต์ k("https://myserver.com/posts/abc123") ๊ฐ์ด Map์ ์
ํ
๋จ
Here, the key "https://myserver.com/posts/abc123" is set in the `announcesByObject` map.
- Map์ ๋๋ฒ์งธ ์ธ์๊ฐ ์ซ์์ด๊ณ ์ง๊ธ ์๋ฌด๊ฒ๋ ์ ์ฅํ์ง ์์์ผ๋ announcesByObject.get(k) ์ด๊ฑด ๊ธฐ๋ณธ๊ฐ์ด 0์ด๋จ
Since the second argument of the Map is a number and nothing has been stored yet, `announcesByObject.get(k)` defaults to 0.
- 0์ + 1ํ๋ฉด ๊ฐ์ด 1 ๊ทธ๋์ ๋งต์๋ ["https://myserver.com/posts/abc123",1] ์ด๋ ๊ฒ ์ ์ฅ๋จ.
Adding 1 to 0 results in a value of 1, so it is stored in the map like this: `["https://myserver.com/posts/abc123", 1]`.
- ๊ทธ๋ฆฌ๊ณ ๋๋ฒ์งธ ์์ "abc123"๋ ["abc123",1] ์ด๋ ๊ฒ ์ ์ฅ๋จ
And the second element, "abc123", is also stored as `["abc123", 1]`.
# ๋๋ฒ์งธ ๋ฐ์ดํฐ ์์ / Second data example
1.์ฒซ๋ฐ์ดํฐ ์์์ 1,2๋ฒ์ ๊ณผ์ ์ด ๋์ผํ๊ฒ ์คํ๋จ.
Steps 1 and 2 from the first data example are executed in the same way.
2.announcesByObject.set(k, (announcesByObject.get(k) || 0) + 1);
- ์ฌ๊ธฐ์ ๋งต์ ๊ธฐ์กด์ ์ ์ฅํ๋ ๋์ผํ ํค๊ฐ "https://myserver.com/posts/abc123"์ด๊ฑฐ๊ณ ๊ธฐ์กด๊ฐ์ 1
Here, the identical key previously saved in the map is "https://myserver.com/posts/abc123", and the existing value is 1.
- ๊ทธ๋์ ๊ธฐ์กดํค์ ๋๋ฒ์งธ ์ธ์๋ 2๊ฐ ๋๋ค.
Therefore, the second argument of the existing key becomes 2.
- ["https://myserver.com/posts/abc123",2] , ["abc123",2]
# ์ธ๋ฒ์งธ ๋ฐ์ดํฐ ์์ / Third data example
1.์ธ๋ฒ์งธ ๋ฐ์ดํฐ๋ "abc123"์ด ๊ฐ๋ง ์๊ธฐ๋๋ฌธ์ ์ค๋ณต ์ ๊ฑฐํ๋ฉด ๊ธฐ์กด์ "abc123"์ ํค์ +1์ด ๋ฉ๋๋ค.
Since the third data item consists only of the value "abc123," removing duplicates results in the count for the existing "abc123" key increasing by 1.
2.๊ทธ๋ฌ๋ฉด ["https://myserver.com/posts/abc123",2] ,["abc123",3] ์ด๋ ๊ฒ ๋ฉ๋๋ค.
Then it becomes `["https://myserver.com/posts/abc123", 2], ["abc123", 3]`.
3.์ง๊ธ ํธ์ถ๋ฐฉ์์ด ์ ์ฒด URL๋ก ํธ์ถํ๋ฏ๋ก ["https://myserver.com/posts/abc123",2] ์ด๊ฑธ ๊ธฐ์ค์ผ๋ก ์ฌ์ฉํ๋ฉด๋ฉ๋๋ค.
Since the current invocation method uses the full URL, you can use `["https://myserver.com/posts/abc123", 2]` as the basis.
# ๊ฒฐ๊ตญ์ ์ ์ฒด url๋ก ํธ์ถํ๋์ง ์งง์ id๋ก ํธ์ถํ๋์ง ๋๋ค ๋์ํ๊ธฐ ์ํ ๋ก์ง์ด๋ฉฐ ์ฌ์ฉํ ๋ ๋์ค ํ๋๋ง ์ฌ์ฉํ ๊ฒ.
# Ultimately, the logic is designed to handle calls made using either the full URL or the short ID; however, you should use only one of the two methods.
๋งํ๋ณต์ 21์ฅ 22์ / Matthew 21: 22
“๋ํฌ๊ฐ ๊ธฐ๋ํ ๋์ ๋ฌด์์ด๋ ์ง ๋ฏฟ๊ณ ๊ตฌํ๋ ๊ฒ์ ๋ค ๋ฐ์ผ๋ฆฌ๋ผ ํ์๋๋ผ”
“And everything you ask in prayer, believing, you will receive.”