Socket을 사용한 Read-Time 채팅 구현하기 - (4) useInfiniteQuery Hook 구현하기
Socket을 사용한 Read-Time 채팅 구현하기 - (4) useInfiniteQuery Hook 구현하기
⚡️ Socket을 사용한 Real-Time 채팅 구현하기 - (4) useInfiniteQuery Hook 구현하기
이전 글에서 다룬 내용
- Socket.io 초기 설정
- 채팅 입력창 및 메시지 전송 API 구현
- 무한 스크롤을 지원하는 채팅 메시지 UI 구현
이번 글에서는 무한 스크롤을 지원하는 채팅 메시지 페칭 로직을 구현한다.
이를 위해 React Query의 useInfiniteQuery
를 활용하여 데이터를 효율적으로 가져오고,
소켓 상태에 따라 실시간 갱신을 최적화하는 커스텀 훅을 만든다.
1️⃣ React Query 패키지 설치
먼저 TanStack Query(React Query) 패키지를 설치한다.
1
npm install @tanstack/react-query
1
"@tanstack/react-query": "^5.60.2"
설치가 완료되면, 전역 상태로 QueryClient
와 QueryClientProvider
를 설정해야 한다.
2️⃣ QueryClient 및 QueryProvider 설정
React Query를 사용하려면 전역적으로 QueryClient
인스턴스를 생성하고,
QueryClientProvider
를 통해 애플리케이션 전반에서 사용할 수 있도록 설정해야 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
// components/providers/query-provider.tsx
"use client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { useState } from "react";
export const QueryProvider = ({ children }: { children: React.ReactNode }) => {
const [queryClient] = useState(() => new QueryClient());
return (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
};
이제
QueryProvider
를layout.tsx
에 추가하면, 애플리케이션에서 React Query를 사용할 준비가 완료된다.
1
2
3
4
5
6
7
8
9
10
// app/layout.tsx
import { QueryProvider } from "@/components/providers/query-provider";
export default function RootLayout({
children
}: {
children: React.ReactNode;
}) {
return <QueryProvider>{children}</QueryProvider>;
}
3️⃣ useChatQuery 커스텀 훅 구현
이제 무한 스크롤을 지원하는 채팅 메시지를 불러오는 커스텀 훅을 만든다.
여기서는 useInfiniteQuery
를 활용하여 데이터를 가져오고,
소켓 연결 상태에 따라 데이터를 자동 갱신하는 기능을 추가한다.
✅ useChatQuery
기능 정리
1️⃣ fetchMessages 함수
- API 요청을 보내서 메시지를 가져온다.
pageParam
을 사용하여 페이징을 지원한다.useInfiniteQuery
의queryFn
으로 사용된다.
2️⃣ useInfiniteQuery 사용
queryKey
: React Query의 캐싱을 위한 키.queryFn
: 데이터를 가져오는 함수 (fetchMessages
).getNextPageParam
: 다음 페이지의 cursor 값을 반환하여 페이징을 지원.refetchInterval
: 소켓이 연결되지 않았을 때만 1초 간격으로 데이터를 갱신한다.
3️⃣ 소켓 연결 여부 확인
useSocket
훅을 사용하여 소켓 연결 상태를 확인한다.- 소켓이 연결된 상태에서는 서버 요청을 최소화하고, 실시간 업데이트만 사용한다.
4️⃣ 훅의 반환값
data
: 채팅 메시지 데이터 (페이지 단위).fetchNextPage
: 다음 페이지의 데이터를 가져오는 함수.hasNextPage
: 더 가져올 데이터가 있는지 여부.isFetchingNextPage
: 다음 페이지 로드 중 상태.status
: 데이터 요청 상태 (loading
,success
,error
).
3-1. useChatQuery
구현 코드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// hooks/use-chat-query.ts
import { useInfiniteQuery } from "@tanstack/react-query";
import { useSocket } from "@/components/providers/socket-provider";
import qs from "query-string";
interface ChatQueryProps {
queryKey: string;
apiUrl: string;
paramKey: string;
paramValue: string;
}
export const useChatQuery = ({
queryKey,
apiUrl,
paramKey,
paramValue
}: ChatQueryProps) => {
const { isConnected } = useSocket(); // 소켓 연결 상태 확인
// 1️⃣ fetchMessages 함수: 메시지 데이터를 가져오는 API 요청
const fetchMessages = async ({ pageParam = undefined }) => {
const url = qs.stringifyUrl(
{
url: apiUrl,
query: {
cursor: pageParam,
[paramKey]: paramValue
}
},
{ skipNull: true }
);
const res = await fetch(url);
return res.json();
};
// 2️⃣ useInfiniteQuery를 활용한 무한 스크롤 적용
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, status } =
useInfiniteQuery({
queryKey: [queryKey], // 캐싱을 위한 고유 키
queryFn: fetchMessages, // 메시지 데이터 요청 함수
getNextPageParam: (lastPage) => lastPage?.nextCursor, // 다음 페이지 cursor 설정
refetchInterval: isConnected ? false : 1000 // 소켓이 끊겼을 때만 1초 간격으로 데이터 갱신
});
return { data, fetchNextPage, hasNextPage, isFetchingNextPage, status };
};
4️⃣ useChatQuery
를 채팅 메시지 컴포넌트에 적용
이제 useChatQuery
를 사용하여 채팅 메시지를 무한 스크롤로 렌더링한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
// components/chat/chat-messages.tsx
"use client";
import { useChatQuery } from "@/hooks/use-chat-query";
import { ChatWelcome } from "@/components/chat/chat-welcome";
import { Loader2, ServerCrash } from "lucide-react";
import { Fragment } from "react";
interface ChatMessagesProps {
name: string;
chatId: string;
apiUrl: string;
paramKey: "channelId" | "conversationId";
paramValue: string;
type: "channel" | "conversation";
}
export const ChatMessages = ({
name,
chatId,
apiUrl,
paramKey,
paramValue,
type
}: ChatMessagesProps) => {
const queryKey = `chat:${chatId}`;
// useChatQuery 훅 사용
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, status } =
useChatQuery({ queryKey, apiUrl, paramKey, paramValue });
// 1️⃣ 로딩 상태 처리
if (status === "loading") {
return (
<div className="flex flex-col flex-1 justify-center items-center">
<Loader2 className="h-7 w-7 text-zinc-500 animate-spin my-4" />
<p className="text-xs text-zinc-500 dark:text-zinc-400">
Loading messages...
</p>
</div>
);
}
// 2️⃣ 에러 처리
if (status === "error") {
return (
<div className="flex flex-col flex-1 justify-center items-center">
<ServerCrash className="h-7 w-7 text-zinc-500 my-4" />
<p className="text-xs text-zinc-500 dark:text-zinc-400">
Something went wrong!
</p>
</div>
);
}
// 3️⃣ 메시지 렌더링
return (
<div className="flex-1 flex-col flex overflow-y-auto">
<ChatWelcome name={name} type={type} />
<div className="flex flex-col-reverse mt-auto">
{data?.pages?.map((group, i) => (
<Fragment key={i}>
{group.items.map((message) => (
<div key={message.id}>{message.content}</div>
))}
</Fragment>
))}
</div>
</div>
);
};
✅ 정리
- React Query의
useInfiniteQuery
를 활용하여 무한 스크롤 지원 - 소켓 연결 상태에 따라 자동 갱신 최적화
- 채팅 메시지 컴포넌트에서 데이터를 실시간으로 불러와 표시
This post is licensed under CC BY 4.0 by the author.