๐๐ป ์ฌ๊ธฐ์๋ ์กํฐ๋ธํ
๋ง์ ํ์๋ผ์ธ์ ์ปค์ ํ์ด์ง ๋ค์ด์
๊ตฌ์กฐ๋ฅผ ๋ถ์ํด ๋ด
๋๋ค.
Here, we analyze the cursor-based pagination structure of Active Themes and Timelines.
๐๐ป ์ปค์ํ์ด์ง๋ค์ด์
์(ํ์ด์ง ๋ค์ด์
+ ๋ฌดํ์คํฌ๋กค)์
๋๋ค.
Cursor-based pagination is a combination of pagination and infinite scrolling.
๐๐ป ๋ณดํต ์ฐ๋ฆฌ๊ฐ ๋ชจ๋ฐ์ผ ํฐ์์ ์คํฌ๋กค์ ์๋๋ก ๋ด๋ ค์ ํ๋ฉด๋์ ๋๋ฌํ๋ฉด ๋ฐ์ดํฐ๋ฅผ ์ถ๊ฐ๋ก ๋ค์ ๋ถ์ด๋ ๋ฐฉ์์ ์๋ฏธํฉ๋๋ค.
It typically refers to the method where additional data is appended as you scroll down on a mobile phone and reach the bottom of the screen.
๐๐ป ์ด ๋ถ๋ถ์ ์ด SNSํ์ด์ง์์ ๊ตฌํํ๋๊ฒ ์ปค์ ํ์ด์ง ๋ค์ด์
์
๋๋ค.
The key feature being implemented on this social media page is pagination.
๐ ํ
๋งํธ์ถ ๋ฐ ํ์๋ผ์ธ ๋ฆฌ์คํธํธ์ถ ํ๋ฆ
Flow of Theme and Timeline List Retrieval
โ๏ธ ํ์ด์ง๋ค์ด์
์ด ์ ์ฉ๋๋ ๋ถ๋ถ์ ํ
๋ง๋๋ ํ ๋ฆฌ์ TimelineList.tsx์
๋๋ค.
The section where pagination is applied is TimelineList.tsx in the theme directory.
— app/usersui/[username]/_components/themes/themeex/TimelineList.tsx
โ๏ธ ์ถํ ๋๋จธ์ง ๋ถ๋ถ๋ ํ์ด์ง๋ค์ด์
์ ์ฉ์ ํ ์์ ์
๋๋ค.
We plan to implement pagination for the remaining sections as well.
โ๏ธ ํ์ด์ง๋ค์ด์
์ ๊ฐ๋
์ ์ดํดํ๊ธฐ ์ํด์ ๋ผ์ฐํธ ์คํ ํ ์ค์ ๋ก ๋ฐ์ดํฐ๊ฐ ์ด๋ป๊ฒ ํ๋ฌ๊ฐ๋์ง ์ดํด ๋ณผ ํ์๊ฐ ์์ต๋๋ค.
To understand the concept of pagination, it is necessary to examine how data actually flows after a route is executed.
โ๏ธ ์๋๋ ๋ฐ์ดํฐ ํ๋ฆ ๊ฒฝ๋ก ์ค๋ช
์
๋๋ค.
The following is a description of the data flow path.
1)๋ผ์ฐํธ๊ฐ ์คํ๋๋ฉด page.tsxํ์ผ์ ์คํํฉ๋๋ค.
When the route is executed, the page.tsx file is executed.
A.์ด page.tsx์์ ๊ฐ์
๋์ง ์์ ์ฌ์ฉ์ ์ด๋ฆ์ผ๋ก ์ ๊ทผํ๋ฉด 404(page not found)์ค๋ฅ๋ฅผ ๋ฐ์ ์ํต๋๋ค.
In this page.tsx, accessing the page with a username that does not exist triggers a 404 (page not found) error.
B. @username์ด ๊ฐ์
๋ ์ฌ์ฉ์๋ฉด ClientPage.tsxํ์ผ์ ํธ์ถ ํฉ๋๋ค.
If @username is a registered user, the ClientPage.tsx file is called.
# ๋ผ์ฐํธ ์คํ ์ฃผ์(URl) / Route Execution URL
https://aloy-horizon.duckdns.org/@user1 or @your_user_name
# page.tsx ๋๋ ํ ๋ฆฌ ๊ฒฝ๋ก / page.tsx directory path
app/usersui/[username]/page.tsx
๐ก ์ฌ๊ธฐ์ ๋ถํฐ๋ ์กํฐ๋ธ ํ
๋ง ์ค๋ช
์
๋๋ค.
The following is an explanation of the active theme.
2)ClientPage.tsx์์ ์กํฐ๋ธ ํ
๋ง๋ฅผ ํธ์ถํฉ๋๋ค.
Call the active theme in ClientPage.tsx.
โ๏ธ app/usersui/[username]/ClientPage.tsx
โ๏ธ ๋ก๊ทธ์ธ ์ฌ๋ถ์ ๋ฐ๋ผ์ ์กํฐ๋ธ ํ
๋ง์ ์ด๊ธฐ๋ฐ์ดํฐ, ๋ถ์คํธ, ์ข์์, ๊ณต์ ๊ธฐ๋ฅ์ ์กํฐ๋ธ ํ
๋ง์ ์ ๋ฌํฉ๋๋ค.
Depending on the login status, initial data, boost, like, and share functions are passed to the active theme.
const props = { timeline, username, initialView, onBoost: handleBoost, onLike: handleLike, onShare: handleShare };
// getThemeComponent : import { themeRegistry, getThemeComponent } from './_components/themes'; --> index.ts
const ActiveTheme = getThemeComponent(theme as any) || themeRegistry.themeex?.component || themeRegistry[themeNames[0]]?.component;
3) ์กํฐ๋ธ ํ
๋ง ํธ์ถ ๋ก์ง
Active theme invocation logic
โ๏ธ ์กํฐ๋ธ ํ
๋ง๋ฅผ ๋ถ๋ฌ์ค๋ ๋ก์ง์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
The logic for loading the active theme is as follows.
A.ClientPage.tsx์์ index.tsํ์ผ์ ์ ์ฅ๋ getThemeComponent(theme as any) ํจ์๋ฅผ ํธ์ถํฉ๋๋ค.
ClientPage.tsx calls the getThemeComponent(theme as any) function stored in the index.ts file.
- ClientPage.tsx
import { themeRegistry, getThemeComponent } from './_components/themes';
... ...
const ActiveTheme = getThemeComponent(theme as any)
- ./_components/themes/index.ts
# index.ts๋ ์๋์ผ๋ก ์์ฑ๋๋ ํ์ผ / index.ts is an automatically generated file.
# ์ฌ๊ธฐ์๋ themeex๋ฅผ ๊ธฐ์ค์ผ๋ก ์ค๋ช
ํจ. / The explanation here is based on themeex.
# MainTheme.tsx๋ฅผ ํธ์ถํด์ ๋ด์ฉ์ MainTheme๋ณ์์ ์ ์ฅํ๊ณ themeexCompo๋ณ์์ ์ ์ฅ
Call MainTheme.tsx, then store the content in the MainTheme variable and the themeexCompo variable.
# themeRegistry์ ๋ชจ๋ MainTheme๋ฅผ ์ฌ์ฉํ๋ฉด ์ถฉ๋์ด ์์ผ๋ฏ๋ก themeexComp์ฒ๋ผ ๋ค๋ฅธ ์ด๋ฆ์ ์ฉ
# Since using MainTheme for everything in the themeRegistry causes conflicts,
# apply a different name like themeexComp
... ...
import { MainTheme as themeexComp} from './themeex/MainTheme';
# component๋ ํธ์ถ๋ ํจ์๋ก themeexComp(=Maintheme)๋ฅผ ์ง์ ํจ.
# The `component` specifies `themeexComp` (i.e., `Maintheme`) as the function to be called.
export const themeRegistry = {
mastodon: { component: mastodonComp, label: 'mastodon' },
minimal: { component: minimalComp, label: 'minimal' },
pinafore: { component: pinaforeComp, label: 'pinafore' },
themeex: { component: themeexComp, label: 'themeex' },
} as const;
# typeof : ... pinafore: {...}, themeex: {...}
# keyof : 'mastodon' | 'minimal' | 'pinafore' | 'themeex'
export type ThemeName = keyof typeof themeRegistry;
# ์ค์ ๋ก themeRegistry๋ฐฐ์ด์์ themeexํด๋น ํ
๋ง์ component์ธ MainThemeํจ์๋ฅผ ๋ฆฌํด
# Actually returns the MainTheme function, which is the component of the corresponding theme, from the themeRegistry array themeex
export function getThemeComponent(name: ThemeName) {
return themeRegistry[name]?.component;
}
- MainThemeํจ์ props / MainTheme function props
# ClientPage.tsx ->
# const props = { timeline, username, initialView, onBoost: handleBoost, onLike: handleLike, onShare: handleShare };
# MainTheme.tsx์ MainThemeํจ์
# The MainTheme function in MainTheme.tsx
export function MainTheme({ timeline : initialTimeline, username, initialView, onBoost, onLike, onShare }: any) { ... }
๐ก ์กํฐ๋ธ ํ ๋ง ์ค๋ช ๋ / End of active theme description.
4)MainTheme.tsx์ MainThemeํจ์์ ์ ๋ฌ๋ props๋ค์ ์์ ํ TimelineList.tsx์ ์ ๋ฌ๋ฉ๋๋ค.
The props passed to the MainTheme function in MainTheme.tsx are passed to TimelineList.tsx after modification.
- username์ @username๊ฐ์ผ๋ก ๋ก๊ทธ์ธํ์ง ์์ ๊ฒฝ์ฐ ๋ณด์ฌ์ค๋ฐ์ดํฐ๋ ๋ผ์ฐํธ๋ฅผ ์ํด์ ์ฌ์ฉํฉ๋๋ค.
Theusernameis used for data or routes to be displayed when the user has not logged in using the@usernamevalue. - onBoost,onLike๋ handleBoostClick, handleLikeClickํจ์์ ์ถ๊ฐ๋ฉ๋๋ค.
onBoost and onLike are added to the handleBoostClick and handleLikeClick functions. - handleBoostClick,handLikeClickํจ์๋ ๋ก๊ทธ์ธํ์ง ์์ ๊ฒฝ์ฐ ํด๋ฆญํ๋ฉด ๋ก๊ทธ์ธ ํ์ด์ง๋ก ์ด๋ํ๋ ์ญํ ์ ์ํํฉ๋๋ค.
ThehandleBoostClickandhandleLikeClickfunctions redirect the user to the login page if they are not logged in.
... ...
import TimelineList from './TimelineList'; // โ
myapp54
... ...
# MainTheme.tsx์ MainThemeํจ์
# The MainTheme function in MainTheme.tsx
export function MainTheme({ timeline : initialTimeline, username, initialView, onBoost, onLike, onShare }: any) { ... }
... ...
# ์ ๋ฌ ๋ฐ์ props๊ฐ ๋ณ๊ฒฝ๋ ๋ถ๋ถ
# Parts where received props have changed
const [timeline, setTimeline] = useState<any[]>(initialTimeline || []);
const handleBoostClick = async (p: any) => {
if (!currentUser) return window.location.href = `/auth/login?next=/@${username}`;
onBoost(p)
};
const handleLikeClick = (p: any) => {
if (!currentUser) return window.location.href = `/auth/login?next=/@${username}`;
onLike(p);
};
... ...
# TimelineList.tsx์ props ์ ๋ฌ
# Passing props to TimelineList.tsx
{view === 'timeline' && (
<>
{/* โ
myapp56 - ADD Dropdown(Home/Local/Federated Tabs ) */}
<TimelineList
timeline={timeline}
tab={tab}
onTabChange={setTab}
currentUser={currentUser}
onDelete={handleDelete}
onBoost={handleBoostClick}
onLike={handleLikeClick}
onShare={onShare}
/>
</>
)} {/* end view === 'timeline' */}
5)TimelineList.tsx
- ๊ฐ onBoost,onLike,onShareํจ์๊ฐ ๋ด์ฅ๋ ๋ฒํผ์ ํด๋ฆญํ๋ฉด ClientPage.tsx์ handleBoost, handleLike, handleShare ํจ์๊ฐ ์คํ๋ฉ๋๋ค.
Clicking a button with the built-inonBoost,onLike, oronSharefunction triggers the correspondinghandleBoost,handleLike, orhandleSharefunction inClientPage.tsx. - ๊ฐ ์ปจํ
์ธ ๋ .map((p: any, idx: number) => { ์ผ๋ก ํ๋ฉด์ ๋ฐ์ดํฐ๋ฅผ ํ์ํฉ๋๋ค.
Each piece of content displays data on the screen using.map((p: any, idx: number) => {.
export default function TimelineList({
timeline: initialTimeline, currentUser, tab, onTabChange, onDelete, onBoost, onLike, onShare }: Props) { ... ...
{/**
* p && p.id.map(...)
* - p์ p.id๊ฐ ์์ผ๋ฉด map๋ถ๋ถ ์คํ / If p and p.id exist, execute the map section.
*
* idx: number
* - map()ํจ์์์ ์๋์ผ๋ก ์ ๊ณต๋๋ ์ธ๋ฑ์ค(0๋ถํฐ ์์)
* The index (starting from 0) automatically provided by the map() function.
*
*/}
{data.filter((p:any) => p && p.id).map((p: any, idx: number) => {
const uniqueKey = `${p.id}-${idx}`;
// โ
myapp61
const acct = p.account?.acct || '';
const username = p.account?.username || p.username || '';
const acctDomain = acct.includes('@')? acct.split('@')[1] : DOMAIN;
const isLocalAccount = acctDomain === DOMAIN;
const isMine =!!(currentUser && isLocalAccount && username === currentUser && p.account?.id!== `remote_${username}@${acctDomain}`);
... ...
return(
... ...
{/* post*/}
<div className="flex gap-2 items-center flex-wrap">
<b className="text-">{/*โ
myapp61*/ getDisplayName(p)}</b>
<span className="text-[#999] text-xs">@{ /*โ
myapp61*/getHandle(p)
/* new Date(p.created_at).toLocaleString()*/}</span>
{isRemote(p) && <span className="text-[10px]">๐</span>}
{p.isBoostedPost && <span className="text-[#16a34a] text-xs bg-[#dcfce7] px-1.5 py-0.5 rounded">๐ {getDisplayName(p.actor)}</span>}
{/* โ
myapp61 */}
{isMine && (
<button onClick={() => onDelete(p.id)} className="text-[#e0245e] ml-auto text-xs">
๐ Delete
</button>
)}
</div>
{/* contents*/}
<div className="my-2 leading-[1.4] break-words text- [&_p]:m-0" dangerouslySetInnerHTML={{ __html: p.content }} />
{/* image */}
{p.media_attachments && p.media_attachments.length > 0 && (
<div className={`grid gap-2 mt-2.5 rounded-xl overflow-hidden ${p.media_attachments.length === 1? 'grid-cols-1' : 'grid-cols-2'}`}>
{p.media_attachments.map((m: any) => (
<img key={m.id} src={toAbsolute(m.preview_url || m.url)} alt="media" loading="lazy" className="w-full max-h- object-cover rounded-lg border border-[#e6ecf0]" />
))}
</div>
)}
{/* boost, like, share */}
<div className="flex gap-4 mt-2">
<button onClick={() => onBoost(p)} className={`bg-none border-none cursor-pointer text-[#999] px-2 py-1 rounded-xl text-sm hover:bg-[#f3f4f6] hover:text-[#333] ${p.isMyBoost? '!text-[#1b9c59] font-bold bg-[#dcfce7]' : ''}`}>๐ {p.boostCount || ''}</button>
<button onClick={() => onLike(p)} className={`bg-none border-none cursor-pointer text-[#999] px-2 py-1 rounded-xl text-sm hover:bg-[#f3f4f6] hover:text-[#333] ${p.isMyLike? '!text-[#e0245e] bg-[#ffe4e6]' : ''}`}>โญ {p.likeCount || ''}</button>
<button onClick={() => onShare(p)} className="bg-none border-none cursor-pointer text-[#999] px-2 py-1 rounded-xl text-sm hover:bg-[#f3f4f6] hover:text-[#333]">โ</button>
<button className="bg-none border-none cursor-pointer text-[#999] px-2 py-1 rounded-xl text-sm hover:bg-[#f3f4f6] hover:text-[#333]">๐ฌ</button>
</div>
... ...
); //end return
})} // end timelielist function
6)ํ์๋ผ์ธ API ๋ฐ์ดํฐ ์์ / Timeline API Data Example
- ์๋์ฒ๋ผ ํ์๋ผ์ธ API๋ฅผ ์คํํ๋ ๊ฒฝ์ฐ ์์ ๋ฐ์ดํฐ์
๋๋ค.(๋ก๊ทธ์ธํ ๊ฒฝ์ฐ)
The following is example data for executing the Timeline API (when logged in).
https://aloy-horizon.duckdns.org/api/timeline?username=user1&type=home
[
{
"id": "01M4J098H7BH1QZSDQ2B63V4WM",
"content": "\u003Cp\u003Egts public\u003C/p\u003E",
"username": "user1",
"actor": "https://freelifemakers.com/users/user1",
"created_at": "2026-10-10T13:14:05+09:00",
"visibility": "public",
"type": "Note",
"source": "inbox",
"original_id": "https://freelifemakers.com/users/user1/statuses/01M4J098H7BH1QZSDQ2B63V4WM",
"fullId": "https://freelifemakers.com/users/user1/statuses/01M4J098H7BH1QZSDQ2B63V4WM",
"isMyBoost": false,
"boostCount": 0,
"isMyLike": false,
"likeCount": 0,
"isBoostedPost": false,
"isMine": false
},
{
"id": "5cdd75c8-1dfe-4acc-90e9-f24968a8e1f0",
"content": "local public",
"username": "user1",
"actor": "user1",
"created_at": 1791605663435,
"visibility": "public",
"type": "Note",
"source": "mine",
"original_id": "5cdd75c8-1dfe-4acc-90e9-f24968a8e1f0",
"fullId": "https://aloy-horizon.duckdns.org/users/user1/statuses/5cdd75c8-1dfe-4acc-90e9-f24968a8e1f0",
"isMyBoost": false,
"boostCount": 0,
"isMyLike": false,
"likeCount": 0,
"isBoostedPost": false,
"isMine": true
},
{
"id": "608e90d3-f138-47a3-afff-93239335bea8",
"content": "test4 private",
"username": "user1",
"actor": "user1",
"created_at": 1791590136977,
"visibility": "private",
"type": "Note",
"source": "mine",
"original_id": "608e90d3-f138-47a3-afff-93239335bea8",
"fullId": "https://aloy-horizon.duckdns.org/users/user1/statuses/608e90d3-f138-47a3-afff-93239335bea8",
"isMyBoost": false,
"boostCount": 0,
"isMyLike": false,
"likeCount": 0,
"isBoostedPost": false,
"isMine": true
}
]
๐ก ๋ค์ ํฌ์คํธ์์ ๊ณ์ ๋ฉ๋๋ค. / To be continued in the next post.
๋งํ๋ณต์ 21์ฅ 22์ / Matthew 21: 22
“๋ํฌ๊ฐ ๊ธฐ๋ํ ๋์ ๋ฌด์์ด๋ ์ง ๋ฏฟ๊ณ ๊ตฌํ๋ ๊ฒ์ ๋ค ๋ฐ์ผ๋ฆฌ๋ผ ํ์๋๋ผ”
“And everything you ask in prayer, believing, you will receive.”