[nextjs]SNS Server-51(myapp61+)[Pagination+API]ย 

๐Ÿ‘‰๐Ÿป ์—ฌ๊ธฐ์„œ๋Š” ํŽ˜์ด์ง€๋„ค์ด์…˜์„ ๊ตฌํ˜„ํ• ๋•Œ ํ˜ธ์ถœ๋˜๋Š” 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.”

Leave a Reply