그룹채팅 - Part3 메시지 전송
ㆍProject Diary/Next.js + Prisma + MariaDB (KiloFlow)
사용자가 채팅방에서 텍스트 메시지를 전송하는 기능을 구현하는 방법에 대해 알아보겠습니다. 이 기능은 사용자가 텍스트 메시지를 웹소켓을 통해 실시간으로 전송할 수 있도록 합니다. 백엔드에서는 이를 데이터베이스에 저장하고 클라이언트에 실시간으로 전달합니다.
데이터베이스 스키마
chatMessages 테이블
model chatMessages {
id Int @id @default(autoincrement())
chatroom_id Int
user_id Int?
message String?
created_at DateTime @default(now())
chatroom chatrooms @relation(fields: [chatroom_id], references: [id], onDelete: Cascade)
user users? @relation(fields: [user_id], references: [user_id])
@@index([chatroom_id])
@@index([user_id])
}
- chatroom필드
chatroom_id 필드는 chatrooms 모델의 id 필드를 참조합니다 - user필드
user_id 필드는 users 모델의 user_id 필드를 참조합니다.
프론트엔드
1. 메시지 입력 컴포넌트
import React, { useState, KeyboardEvent } from "react";
import { useRouter } from "next/router";
import styled from "styled-components";
import socket from "../../lib/socket"; // 웹소켓 연결 모듈을 가져옵니다.
const ChatContainer = styled.div`
/* 스타일 생략 */
`;
const ChatRoom = () => {
const router = useRouter(); // 라우터 훅을 사용하여 현재 채팅방 ID를 가져옵니다.
const [message, setMessage] = useState(""); // 메시지 상태를 관리합니다.
// 엔터 키를 누르면 메시지를 전송하는 함수입니다.
const handleKeyPress = (event: KeyboardEvent<HTMLInputElement>) => {
if (event.key === "Enter") {
sendMessage();
}
};
// 메시지를 서버로 전송하는 함수입니다.
const sendMessage = () => {
if (message.trim()) { // 메시지가 공백이 아닌 경우에만 전송합니다.
socket.emit("send_message", {
roomId: router.query.id, // 현재 채팅방 ID
message,
});
setMessage(""); // 메시지를 전송한 후 입력 필드를 비웁니다.
}
};
return (
<ChatContainer>
<input
type="text"
value={message} // 입력 필드의 값을 message 상태로 설정합니다.
onChange={(e) => setMessage(e.target.value)} // 입력값이 변경되면 message 상태를 업데이트합니다.
onKeyPress={handleKeyPress} // 엔터 키를 누르면 handleKeyPress 함수를 호출합니다.
/>
<button onClick={sendMessage}>Send</button> {/* 전송 버튼을 클릭하면 sendMessage 함수를 호출합니다. */}
</ChatContainer>
);
};
export default ChatRoom;
(설명 : 주석참고)
2. 메시지 렌더링 컴포넌트
import React from "react";
import Image from "next/image";
import styled from "styled-components";
import unknownUser from "../../public/unknownUser.jpg"; // 기본 프로필 이미지
const MessageContainer = styled.div`
/* 스타일 생략 */
`;
interface Message {
id: number;
user_id: number | null;
message: string | null;
created_at: string;
}
interface User {
user_id: number;
nickname: string;
profile_image: string;
}
interface ChatMessageProps {
message: Message;
isCurrentUser: boolean;
messageUser?: User;
formatTime: (time: string) => string;
}
const ChatMessage: React.FC<ChatMessageProps> = ({
message,
isCurrentUser,
messageUser,
formatTime,
}) => {
return (
<MessageContainer isCurrentUser={isCurrentUser}>
{!isCurrentUser && (
<Image
src={messageUser ? messageUser.profile_image : unknownUser}
alt="프로필"
className="profile__image"
width={40}
height={40}
/>
)} {/* 메시지를 보낸 사용자의 프로필 이미지 표시 */}
<div className="message__content">
{!isCurrentUser && (
<div className="nickname">
{messageUser ? messageUser.nickname : "알 수 없는 사용자"}
</div>
)} {/* 메시지를 보낸 사용자의 닉네임 표시 */}
{message.message && <div>{message.message}</div>} {/* 메시지 내용 표시 */}
<div className="message__time">{formatTime(message.created_at)}</div> {/* 메시지 전송 시간 표시 */}
</div>
</MessageContainer>
);
};
export default ChatMessage;
백엔드
1. 소켓 서버 초기화 및 이벤트 핸들러 설정
import { Server } from "socket.io";
import { NextApiRequest, NextApiResponse } from "next";
import prisma from "../../../lib/prisma";
// 소켓 핸들러 함수 정의
const SocketHandler = (req: NextApiRequest, res: NextApiResponse) => {
if (!res.socket.server.io) { // 서버에 소켓이 초기화되지 않은 경우
const io = new Server(res.socket.server, {
path: "/api/community/socket", // 소켓 경로 설정
});
res.socket.server.io = io;
io.on("connection", (socket) => { // 새 소켓 연결을 수신합니다.
console.log("New socket connection");
// 사용자가 채팅방에 참여할 때 실행되는 핸들러
socket.on("join_room", async ({ roomId, userId }) => {
console.log(`User ${userId} joined room ${roomId}`);
socket.join(roomId); // 소켓을 해당 채팅방에 추가합니다.
// 데이터베이스에서 해당 채팅방의 메시지를 가져옵니다.
const messages = await prisma.chatMessages.findMany({
where: { chatroom_id: Number(roomId) },
orderBy: { created_at: "asc" },
});
socket.emit("load_messages", messages); // 클라이언트에 메시지를 전송합니다.
});
// 사용자가 메시지를 전송할 때 실행되는 핸들러
socket.on("send_message", async ({ roomId, userId, message }) => {
// 새 메시지를 데이터베이스에 저장합니다.
const newMessage = await prisma.chatMessages.create({
data: {
chatroom_id: Number(roomId),
user_id: userId ? Number(userId) : null,
message,
created_at: new Date(),
},
});
// 채팅방의 모든 사용자에게 새 메시지를 전송합니다.
io.to(roomId).emit("new_message", newMessage);
});
});
}
res.end(); // 응답을 종료합니다.
};
export default SocketHandler;
1-1 소켓 서버 초기화 및 연결 이벤트 처리
if (!res.socket.server.io) { // 서버에 소켓이 초기화되지 않은 경우
const io = new Server(res.socket.server, { path: "/api/community/socket" }); // 새 소켓 서버를 생성합니다.
res.socket.server.io = io;
io.on("connection", (socket) => { // 새 소켓 연결을 수신합니다.
console.log("New socket connection");
});
}
서버에 소켓 서버가 초기화되지 않은 경우, 새 소켓 서버를 생성하고 연결 이벤트를 처리합니다.
1-2 사용자가 메시지를 전송할 때
socket.on("send_message", async ({ roomId, userId, message }) => {
const newMessage = await prisma.chatMessages.create({
data: {
chatroom_id: Number(roomId),
user_id: userId ? Number(userId) : null,
message,
created_at: new Date(),
},
});
io.to(roomId).emit("new_message", newMessage); // 채팅방의 모든 사용자에게 새 메시지를 전송합니다.
});
사용자가 메시지를 전송할 때 실행되며, 새 메시지를 데이터베이스에 저장하고 채팅방의 모든 사용자에게 전송합니다.
