[nextjs]SNS Server-51(myapp61+)[Pagenation] 

👉🏻 이번에는 페이지네이션 기능의 구조를 자세히 살펴 봅시다.
Now, let’s take a closer look at the structure of the pagination feature.

👉🏻 이전 포스트에서 데이터가 app/usersui/[username]/page.tsx에서 시작해서 app/usersui/[username]/_components/themes/themeex/Timeline.tsx까지 어떻게 흘러가는지 살펴 보았습니다.
In the previous post, we examined how data flows from app/usersui/[username]/page.tsx down to app/usersui/[username]/_components/themes/themeex/Timeline.tsx.

👉🏻 이 페이지네이션 파트에서는 초기 데이터가 화면에 표시된 후에 스크롤을 내리면 어떻게 데이터가 더 추가되는 건지 자세히 살펴봅니다.
In this pagination section, we take a detailed look at how additional data is loaded when scrolling down after the initial data is displayed on the screen.

📁 페이지네이션 흐름 / Pagination flow

1.라우트 실행 / Execute route
- https://aloy-horizon.duckdns.org/@user1
- or https://aloy-horizon.duckdns.org/usersui/user1
- page.tsx 실행
- ClientPage.tsx 파일 로드 / file load

2.ClientPage.tsx 
- 초기 데이터로드 / Initial data load
- 테마 호출 / Theme Call
- 테마 디렉토리의 TimelinList.tsx에 초기 데이터 전달
Passing initial data to TimelinList.tsx in the theme directory.

3.TimelinList.tsx
- 초기데이터 타임라인에 표시
Display initial data on the timeline
- 스크롤 내려서 초기데이터 끝에 도달하면 데이터 추가 호출 
Call for additional data when scrolling down and reaching the end of the initial data.
- 호출된 데이터를 초기데이터 뒤에 붙이기
Append the fetched data after the initial data.

📁 초기데이터 로드 – ClientPage.tsx

1)username이 바뀌면 초기 데이터가 로드가 시작됩니다.
When the username changes, the initial data begins to load.

useEffect(() => { ... ... }, [username]);

2)타임라인 분기는 로그인한 경우와 로그인 하지 않은 경우 보여지는 데이터가 다르기 때문에 타임라인 분기를 사용합니다.
Timeline branching is used because the data displayed differs depending on whether the user is logged in or not.

# 로그인 한 경우 / If logged in
# 쿠키 포함해서 전송 / Send with cookies
- credentials: 'include' 
const homeRes = await fetch(`/api/v1/timelines/home?limit=20`, { credentials: 'include' });

# 로그인 하지 않은 경우 / If not logged in
const pubRes = await fetch(`/api/v1/timelines/public?local=true&limit=20`);

3) 데이터 전달과정은 [nextjs]SNS Server-51(myapp61+)[Active theme] 이 포스트를 참조하세요
Please refer to the post “[nextjs]SNS Server-51(myapp61+)[Active theme]” for the data transmission process.

... ...
const [loading, setLoading] = useState(true);
... ...  
// ✅ myapp61 
  // - 첫 로딩: 로그인=home, 비로그인=public local
  // - Initial load: Logged in = Home; Not logged in = Public Local
  useEffect(() => {
... ...

    // 2. 타임라인 분기 / Timeline branching
    if (me) {
      // 로그인 O -> 내 home 타임라인
      // Logged in -> My home timeline
      console.log('[timeline] 로그인 유저 home 로드 / Login user home load');
      try {
        const homeRes = await fetch(`/api/v1/timelines/home?limit=20`, { credentials: 'include' });

        const json = await homeRes.json();
        let arr = Array.isArray(json)? json : json.data || [];

        setTimeline(arr);
      } catch (e) {
        console.error(e);
      }
    } else {
      // 로그인 X -> URL의 @username 글 만
      // No login -> Only the @username post in the URL
      console.log('[timeline] 비로그인/not log in, param username:', username, '글만/only posts');
      const pubRes = await fetch(`/api/v1/timelines/public?local=true&limit=20`);
      const json = await pubRes.json();
      const arr = Array.isArray(json)? json : json.data || [];

      // @user1 페이지면 user1 글만 필터
      // If on the user1 page, filter to show only user1's posts.
      const filtered = username? arr.filter((p:any) => p.account?.username === username || p.account?.acct?.startsWith(username)) : arr;
      console.log('[timeline] 비로그인 filtered:', filtered.length);
      setTimeline(filtered);
    }
    setLoading(false);
  };
  init();
}, [username]);

