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 (
+
아직 팔로우 한 사람이 없어요.
마음에 드는 유저를 팔로우 해보세요!
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 (