오늘은 실시간 채팅을 만들어 봤습니다.
프론트는 nextjs로 구현하였고, 백엔드는 nodejs의 express를 활용하여 구현하였습니다.
저는 프론트를 중점적으로 배웠고 이번에 처음으로 백엔드를 구현해서 서버를 구현하였기 때문에 코드에 오류가 있을 수 있습니다. 그럴경우 알려주시면 감사하겠습니다.
Github Repo
참고한 영상
nodejs의 express서버 세팅 및 soket.io setting입니다
//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 서버를 넘긴 후 파일을 분리 하였습니다.
//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를 참고해 주세요