실시간 채팅 웹 만들기(nextjs, nodejs, soketio)

정해준·2024년 9월 27일

오늘은 실시간 채팅을 만들어 봤습니다.

프론트는 nextjs로 구현하였고, 백엔드는 nodejs의 express를 활용하여 구현하였습니다.

저는 프론트를 중점적으로 배웠고 이번에 처음으로 백엔드를 구현해서 서버를 구현하였기 때문에 코드에 오류가 있을 수 있습니다. 그럴경우 알려주시면 감사하겠습니다.
Github Repo
참고한 영상

백엔드

nodejs의 express서버 세팅 및 soket.io setting입니다

express 및 soket.io server 세팅

//index.js
const express = require("express");
const mongoose = require("mongoose");
require("dotenv").config();
const { Server } = require("socket.io");
const http = require("http");
const cors = require("cors");

const PORT = process.env.PORT;

const app = express();

mongoose
  .connect(process.env.DB)
  .then(() => console.log("connected to database"));

const httpServer = http.createServer(app);

// CORS 설정: 모든 출처를 허용
app.use(cors({ origin: "*" }));

const io = new Server(httpServer, {
  cors: {
    origin: "*", // 모든 출처 허용
    methods: ["GET", "POST"],
    credentials: true, // 쿠키 전송 허용 시 설정
  },
});

require("./util/io")(io);

// 서버 시작
httpServer.listen(PORT, () =>
  console.log(`서버가 ${PORT}에서 시작되었습니다.`)
);

정보 저장을 위해 mongoose를 사용하여 mongoDB를 사용했습니다.
express 서버 설정 및 cors, soket.io를 세팅 후 soket 서버를 넘긴 후 파일을 분리 하였습니다.

soket 통신 세팅

//util/io.js
const chatController = require("../Controllers/chat.controller");
const userController = require("../Controllers/user.controller");

function socket(io) {
  io.on("connection", async (socket) => {
    console.log("새로운 유저가 접속했습니다.", socket.id);

    // 유저가 처음 로그인 했을 때
    socket.on("login", async (user, cb) => {
      try {
        const userData = await userController.saveUser(user, socket.id);
        const welcomeMessage = {
          chat: `${user}이 참여하였습니다.`,
          user: { id: null, name: "system" },
        };
        io.emit("message", welcomeMessage);
        io.emit("user", userData);
        cb({ ok: true, data: userData });
      } catch (error) {
        cb({ ok: false, error: error.message });
      }
    });

    //메세지를 보냈을 때
    socket.on("sendMessage", async (message, cb) => {
      try {
        const user = await userController.checkUser(socket.id);

        const newMessage = await chatController.saveChat(message, user);

        io.emit("message", newMessage);
        cb({ ok: true });
      } catch (error) {
        cb({ ok: false, error: error.message });
      }
    });

    // 연결을 종료했을 때
    
    socket.on("disconnect", async (cb) => {
      const user = await userController.checkUser(socket.id);

      const outMessage = {
        chat: `${user.name}이 접속을 종료하였습니다..`,
        user: { id: null, name: "system" },
      };

      io.emit("message", outMessage);
    });
  });
}

module.exports = socket;

백엔드 소켓통신 부분은 이렇게 구현하였습니다.

프론트

프론트는 nextjs와 socket.io-client를 사용하여 구현했습니다.

"use client";

import React, { useState, useEffect, FormEvent } from "react";
import { useSearchParams } from "next/navigation";

import socket from "@/util/server";

import InfoBar from "@/components/InfoBar/InfoBar";
import Messages from "@/components/Messages/Messages";
import Input from "@/components/Input/Input";
import TextContainer from "@/components/TextContainer/TextContainer";

import "./Chat.css";

import type { Message, socketLoginRes, User } from "@/types";

const Chat = () => {
  const [users, setUsers] = useState<User[]>([]);
  const [message, setMessage] = useState("");
  const [messages, setMessages] = useState<Message[]>([]);

  const params = useSearchParams();
  const name = params.get("name");

  useEffect(() => {
    if (!name) return; // 이름이 없을 때는 실행하지 않음

    // 로그인 이벤트 등록
    socket.emit("login", name, (res: socketLoginRes) => {
      if (res.ok) {
        setUsers((prev) => {
          // 중복 사용자 제거
          if (prev.find((user) => user.name === res.data.name)) return prev;
          return [...prev, res.data];
        });
      }
    });

    // 사용자 정보 및 메시지 수신 핸들러
    const handleUser = (userDate: User) => {
      setUsers((prev) => {
        // 중복 사용자 제거
        if (prev.find((user) => user.name === userDate.name)) return prev;
        return [...prev, userDate];
      });
    };

    const handleMessage = (message: { user: User; chat: string }) => {
      setMessages((prev) => {
        //중복채팅 제거
        if (
          prev.find(
            (prev) =>
              prev.text === message.chat && prev.user === message.user.name
          )
        )
          return prev;
        return [...prev, { user: message.user.name, text: message.chat }];
      });
    };

    // 소켓 이벤트 리스너 등록
    socket.on("user", handleUser);
    socket.on("message", handleMessage);

    // Cleanup: 컴포넌트가 언마운트될 때 기존 리스너 제거
    return () => {
      socket.off("user", handleUser);
      socket.off("message", handleMessage);
    };
  }, [name]); // name이 바뀔 때만 실행

  const sendMessage = (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    if (message) {
      socket.emit("sendMessage", message, (res: any) => {
        console.log(res);
      });
      setMessage(""); // 전송 후 메시지 초기화
    }
  };

  return (
    <div className="outerContainer">
      <div className="container">
        <InfoBar />
        <Messages messages={messages} name={name!} />
        <Input
          message={message}
          setMessage={setMessage}
          sendMessage={sendMessage}
        />
      </div>
      <TextContainer users={users} />
    </div>
  );
};

export default Chat;

위의 조건은 렌더링이 되면서 값이 2번씩 들어가서 조건을 작성하였습니다.
채팅에도 똑같은 조건이 들어가서 중복채팅이 들어가지 않습니다.

나머지 필요하신 코드가 있으면 위에 있는 github repo를 참고해 주세요

0개의 댓글