Post

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"

설치가 완료되면, 전역 상태로 QueryClientQueryClientProvider를 설정해야 한다.


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>
  );
};

이제 QueryProviderlayout.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을 사용하여 페이징을 지원한다.
  • useInfiniteQueryqueryFn으로 사용된다.

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.