diff --git a/src/app/message/page.test.tsx b/src/app/message/page.test.tsx index f2c53c8f..0d9e2a46 100644 --- a/src/app/message/page.test.tsx +++ b/src/app/message/page.test.tsx @@ -1,10 +1,10 @@ +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { render, screen } from '@testing-library/react'; +import { FollowingContent } from '@/components/pages/message/message-following-content'; import { ModalProvider } from '@/components/ui'; import { useInfiniteScroll } from '@/hooks/use-group/use-group-infinite-list'; -import FollowingPage from './page'; - jest.mock('@/hooks/use-group/use-group-infinite-list', () => ({ useInfiniteScroll: jest.fn(), })); @@ -15,12 +15,18 @@ jest.mock('next/navigation', () => ({ }), })); -jest.mock('js-cookie', () => ({ - get: () => '1', -})); - describe('FollowingPage 테스트', () => { + let queryClient: QueryClient; + beforeEach(() => { + queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }); + (useInfiniteScroll as jest.Mock).mockReturnValue({ items: [], error: null, @@ -31,11 +37,17 @@ describe('FollowingPage 테스트', () => { }); }); + afterEach(() => { + queryClient.clear(); + }); + test('팔로잉이 없을 경우 FollowingNone을 보여준다', async () => { render( - - - , + + + + + , ); expect(await screen.findByText('아직 팔로우 한 사람이 없어요.')).toBeInTheDocument(); diff --git a/src/app/message/page.tsx b/src/app/message/page.tsx index 9b99a901..2c54d0f8 100644 --- a/src/app/message/page.tsx +++ b/src/app/message/page.tsx @@ -1,105 +1,41 @@ -'use client'; +import { cookies } from 'next/headers'; -import { useSearchParams } from 'next/navigation'; - -import { useEffect, useState } from 'react'; - -import Cookies from 'js-cookie'; +import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query'; import { API } from '@/api'; -import { ChatList } from '@/components/pages/chat'; -import { FollowingList, FollowingNone, FollowingSearch } from '@/components/pages/message'; -import { TabNavigation } from '@/components/shared'; -import { useInfiniteScroll } from '@/hooks/use-group/use-group-infinite-list'; -import { useIntersectionObserver } from '@/hooks/use-intersection-observer'; -import { INTERSECTION_OBSERVER_THRESHOLD } from '@/lib/constants/group-list'; +import { FollowingContent } from '@/components/pages/message/message-following-content'; -const SOCIAL_TABS = [ - { label: '팔로잉', value: 'following' }, - { label: '메세지', value: 'chat' }, -]; +const INITIAL_PAGE_SIZE = 10; -export default function FollowingPage() { - const [userId, setUserId] = useState(0); - const params = useSearchParams(); - const tab = params.get('tab') || 'chat'; +export default async function MessagePage() { + const cookieStore = await cookies(); + const userId = Number(cookieStore.get('userId')?.value || 0); - useEffect(() => { - const id = Cookies.get('userId'); - // eslint-disable-next-line react-hooks/set-state-in-effect - setUserId(Number(id)); - }, []); + const queryClient = new QueryClient(); - // 1. 무한 스크롤 훅 호출 - const { - items: followers, - error, - fetchNextPage, - hasNextPage, - isFetchingNextPage, - completedMessage, - } = useInfiniteScroll({ - queryFn: async ({ cursor, size }) => { + // 첫 페이지 우선 prefetch + await queryClient.prefetchInfiniteQuery({ + queryKey: ['followers', userId], + queryFn: async () => { return await API.followerService.getFollowers({ userId, - cursor, - size, + cursor: undefined, + size: INITIAL_PAGE_SIZE, }); }, - queryKey: ['followers', userId], - completedMessage: '모든 팔로잉을 불러왔습니다.', - enabled: !!userId, - }); - - // 2. IntersectionObserver로 스크롤 감지 - const sentinelRef = useIntersectionObserver({ - onIntersect: () => { - if (hasNextPage && !isFetchingNextPage) { - fetchNextPage(); - } + initialPageParam: undefined, + getNextPageParam: (lastPage) => { + return lastPage.nextCursor ?? undefined; }, - enabled: hasNextPage && error === null, - threshold: INTERSECTION_OBSERVER_THRESHOLD, + pages: 1, }); - return ( -
- - - {tab === 'chat' && } - {tab === 'following' && ( - <> - - {!error && followers && followers.length > 0 ? ( - <> - - - {/* 3. Sentinel 요소 (필수!) */} - {hasNextPage &&
} + // dehydrate로 직렬화 + const dehydratedState = dehydrate(queryClient); - {/* 4. 다음 페이지 로딩 중 */} - {isFetchingNextPage && ( -
- 더 불러오는 중... -
- )} - - {/* 5. 완료 메시지 */} - {!hasNextPage && ( -
- {completedMessage} -
- )} - - ) : ( - !error && ( -
- -
- ) - )} - - )} -
+ return ( + + + ); } diff --git a/src/components/pages/message/message-following-content/index.tsx b/src/components/pages/message/message-following-content/index.tsx new file mode 100644 index 00000000..b8f0ce71 --- /dev/null +++ b/src/components/pages/message/message-following-content/index.tsx @@ -0,0 +1,98 @@ +'use client'; + +import { useSearchParams } from 'next/navigation'; + +import { API } from '@/api'; +import { ChatList } from '@/components/pages/chat'; +import { FollowingList, FollowingNone, FollowingSearch } from '@/components/pages/message'; +import { TabNavigation } from '@/components/shared'; +import { useInfiniteScroll } from '@/hooks/use-group/use-group-infinite-list'; +import { useIntersectionObserver } from '@/hooks/use-intersection-observer'; +import { INTERSECTION_OBSERVER_THRESHOLD } from '@/lib/constants/group-list'; + +const SOCIAL_TABS = [ + { label: '팔로잉', value: 'following' }, + { label: '메세지', value: 'chat' }, +]; + +interface FollowingContentProps { + initialUserId: number; +} + +export const FollowingContent = ({ initialUserId }: FollowingContentProps) => { + const params = useSearchParams(); + const tab = params.get('tab') || 'chat'; + + // 1. 무한 스크롤 훅 호출 (서버에서 prefetch된 데이터가 자동으로 사용됨) + const { + items: followers, + error, + fetchNextPage, + hasNextPage, + isFetchingNextPage, + completedMessage, + } = useInfiniteScroll({ + queryFn: async ({ cursor, size }) => { + return await API.followerService.getFollowers({ + userId: initialUserId, + cursor, + size, + }); + }, + queryKey: ['followers', initialUserId], + completedMessage: '모든 팔로잉을 불러왔습니다.', + enabled: !!initialUserId, + }); + + // 2. IntersectionObserver로 스크롤 감지 + const sentinelRef = useIntersectionObserver({ + onIntersect: () => { + if (hasNextPage && !isFetchingNextPage) { + fetchNextPage(); + } + }, + enabled: hasNextPage && error === null, + threshold: INTERSECTION_OBSERVER_THRESHOLD, + }); + + return ( +
+ + + {tab === 'chat' && } + {tab === 'following' && ( + <> + + {!error && followers && followers.length > 0 ? ( + <> + + + {/* 3. Sentinel 요소 (필수!) */} + {hasNextPage &&
} + + {/* 4. 다음 페이지 로딩 중 */} + {isFetchingNextPage && ( +
+ 더 불러오는 중... +
+ )} + + {/* 5. 완료 메시지 */} + {!hasNextPage && ( +
+ {completedMessage} +
+ )} + + ) : ( + !error && ( +
+ +
+ ) + )} + + )} +
+ ); +}; diff --git a/src/components/pages/message/message-following-none/index.tsx b/src/components/pages/message/message-following-none/index.tsx index d50f9705..49f35586 100644 --- a/src/components/pages/message/message-following-none/index.tsx +++ b/src/components/pages/message/message-following-none/index.tsx @@ -1,6 +1,9 @@ +import Image from 'next/image'; + export const FollowingNone = () => { return (
+ Empty Data 아직 팔로우 한 사람이 없어요. 마음에 드는 유저를 팔로우 해보세요!
diff --git a/src/components/pages/message/message-following-search/index.tsx b/src/components/pages/message/message-following-search/index.tsx index 63b64401..da240505 100644 --- a/src/components/pages/message/message-following-search/index.tsx +++ b/src/components/pages/message/message-following-search/index.tsx @@ -9,7 +9,7 @@ export const FollowingSearch = ({ userId }: { userId: number }) => { const { open } = useModal(); return (
open()} >