📁 전달 받은초기 데이터 흐름 / Initial data flow received

1.전달 받은 timeline데이터를 아래의 훅에 넣기
Pass the received timeline data into the hook below.
- TimelineList.sx

... ...
  const {
    flatData,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
  } = useCursorPagination({
     initialData: initialTimeline, // 20개 넣음 / put in 20
     fetcher:...
   })


2.useCursorPagination 함수 내부
Inside the `useCursorPagination` function
- app/hook/userCursorPagination.tsx

- 2차원 배열로 데이터 page에 저장
Store data in a 2D array on the page.
[[initialData1][initialData2]... ...]

- 이 page 배열하나 마다 20개씩 데이터 저장됨. 
Data for 20 items is stored in each element of this page array.

const [pages, setPages] = useState<T[][]>(initialData.length? [initialData] : []);

- flatData로 리턴 / Return as flatData.
- flat()함수는 2차원 배열을 1차원 배열로로 변경함(그냥 데이터 나열)
The `flat()` function converts a 2D array into a 1D array (simply listing the data).
- filter(Boolean)
 fetch실패로 null,undefined,false가 있으면 제거
 Remove null, undefined, or false values ​​resulting from a fetch failure.
... ...   

const flatData = pages.flat().filter(Boolean) as T[]; 

  return {
    flatData,
    pages: pages.map(data => ({ data })),
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
    isLoading,
  };

3.UI에 데이터 표시 / Display data on the UI
- 데이터 중복 제거, uniquekey를 위해 인덱스 사용
Using indexes for data deduplication and unique keys.

... ...
  const flat = (flatData || []).flat().filter(Boolean) as any[];
  const data = flat.length > 0 ? flat : initialTimeline.filter((p:any) => p && p.id);
... ...
return (
... ...
 {data.filter((p:any) => p && p.id).map((p: any, idx: number) => {... ...}
... ...
);

📁 페이지네이션 / Pagenation

✔️ 스크롤 감지 / Scroll detection

1) 페이지를 스크롤해서 내리다가 무한 스크롤 트리거 위치까지 30% (threshhold: 0.3)남았을때 fetchNextPage()함수 실행
Execute the fetchNextPage() function when scrolling down the page and reaching a point 30% (threshold: 0.3) away from the infinite scroll trigger position.

2)<div ref={loaderRef} 이렇게 무한스크롤 옵저버가 트리거의 위치를 감시함.
With <div ref={loaderRef}>, the infinite scroll observer monitors the position of the trigger.

1.무한스크롤 옵저버 / infinite scroll observer

  const loaderRef = useRef<HTMLDivElement>(null);
  useEffect(() => {
    const el = loaderRef.current;
    if (!el ||!hasNextPage) return;
    const observer = new IntersectionObserver(
      ([entry]) => {
        if (entry.isIntersecting &&!isFetchingNextPage) fetchNextPage();
      },
      { threshold: 0.3 }
    );
    observer.observe(el);
    return () => observer.disconnect();
  }, [hasNextPage, isFetchingNextPage, fetchNextPage]);

2. 무한 스크롤 트리거 / Infinite scroll trigger

      <div ref={loaderRef} className="h-10 flex items-center justify-center text-sm text-[#999]">
        {isFetchingNextPage && <span>불러오는 중...</span>}
        {!hasNextPage && data.length > 0 && <span className="text-xs">끝 / No more posts</span>}
      </div>

✔️ 데이터 추가 / Add Data

— TimelinList.tsx의 fetcher콜백함수(실제로 데이터 호출하는 함수)를 useCursorPagination함수 내에서 실행
Execute the fetcher callback function (the function that actually fetches the data) from TimelinList.tsx within the useCursorPagination function.

— useCursorPagination함수내의 fetchNextPage함수내에서 기존데이터에신규 데이터를추가
Add new data to existing data within the fetchNextPage function inside the useCursorPagination function

— TimelineList.tsx에서 사용가능하도록 flatData변수로 리턴,fetchNextPage함수도 사용가능하도록 리턴
Return the flatData variable so it can be used in TimelineList.tsx, and also return the fetchNextPage function so it is available for use.

