Socket을 사용한 Read-Time 채팅 구현하기 - (3) 채팅 메시지 컴포넌트 구현하기
Socket을 사용한 Read-Time 채팅 구현하기 - (3) 채팅 메시지 컴포넌트 구현하기
📢 Socket을 사용한 Real-Time 채팅 구현하기 - (3) 채팅 메시지 컴포넌트 구현하기
이전 글에서 했던 작업
- (1) Socket IO 초기 세팅
- (2) 채팅 입력창 및 메시지 전송 API 구현
이번 글에서는 실제로 채팅 메시지들을 렌더링하는 컴포넌트를 구현한다.
메시지를 불러올 때 페이징을 적용하여 성능을 최적화하고,
새로운 메시지가 올 때 실시간으로 업데이트되는 기능도 추가할 것이다.
1️⃣ 채팅 메시지 API 구현
채팅에서 가장 중요한 부분 중 하나는 메시지 데이터를 효율적으로 불러오는 것이다.
이를 위해 페이징(pagination)과 무한 스크롤을 지원하는 메시지 API를 만들었다.
1-1. Messages API 생성 (GET 요청 처리)
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
// app/api/messages/route.ts
import { NextResponse } from "next/server";
import { currentProfile } from "@/lib/current-profile";
import { Message } from "@prisma/client";
import { db } from "@/lib/db";
// (1) 한 번에 로드할 메시지 개수 제한
const MESSAGES_BATCH = 10;
export async function GET(req: Request) {
try {
const profile = await currentProfile();
const { searchParams } = new URL(req.url);
const cursor = searchParams.get("cursor"); // 페이징 기준점
const channelId = searchParams.get("channelId");
if (!profile) {
return new NextResponse("Unauthorized", { status: 401 });
}
if (!channelId) {
return new NextResponse("Channel ID Missing", { status: 400 });
}
// (2) 메시지 조회 (페이징 적용)
let messages: Message[] = [];
if (cursor) {
messages = await db.message.findMany({
take: MESSAGES_BATCH, // 10개씩 불러오기
skip: 1,
cursor: { id: cursor },
where: { channelId },
include: {
member: { include: { profile: true } }
},
orderBy: { createdAt: "desc" }
});
} else {
messages = await db.message.findMany({
take: MESSAGES_BATCH,
where: { channelId },
include: {
member: { include: { profile: true } }
},
orderBy: { createdAt: "desc" }
});
}
// (3) 다음 페이지의 cursor 설정
let nextCursor = null;
if (messages.length === MESSAGES_BATCH) {
nextCursor = messages[MESSAGES_BATCH - 1].id;
}
return NextResponse.json({ items: messages, nextCursor });
} catch (error) {
console.log("[MESSAGES_GET]", error);
return new NextResponse("Internal Error", { status: 500 });
}
}
✅ API 동작 방식
- 한 번에 10개씩 메시지를 가져온다.
cursor
를 기반으로 페이징을 처리한다.cursor
가 없으면 최신 메시지를 가져온다.cursor
가 있으면, 해당 메시지 이후의 데이터를 가져온다.
nextCursor
값을 반환하여 클라이언트에서 무한 스크롤을 적용할 수 있게 한다.
2️⃣ 채팅 메시지 UI 구현
이제 실제로 채팅 메시지들을 화면에 렌더링하는 컴포넌트를 구현한다.
메시지 불러오기, 무한 스크롤 적용, 실시간 업데이트 등의 기능이 포함된다.
2-1. 채팅 시작 화면 표시
- 새로운 채널에 들어왔을 때 기본적으로 환영 메시지를 표시한다.
- 채널인지, 개인 대화인지에 따라 UI를 다르게 구성한다.
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
// components/chat/chat-welcome.tsx
import { Hash } from "lucide-react";
interface ChatWelcomeProps {
name: string;
type: "channel" | "conversation";
}
export const ChatWelcome = ({ name, type }: ChatWelcomeProps) => {
return (
<div className="space-y-2 px-4 mb-4">
{type === "channel" && (
<div className="h-[75px] w-[75px] rounded-full bg-zinc-500 dark:bg-zinc-700 flex items-center justify-center">
<Hash className="h-12 w-12 text-white" />
</div>
)}
<p className="text-xl md:text-3xl font-bold">
{type === "channel" ? "Welcome to #" : ""}
{name}
</p>
<p className="text-zinc-600 dark:text-zinc-400 text-sm">
{type === "channel"
? `This is the start of the ${name} channel.`
: `This is the start of your conversation with ${name}.`}
</p>
</div>
);
};
2-2. 채팅 메시지 컴포넌트 구현
- 무한 스크롤을 적용하여 이전 메시지를 불러올 수 있도록 한다.
- 메시지를 실시간으로 업데이트할 수 있도록
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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
// components/chat/chat-messages.tsx
"use client";
import { Member, Message, Profile } from "@prisma/client";
import { Loader2, ServerCrash } from "lucide-react";
import { useChatQuery } from "@/hooks/use-chat-query";
import { ChatWelcome } from "@/components/chat/chat-welcome";
import { Fragment } from "react";
type MessageWithMemberWithProfile = Message & {
member: Member & {
profile: Profile;
};
};
interface ChatMessagesProps {
name: string;
member: Member;
chatId: string;
apiUrl: string;
socketUrl: string;
socketQuery: Record<string, string>;
paramKey: "channelId" | "conversationId";
paramValue: string;
type: "channel" | "conversation";
}
export const ChatMessages = ({
name,
member,
chatId,
apiUrl,
socketUrl,
socketQuery,
paramKey,
paramValue,
type
}: ChatMessagesProps) => {
const queryKey = `chat:${chatId}`;
// (1) useChatQuery 가져오기 (무한 스크롤 & 실시간 메시지 적용)
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, status } =
useChatQuery({ queryKey, apiUrl, paramKey, paramValue });
// (2) 로딩 & 에러 처리
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>
);
}
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">
<div className="flex-1" />
<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: MessageWithMemberWithProfile) => (
<div key={message.id}>{message.content}</div>
))}
</Fragment>
))}
</div>
</div>
);
};
✅ 정리
- 메시지 API를 만들어 페이징 처리 적용
- 무한 스크롤과 실시간 메시지를 지원하는 채팅 컴포넌트 구현
- 채팅 UI를 개선하여 환영 메시지 및 메시지 리스트 추가
This post is licensed under CC BY 4.0 by the author.