채팅 구현하기
프런트에서는 서버에서 보내는 채팅 데이터를 받을 소켓 이벤트 리스너가 필요합니다. chat.html 파일에 추가합니다.
{% extends 'layout.html' %} {% block content %}
<h1>{{title}}</h1>
<a href="/" id="exit-btn">방 나가기</a>
<fieldset>
<legend>채팅 내용</legend>
<div id="chat-list">
{% for chat in chats %} {% if chat.user === user %}
<div class="mine" style="color: {{chat.user}}">
<div>{{chat.user}}</div>
{% if chat.gif %}
<img src="/gif/{{chat.gif}}" />
{% else %}
<div>{{chat.chat}}</div>
{% endif %}
</div>
{% elif chat.user === 'system' %}
<div class="system">
<div>{{chat.chat}}</div>
</div>
{% else %}
<div class="other" style="colo: {{chat.user}}">
<div>{{chat.user}}</div>
{% if chat.gif %}
<img src="/gif/{{chat.gif}}" />
{% else %}
<div>{{chat.chat}}</div>
{% endif %}
</div>
{% endif %} {% endfor %}
</div>
</fieldset>
<form action="/chat" id="chat-form" method="post" enctype="multipart/form-data">
<label for="gif">GIF 올리기</label>
<input type="file" id="gif" name="gif" accept="image/gif" />
<input type="text" id="chat" name="chat" />
<button type="submit">전송</button>
</form>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script>
const socket = io.connect("http://localhost:8005/chat", {
path: "/socket.io",
});
socket.on("join", function (data) {
const div = document.createElement("div");
div.classList.add("system");
const chat = document.createElement("div");
div.textContent = data.chat;
div.appendChild(chat);
document.querySelector("#chat-list").appendChild(div);
});
socket.on("exit", function (data) {
const div = document.createElement("div");
div.classList.add("system");
const chat = document.createElement("div");
div.textContent = data.chat;
div.appendChild(chat);
document.querySelector("#chat-list").appendChild(div);
});
socket.on("chat", function (data) {
const div = document.createElement("div");
if (data.user === "{{user}}") {
div.classList.add("mine");
} else {
div.classList.add("other");
}
const name = document.createElement("div");
name.textContent = data.user;
div.appendChild(name);
if (data.chat) {
const chat = document.createElement("div");
chat.textContent = data.chat;
div.appendChild(chat);
} else {
const gif = document.createElement("img");
gif.src = "/gif/" + data.gif;
div.appendChild(gif);
}
div.style.color = data.user;
document.querySelector("#chat-list").appendChild(div);
});
document.querySelector("#chat-form").addEventListener("submit", function (e) {
e.preventDefault();
if (e.target.chat.value) {
axios
.post("/room/{{room._id}}/chat", {
chat: this.chat.value,
})
.then(() => {
e.target.chat.value = "";
})
.catch((err) => {
console.error(err);
});
}
});
</script>
{% endblock %}
socket에 chat 이벤트 리스너를 추가했습니다. chat 이벤트는 채팅 메시지가 웹 소켓으로 전송될 때 호출됩니다. 채팅 메시지 발송자(data.user)에 따라 내 메시지(mine 클래스)인지 확인한 후 그에 맞게 렌더링합니다. 채팅을 전송하는 폼에 submit 이벤트 리스너도 추가했습니다.
채팅은 여러 가지 방식으로 구현할 수 있습니다. 현재 GIF 채팅방의 경우에는 채팅 내용을 데이터베이스에 저장하므로 라우터를 거치도록 설계했습니다.
이제 방에 접속하는 부분과 채팅을 하는 부분을 만들어보겠습니다.
const express = require("express");
const Room = require("../schemas/room");
const Chat = require("../schemas/chat");
const router = express.Router();
router.get("/", async (req, res, next) => {
try {
const rooms = await Room.find({});
res.render("main", { rooms, title: "GIF 채팅방" });
} catch (error) {
console.error(error);
next(error);
}
});
router.get("/room", (req, res) => {
res.render("room", { title: "GIF 채팅방 생성" });
});
router.post("/room", async (req, res, next) => {
try {
const newRoom = await Room.create({
title: req.body.title,
max: req.body.max,
owner: req.session.color,
password: req.body.password,
});
const io = req.app.get("io");
io.of("/room").emit("newRoom", newRoom);
res.redirect(`/room/${newRoom._id}?password=${req.body.password}`);
} catch (error) {
console.error(error);
next(error);
}
});
router.get("/room/:id", async (req, res, next) => {
try {
const room = await Room.findOne({ _id: req.params.id });
const io = req.app.get("io");
if (!room) {
return res.redirect("/?error=존재하지 않는 방입니다.");
}
if (room.password && room.password !== req.query.password) {
return res.redirect("/?error=비밀번호가 틀렸습니다.");
}
const { rooms } = io.of("/chat").adapter;
if (
rooms &&
rooms[req.params.id] &&
room.max <= rooms[req.params.id].length
) {
return res.redirect("/?error=인원이 가득 찬 방입니다.");
}
const chats = await Chat.find({ room: room._id }).sort("createdAt");
return res.render("chat", {
room,
title: room.title,
chats,
user: req.session.color,
});
} catch (error) {
console.error(error);
next(error);
}
});
router.delete("/room/:id", async (req, res, next) => {
try {
await Room.remove({ _id: req.params.id });
await Chat.remove({ room: req.params.id });
res.send("ok");
setTimeout(() => {
req.app;
}, 2000);
} catch (error) {
console.error(error);
next(error);
}
});
router.post("/room/:id/chat", async (req, res, next) => {
try {
const chat = await Chat.create({
room: req.params.id,
user: req.session.color,
chat: req.body.chat,
});
req.app.get("io").of("/chat").to(req.params.id).emit("chat", chat);
res.send("ok");
} catch (error) {
console.error(error);
next(error);
}
});
module.exports = router;
먼저 GET /room/:id 라우터에서 방 접속 시 기준 해팅 내역을 불러오도록 수정합니다. 방에 접속할 때는 DB로부터 채팅 내역을 가져오고, 접속 후에는 웹 소켓으로 새로운 체팅 메시지를 받습니다.
POST /room/:id/chat 라우터를 새로 생성합니다. 채팅을 데이터베이스에 저장한 후 io.of('/chat').to(방 아이디).emit으로 같은 방에 들어 있는 소켓들에게 메시지 데이터를 전송합니다.
이제 채팅을 할 수 있습니다. 채팅을 할 때마다 채팅 내용이 POST /room/:id/chat 라우터로 전송되고, 라우터에서 다시 웹 소켓으로 메시지를 보냅니다.

퇴장 시에는 chat.on('disconnect') 이벤트 리스너를 다른 참여자에게 exit 이벤트를 호출합니다.

'프로그래밍 언어 > NODE JS' 카테고리의 다른 글
| 실시간 경매 시스템 만들기 - 프로젝트 구조 갖추기(1) (0) | 2026.02.13 |
|---|---|
| 프로젝트 마무리하기 (0) | 2026.02.10 |
| 미들웨어와 소켓 연결하기(2) (0) | 2026.02.04 |
| 미들웨어와 소켓 연결하기 (0) | 2026.02.01 |
| 실시간 GIF 채팅방 만들기(4) (0) | 2026.01.29 |