1)콜백함수 fetcher가 useCursorPagination함수 내에서 실행
The callback function fetcher is executed within the useCursorPagination function.

2)Set{id1, id2, …}은 중복없는 집합
Set{id1, id2, …} is a set containing no duplicates.

3)setPages함수에 return […prev, filtered]; 로 기존데이터에 신규 데이터 추가
Add new data to the existing data in the setPages function using return [...prev, filtered];.

4)setPages함수로 저장된 pages값을 flatData로 리턴
Returns the pages value stored by the setPages function as flatData.

5)flatData로 리턴된 값을 const flatData = pages.flat().filter(Boolean) as T[]; 이렇게 flatData변수에 저장 후 리턴
Store the value returned as flatData in the flatData variable—using const flatData = pages.flat().filter(Boolean) as T[];—and then return it.

# 1.콜백함수 / Callback function
- TimelineList.tsx
- 이 콜백함수가 useCursorPagination() 훅 안에 fetchNextPage()가 실행됨.
This callback function executes `fetchNextPage()` within the `useCursorPagination()` hook.

  const {
    flatData,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
  } = useCursorPagination({
    //key: ['timeline', tab],
    key: ['timeline',tab, currentUser || 'anone'], // add current user
    initialData: initialTimeline.slice(0,20),  // ✅ myapp61
    fetcher: async (cursor) => { 
... ...

      let url = '';
      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()}`;
      }
... ...
 const res = await fetch(url, { credentials: 'include' });
 const arr = Array.isArray(json)? json : (json.data || json.statuses || []);
 const withMine = arr.map((p:any) => { ... ...  });

       return {
        data: withMine,
        nextCursor: withMine.length? withMine[withMine.length-1].id : null,
        hasMore: true
      };
  }

# 2.fetchNextPage()함수 / fetchNextPage() function
- 실제 여기서 콜백함수인 fetcher를 실행해서 API에서 가져온 데이터를 기존데이터 뒤에 붙임
Here, the `fetcher` callback function is actually executed to append the data retrieved from the API to the existing data.

- hooks/useCursorPagination.tsx

... ...
const [pages, setPages] = useState<T[][]>(initialData.length? [initialData] : []);
... ...
const fetchNextPage = useCallback(async () => {
... ...
      const res: any = await fetcher(nextCursorRef.current || undefined);
      const data = Array.isArray(res)? res : res.data || [];
... ...
      setPages(prev => {
        const seen = new Set(prev.flat().map((p:any) => p.id));
        const filtered = data.filter((p:any) => p.id &&!seen.has(p.id));
        if (filtered.length === 0) {
          setHasNextPage(false);
          return prev;
        }
        return [...prev, filtered];
      });
}
... ...

 const flatData = pages.flat().filter(Boolean) as T[]; 

  return {
    flatData,
    pages: pages.map(data => ({ data })),
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
    isLoading,
  };

# 3.useCursonpagination() 훅에서 리턴된 flatData를 다시 TimelineList.tsx flatData에 저장하고 jsx로 표시
Store the `flatData` returned from the `useCursorPagination()` hook into the `flatData` of `TimelineList.tsx` and render it in JSX.

  const {
    flatData,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
  } = useCursorPagination({ ... }

... ...
return (
... ...
{data.filter((p:any) => p && p.id).map((p: any, idx: number) => { ... ... }
... ...
);
💡 추가 페이지가 있는 경우 서버는 아래와 같은 형식으로 호출 됨
If there are additional pages, the server is called in the following format

 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

✔️ 글 50개 생성 및 삭제 / Create and delete 50 posts

💡
# 1. 글 50개 생성 (로그인 상태에서) / Generate 50 posts (while logged in)
curl -X POST https://aloy-horizon.duckdns.org/api/test/bulk \
  -H "Content-Type: application/json" \
  -b "refresh_token=YOUR_TOKEN" \
  -d '{"count":50}'

# 2. 삭제 / delete
curl -X DELETE https://aloy-horizon.duckdns.org/api/test/bulk \
  -H "Content-Type: application/json" \
  -b "refresh_token=YOUR_TOKEN" \

마태복음 21장 22절 / Matthew 21: 22

“너희가 기도할 때에 무엇이든지 믿고 구하는 것은 다 받으리라 하시니라”

“And everything you ask in prayer, believing, you will receive.”

Leave a Reply