오늘은 여러 클라이언트가 접속하여 통신할 수 있는 웹 소켓 채팅 서비스를 구현하였다.
파일 구조
BukJeokBukJeok
├─ node_modules
├─ public
│ ├─ css
│ │ └─ style.css
│ ├─ js
│ │ └─ main.js
│ ├─ chat.html
│ └─ index.html
├─ utils
│ ├─ messages.js
│ └─ users.js
├─ server.js
├─ package-lock.json
└─ package.json
소스코드 설명
package.json
{
...
"main": "server.js",
"scripts": {
"start": "node server",
"dev": "nodemon server"
},
...
"license": "MIT",
...
"dependencies": {
"express": "^4.18.2",
"moment": "^2.29.4",
"socket.io": "^4.5.4"
},
"devDependencies": {
"nodemon": "^2.0.20"
}
}
server.js
var os = require('os');
const path = require('path');
const http = require('http');
const express = require('express');
const socketio = require('socket.io');
const fromatMessage = require('./utils/messages');
const { userJoin, getCurrentUser, userLeave, getRoomUsers } = require('./utils/users');
os모듈은 운영체제와 시스템의 정보를 가져올 수 있는 모듈이다.
path모듈은 express 설정할 때 클라이언트 폴더의 주소를 세팅하기 위한 모듈이다.
htpp모듈은 http서버를 사용하기 위한 모듈이다.
express 모듈은 http 모듈에 여러 기능을 추가해서 쉽게 사용할 수 있게 만든 모듈이다.
socket.io모듈은 웹소켓을 열어 서버와 클라이언트 간의 소켓 통신을 하기 위한 모듈이다.formatMessage는 메세지 형식을 포함하고 있다.
userJoin은 연결된 클라이언트를 리스트에 삽입하는 함수이다.
getCurrentUser은 해당 socket id에 맞는 유저 정보를 반환하는 함수이다.
userLeave는 소켓 통신이 해재된 클라이언트의 정보를 반환하는 함수이다.
getRoomUsers는 해당 채팅방에 접속된 클라이언트들의 정보를 반환하는 함수이다.
const app = express();
const server = http.createServer(app);
const io = socketio(server);
app.use(express.static(path.join(__dirname, 'public')));
const Announcement = '공지';
__dirname : 현재 폴더 경로
join : 운영체제에 맞춰 경로 지정하기
express.static
- express 변수에 포함된 stastic이라는 메서드를 미들웨어로서 로드해준다
- static의 인자로 전달되는 파일경로는 밑에 있는 데이터들은 웹브라우저의 요청에 따라 서비스를 제공해줄 수 있다
const PORT = 3000 || process.env.PORT;
server.listen(PORT, () => {
console.log(`Server has been running..!`);
console.log(`[Server address]\n- http://${getIp()}:${PORT}`);
console.log(`- http://127.0.0.1:${PORT}`);
console.log(`- http://localhost:${PORT}`);
});
포트를 3000으로 지정 후 PORT 3000에 서버를 실행
getIp() : IPv4주소를 찾아 반환
function getIp() {
var ifaces = os.networkInterfaces();
var ip = '';
for (var dev in ifaces) {
var alias = 0;
ifaces[dev].forEach(function(details) {
if (details.family == 'IPv4' && details.internal === false) {
ip = details.address;
++alias;
}
});
}
return ip;
}
io.on('connection', socket => {
socket.on('joinRoom', ({ username, room }) => {
const user = userJoin(socket.id, username, room);
socket.join(user.room);
socket.emit('message', fromatMessage(Announcement, '북적북적에 오신것을 환영합니다!!'));
socket.broadcast.to(user.room).emit('message', fromatMessage(Announcement, `${user.username}님이 입장하셨습니다.`));
io.to(user.room).emit('roomUsers', {
room: user.room,
users: getRoomUsers(user.room)
});
});
socket.on('chatMessage', (msg) => {
const user = getCurrentUser(socket.id);
io.to(user.room).emit('message', fromatMessage(user.username, msg));
});
socket.on('disconnect', () => {
const user = userLeave(socket.id);
if(user) {
io.to(user.room).emit('message', fromatMessage(Announcement, `${user.username}님이 퇴장하셨습니다.`));
}
});
});
클라이언트가 연결되면 클라이언트 정보를 리스트에 담는 함수를 호출한다.
클라이언트로부터 메세지가 오면 그 클라이언트 정보를 얻는 함수를 호출 후 메세지를 보낸다.
연결이 해제된 클라이언트가 있을 경우 연결이 해제된 클라이언트 정보를 가져오는 함수를 호출한다.
main.js
const chatForm = document.getElementById('chat-form');
const chatMessages = document.querySelector('.chat-messages');
const roomName = document.getElementById('room-name');
const userList = document.getElementById('users');
chatForm은 메세지를 보내는 버튼이다.
chatMessages는 채팅창이다.
roomName은 채팅방 이름이다.
userList는 접속한 클라이언트 리스트를 제어하는 모듈이다
const { username, room } = Qs.parse(location.search, {
ignoreQueryPrefix: true
});
클라이언트가 접속하면 url에 정보가 표시되므로 그 url을 통하여 클라이언트 정보를 들고온다.
const socket = io();
socket.emit('joinRoom', {username, room });
서버에게 클라이언트 정보를 보내며 접속을 알린다.
socket.on('roomUsers', ({ room, users }) => {
outputRoomName(room);
outputUsers(users);
});
내가 접속한 채팅방의 이름을 표시하고 현재 접속되어 있는 클라이언트 이름을 표시하는 함수를 호출한다.
function outputRoomName(room) {
roomName.innerText = room;
}
채팅방 이름을 HTML로 표시한다.
function outputUsers(users) {
userList.innerHTML = `
${users.map(user => `<li>${user.username}</li>`).join('')}
`;
}
내가 접속한 채팅방에 접속되어 있는 클라이언트 이름을 HTML로 표시한다.
socket.on('message', message => {
console.log(message);
outputMessage(message);
chatMessages.scrollTop = chatMessages.scrollHeight;
});
서버로 부터 받은 메세리를 HTML로 표시하는 함수를 호출후 채팅창 스크롤을 가장 아래로 한다.
function outputMessage(message) {
const div = document.createElement('div');
div.classList.add('message');
div.innerHTML = `
<p class="meta">${message.name} <span>${message.time}</span></p>
<p class="text">${message.text}</p>`;
document.querySelector('.chat-messages').appendChild(div);
}
서버로 부터 받은 메세지를 HTML로 표시한다.
chatForm.addEventListener('submit', (e) => {
e.preventDefault();
const msg = e.target.elements.msg.value;
socket.emit('chatMessage', msg);
e.target.elements.msg.value = '';
e.target.elements.msg.focus();
});
클라이언트가 메세지를 보내려면 서버로 메세지를 보낸 후 입력란의 값을 초기화하고 다시 입력을 대기한다.
messages.js
const moment = require('moment');
날짜를 불러오는 라이브러리이다.
function fromatMessage(name, text) {
return {
name,
text,
time: moment().format('h:mm a')
}
}
메세지 속성에 시간을 추가하여 반환하는 함수이다.
module.exports = fromatMessage;
해당 모듈을 require한다면 fromatMessage함수가 반환되도록 설정한다.
users.js
const users = [];
클라이언트 정보를 담을 리스트이다.
function userJoin(id, username, room) {
const user = { id, username, room };
users.push(user);
return user;
}
접속된 클라이언트 정보를 리스트에 담고 반환한다.
function getCurrentUser(id) {
return users.find(user => user.id === id);
}
socket id에 맞는 클라이언트를 찾아 그 클라이언트 정보를 반환한다.
function userLeave(id) {
const index = users.findIndex(user => user.id === id);
if(index !== -1) {
return users.splice(index, 1)[0];
}
}
연결 해제된 클라이언트를 찾아 그 클라이언트 정보를 반환한다.
function getRoomUsers(room) {
return users.filter(user => user.room === room);
}
인자로 받은 채팅방에 접속되어 있는 클라이언트들의 정보를 반환한다.
module.exports = {
userJoin,
getCurrentUser,
userLeave,
getRoomUsers
}
해당 모듈을 require한다면 userJoin, getCurrentUser, userLeave, getRoomUsers함수가 반환되도록 설정한다.
전채 소스코드
server.js
var os = require('os');
const path = require('path');
const http = require('http');
const express = require('express');
const socketio = require('socket.io');
const fromatMessage = require('./utils/messages');
const { userJoin, getCurrentUser, userLeave, getRoomUsers } = require('./utils/users');
const app = express();
const server = http.createServer(app);
const io = socketio(server);
app.use(express.static(path.join(__dirname, 'public')));
const Announcement = '공지';
io.on('connection', socket => {
socket.on('joinRoom', ({ username, room }) => {
const user = userJoin(socket.id, username, room);
socket.join(user.room);
socket.emit('message', fromatMessage(Announcement, '북적북적에 오신것을 환영합니다!!'));
socket.broadcast.to(user.room).emit('message', fromatMessage(Announcement, `${user.username}님이 입장하셨습니다.`));
io.to(user.room).emit('roomUsers', {
room: user.room,
users: getRoomUsers(user.room)
});
});
socket.on('chatMessage', (msg) => {
const user = getCurrentUser(socket.id);
io.to(user.room).emit('message', fromatMessage(user.username, msg));
});
socket.on('disconnect', () => {
const user = userLeave(socket.id);
if(user) {
io.to(user.room).emit('message', fromatMessage(Announcement, `${user.username}님이 퇴장하셨습니다.`));
}
});
});
const PORT = 3000 || process.env.PORT;
server.listen(PORT, () => {
console.log(`Server has been running..!`);
console.log(`[Server address]\n- http://${getIp()}:${PORT}`);
console.log(`- http://127.0.0.1:${PORT}`);
console.log(`- http://localhost:${PORT}`);
});
function getIp() {
var ifaces = os.networkInterfaces();
var ip = '';
for (var dev in ifaces) {
var alias = 0;
ifaces[dev].forEach(function(details) {
if (details.family == 'IPv4' && details.internal === false) {
ip = details.address;
++alias;
}
});
}
return ip;
}
main.js
const chatForm = document.getElementById('chat-form');
const chatMessages = document.querySelector('.chat-messages');
const roomName = document.getElementById('room-name');
const userList = document.getElementById('users');
const { username, room } = Qs.parse(location.search, {
ignoreQueryPrefix: true
});
const socket = io();
socket.emit('joinRoom', {username, room });
socket.on('roomUsers', ({ room, users }) => {
outputRoomName(room);
outputUsers(users);
});
socket.on('message', message => {
console.log(message);
outputMessage(message);
chatMessages.scrollTop = chatMessages.scrollHeight;
});
chatForm.addEventListener('submit', (e) => {
e.preventDefault();
const msg = e.target.elements.msg.value;
socket.emit('chatMessage', msg);
e.target.elements.msg.value = '';
e.target.elements.msg.focus();
});
function outputMessage(message) {
const div = document.createElement('div');
div.classList.add('message');
div.innerHTML = `
<p class="meta">${message.name} <span>${message.time}</span></p>
<p class="text">${message.text}</p>`;
document.querySelector('.chat-messages').appendChild(div);
}
function outputRoomName(room) {
roomName.innerText = room;
}
function outputUsers(users) {
userList.innerHTML = `
${users.map(user => `<li>${user.username}</li>`).join('')}
`;
}
messages.js
const moment = require('moment');
function fromatMessage(name, text) {
return {
name,
text,
time: moment().format('h:mm a')
}
}
module.exports = fromatMessage;
users.js
const users = [];
function userJoin(id, username, room) {
const user = { id, username, room };
users.push(user);
return user;
}
function getCurrentUser(id) {
return users.find(user => user.id === id);
}
function userLeave(id) {
const index = users.findIndex(user => user.id === id);
if(index !== -1) {
return users.splice(index, 1)[0];
}
}
function getRoomUsers(room) {
return users.filter(user => user.room === room);
}
module.exports = {
userJoin,
getCurrentUser,
userLeave,
getRoomUsers
}
index.html
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.12.1/css/all.min.css"
integrity="sha256-mmgLkCYLUQbXn0B1SRqzHar6dCnv9oZFPEC1g1cwlkk="
crossorigin="anonymous"
/>
<link rel="stylesheet" href="css/style.css" />
<title>북적북적</title>
</head>
<body>
<div class="join-container">
<header class="join-header">
<h1>북적북적</h1>
</header>
<main class="join-main">
<form action="chat.html">
<div class="form-control">
<label for="username">이름</label>
<input
type="text"
name="username"
id="username"
placeholder="이름을 입력하세요."
onfocus="this.placeholder=''"
onblur="this.placeholder='이름을 입력하세요.'"
required
/>
</div>
<div class="form-control">
<label for="room">채팅방</label>
<select name="room" id="room">
<option value="ROOM#556-1">ROOM#556-1</option>
<option value="ROOM#556-2">ROOM#556-2</option>
<option value="ROOM#556-3">ROOM#556-3</option>
<option value="ROOM#556-4">ROOM#556-4</option>
<option value="ROOM#556-5">ROOM#556-5</option>
<option value="ROOM#556-6">ROOM#556-6</option>
</select>
</div>
<button type="submit" class="btn">완료</button>
</form>
</main>
</div>
</body>
</html>
chat.html
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.12.1/css/all.min.css" integrity="sha256-mmgLkCYLUQbXn0B1SRqzHar6dCnv9oZFPEC1g1cwlkk=" crossorigin="anonymous" />
<link rel="stylesheet" href="css/style.css">
<title>북적북적</title>
</head>
<body>
<div class="chat-container">
<header class="chat-header">
<h1> 북적북적</h1>
<a href="index.html" class="btn">나가기</a>
</header>
<main class="chat-main">
<div class="chat-sidebar">
<h3><i class="fas fa-comments"></i> 채팅방</h3>
<h2 id="room-name"></h2>
<h3><i class="fas fa-users"></i> 접속중</h3>
<ul id="users"></ul>
</div>
<div class="chat-messages"></div>
</main>
<div class="chat-form-container">
<form id="chat-form">
<input
id="msg"
type="text"
placeholder="메세지를 입력하세요."
onfocus="this.placeholder=''"
onblur="this.placeholder='메세지를 입력하세요.'"
required
autocomplete="off"
/>
<button class="btn"><i class="fas fa-paper-plane"></i> 전송</button>
</form>
</div>
</div>
<script
src="https://cdnjs.cloudflare.com/ajax/libs/qs/6.11.0/qs.min.js"
integrity="sha512-/l6vieC+YxaZywUhmqs++8uF9DeMvJE61ua5g+UK0TuHZ4TkTgB1Gm1n0NiA86uEOM9JJ6JUwyR0hboKO0fCng=="
crossorigin="anonymous"
referrerpolicy="no-referrer"></script>
<script src="/socket.io/socket.io.js"></script>
<script src="js/main.js"></script>
</body>
</html>
style.css
@import url('https://fonts.googleapis.com/css?family=Roboto&display=swap');
:root {
--dark-color-a: #667aff;
--dark-color-b: #7386ff;
--light-color: #ababab;
--success-color: #5cb85c;
--error-color: #d9534f;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Roboto', sans-serif;
font-size: 16px;
background: #f6f6f7;
margin: 20px;
}
ul {
list-style: none;
}
a {
text-decoration: none;
}
.btn {
cursor: pointer;
padding: 5px 15px;
background: #3750de;
font-weight: 600;
color: #ffffff;
border: solid;
border-width: 2.5px;
border-color: #3750de;
font-size: 17px;
}
.btn:hover {
background: #132172;
color: #3750de;
}
/* Chat Page */
.chat-container {
max-width: 1100px;
background: #fff;
margin: 30px auto;
overflow: hidden;
box-shadow: 0 20px 34px rgba(170, 170, 170, 0.25), 0 16px 16px rgba(170, 170, 170, 0.25);
}
.chat-header {
background: #1d3dca;
color: #fff;
border-top-left-radius: 5px;
border-top-right-radius: 5px;
padding: 15px;
display: flex;
align-items: center;
justify-content: space-between;
}
.chat-main {
display: grid;
grid-template-columns: 1fr 3fr;
}
.chat-sidebar {
background: #3750de;
color: #fff;
padding: 20px 20px 60px;
overflow-y: scroll;
}
.chat-sidebar h2 {
font-size: 20px;
background: rgba(0, 0, 0, 0.1);
padding: 10px;
margin-bottom: 20px;
}
.chat-sidebar h3 {
margin-bottom: 15px;
}
.chat-sidebar ul li {
padding: 10px 0;
}
.chat-messages {
padding: 30px;
max-height: 500px;
overflow-y: scroll;
}
.chat-messages .message {
padding: 10px;
margin-bottom: 15px;
background-color: #3750de;
border-radius: 5px;
box-shadow: 0 12px 26px rgba(170, 170, 170, 0.25), 0 8px 8px rgba(170, 170, 170, 0.25);
}
.chat-messages .message .meta {
font-size: 16px;
font-weight: bold;
color: #abb8ff;
margin-bottom: 7px;
}
.chat-messages .message .meta span {
font-size: 12px;
color: #8799ff;
}
.chat-messages .message .text {
color: #ffffff;
font-size: 14px;
}
.chat-form-container {
padding: 20px 30px;
background-color: #1d3dca;
}
.chat-form-container form {
display: flex;
}
.chat-form-container input[type='text'] {
font-size: 16px;
padding: 5px;
height: 40px;
flex: 1;
border: none;
outline: none;
}
/* Join Page */
.join-container {
max-width: 500px;
margin: 80px auto;
color: #fff;
box-shadow: 0 20px 34px rgba(170, 170, 170, 0.25), 0 16px 16px rgba(170, 170, 170, 0.25);
}
.join-header {
text-align: center;
padding: 20px;
background: #1d3dca;
}
.join-main {
padding: 30px 40px;
background: #f6f6f7;
}
.join-main p {
margin-bottom: 20px;
}
.join-main .form-control {
margin-bottom: 20px;
}
.join-main label {
display: block;
margin-bottom: 5px;
color: #1d3dca;
font-weight: bold;
}
.join-main input[type='text'] {
font-size: 16px;
padding: 5px;
height: 40px;
width: 100%;
outline: none;
border: solid;
border-color: #1d3dca;
border-radius: 10px;
background: none;
}
.join-main select {
font-size: 16px;
padding: 5px;
height: 40px;
width: 100%;
outline: none;
border: solid;
border-color: #1d3dca;
border-radius: 10px;
background: none;
}
.join-main .btn {
margin-top: 20px;
width: 100%;
height: 40px;
font-weight: bold;
color: #ffffff;
border: solid;
border-width: 2.5px;
background: #1d3dca;
border-color: #1d3dca;
border-radius: 10px;
}
.join-main .btn:hover {
color: #1d3dca;
background: #ffffff;
}
@media (max-width: 700px) {
.chat-main {
display: block;
}
.chat-sidebar {
display: none;
}
}
실행 화면
Terminal
Web screen
소스코드 링크
'NodeJS' 카테고리의 다른 글
Nodejs 로그인&회원가입 구현 (2) | 2023.02.03 |
---|---|
Node.js 간단한 웹 채팅 (2) | 2023.01.26 |