그룹채팅 - 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); // 채팅방의 모든 사용자에게 새 메시지를 전송합니다.
});

사용자가 메시지를 전송할 때 실행되며, 새 메시지를 데이터베이스에 저장하고 채팅방의 모든 사용자에게 전송합니다.