๐12์ฅ ํค์๋๐
์น ์์ผ
socket.io
ws
์๋ฐฉํฅ
๋จ๋ฐฉํฅ
์น ์์ผ:
ํด๋ง:
์๋ฒ์ผํธ ์ด๋ฒคํธ SSE:
Socket.IO
gif-chat์ด๋ผ๋ ์๋ก์ด ํ๋ก์ ํธ ์์ฑ, ์๋์ jsonํ์ผ ์์ฑ ํ
npm i ๋ก ํจํค์ง ์ค์น, env, app.js, index.js ํ์ผ ์์ฑ
// .env
COOKIE_SECRET=gifchat
// app.js
const express = require('express');
const path = require('path');
const morgan = require('morgan');
const cookieParser = require('cookie-parser');
const session = require('express-session');
const nunjucks = require('nunjucks');
const dotenv = require('dotenv');
dotenv.config();
const indexRouter = require('./routes');
const app = express();
app.set('port', process.env.PORT || 8005);
app.set('view engine', 'html');
nunjucks.configure('views', {
express: app,
watch: true,
});
app.use(morgan('dev'));
app.use(express.static(path.join(__dirname, 'public')));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser(process.env.COOKIE_SECRET));
app.use(session({
resave: false,
saveUninitialized: false,
secret: process.env.COOKIE_SECRET,
cookie: {
httpOnly: true,
secure: false,
},
}));
app.use('/', indexRouter);
app.use((req, res, next) => {
const error = new Error(`${req.method} ${req.url} ๋ผ์ฐํฐ๊ฐ ์์ต๋๋ค.`);
error.status = 404;
next(error);
});
app.use((err, req, res, next) => {
res.locals.message = err.message;
res.locals.error = process.env.NODE_ENV !== 'production' ? err : {};
res.status(err.status || 500);
res.render('error');
});
app.listen(app.get('port'), () => {
console.log(app.get('port'), '๋ฒ ํฌํธ์์ ๋๊ธฐ์ค');
});
// routes.index.js
const express = require('express');
const router = express.Router();
router.get('/', (req, res) => {
res.render('index');
});
module.exports = router;
npm i ws@8 ๋ก ws๋ชจ๋ ์ค์น, app.js ํ์ผ ์์ ํด์ ์น ์์ผ์ ์ต์คํ๋ ์ค ์๋ฒ์ ์ฐ๊ฒฐ
// app.js
const express = require('express');
const path = require('path');
const morgan = require('morgan');
const cookieParser = require('cookie-parser');
const session = require('express-session');
const nunjucks = require('nunjucks');
const dotenv = require('dotenv');
dotenv.config();
const webSocket = require('./socket');
const indexRouter = require('./routes'); // ์ถ๊ฐ
const app = express();
app.set('port', process.env.PORT || 8005);
app.set('view engine', 'html');
nunjucks.configure('views', {
express: app,
watch: true,
});
app.use(morgan('dev'));
app.use(express.static(path.join(__dirname, 'public')));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser(process.env.COOKIE_SECRET));
app.use(session({
resave: false,
saveUninitialized: false,
secret: process.env.COOKIE_SECRET,
cookie: {
httpOnly: true,
secure: false,
},
}));
app.use('/', indexRouter);
app.use((req, res, next) => {
const error = new Error(`${req.method} ${req.url} ๋ผ์ฐํฐ๊ฐ ์์ต๋๋ค.`);
error.status = 404;
next(error);
});
app.use((err, req, res, next) => {
res.locals.message = err.message;
res.locals.error = process.env.NODE_ENV !== 'production' ? err : {};
res.status(err.status || 500);
res.render('error');
});
const server = app.listen(app.get('port'), () => { //์์ ๋ฐ ์ถ๊ฐ
console.log(app.get('port'), '๋ฒ ํฌํธ์์ ๋๊ธฐ์ค');
});
webSocket(server); // ์ถ๊ฐ
์น ์์ผ ๋ก์ง์ socket.jsํ์ผ์ ์์ฑ
// socket.js
const WebSocket = require('ws');
module.exports = (server) => {
const wss = new WebSocket.Server({ server });
wss.on('connection', (ws, req) => { // ์น์์ผ ์ฐ๊ฒฐ ์
const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress;
console.log('์๋ก์ด ํด๋ผ์ด์ธํธ ์ ์', ip);
ws.on('message', (message) => { // ํด๋ผ์ด์ธํธ๋ก๋ถํฐ ๋ฉ์์ง
console.log(message.toString());
});
ws.on('error', (error) => { // ์๋ฌ ์
console.error(error);
});
ws.on('close', () => { // ์ฐ๊ฒฐ ์ข
๋ฃ ์
console.log('ํด๋ผ์ด์ธํธ ์ ์ ํด์ ', ip);
clearInterval(ws.interval);
});
ws.interval = setInterval(() => { // 3์ด๋ง๋ค ํด๋ผ์ด์ธํธ๋ก ๋ฉ์์ง ์ ์ก
if (ws.readyState === ws.OPEN) {
ws.send('์๋ฒ์์ ํด๋ผ์ด์ธํธ๋ก ๋ฉ์์ง๋ฅผ ๋ณด๋
๋๋ค.');
}
}, 3000);
});
};
ws์ ์ฐ๊ฒฐ๋ ์ด๋ฒคํธ๋ฆฌ์ค๋ 3๊ฐ
setIneterval์ clearInterval๋ก ๋ฐ๊ฟ์ผ ๋ฉ๋ชจ๋ฆฌ ๋์ ๋ง์
์น ์์ผ์ ์ํ:
์ดํ index.html๊ณผ error.html ์์ฑ
// index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>GIF ์ฑํ
๋ฐฉ</title>
</head>
<body>
<div>F12๋ฅผ ๋๋ฌ console ํญ๊ณผ network ํญ์ ํ์ธํ์ธ์.</div>
<script>
const webSocket = new WebSocket("ws://localhost:8005");
webSocket.onopen = function () {
console.log('์๋ฒ์ ์น์์ผ ์ฐ๊ฒฐ ์ฑ๊ณต!');
};
webSocket.onmessage = function (event) {
console.log(event.data);
webSocket.send('ํด๋ผ์ด์ธํธ์์ ์๋ฒ๋ก ๋ต์ฅ์ ๋ณด๋
๋๋ค');
};
</script>
</body>
</html>
// error.html
<h1>{{message}}</h1>
<h2>{{error.status}}</h2>
<pre>{{error.stack}}</pre>
npm start ๋ก ์คํํ๊ณ http://localhoset:8005 ์ ์
ํฐ๋ฏธ๋์ ํตํด์ 3์ด ๊ฐ๊ฒฉ์ผ๋ก ๋ด์ฉ์ด ๋์ค๋ ๊ฒ์ ํ์ธํ ์ ์๋ค.
WS๋ชจ๋์ด Socket.IO๊ฐ ํ๋ ์ผ์ ๋ชปํ๋ ์ผ์ ๋ชปํ์ง ์์ง๋ง,
๋ณด๋ค ๋ณต์กํ ์ผ์ ์์ ์ ํ๊ธฐ์ Socket.IO๊ฐ ํธ์๊ธฐ๋ฅ์ด ๋ง์ด ์ถ๊ฐ๋์ด ๋ ์ ํฉํ๋ค.
npm i socket.io@4 ๋ช ๋ น์ด๋ฅผ ํตํด์ ์์ผ์ ์ค์นํ๋ค.
๊ทธ๋ฆฌ๊ณ socket.jsํ์ผ์ ์ฐ๊ฒฐํ๋ค.
//socket.js
const SocketIO = require('socket.io'); // ์ฃผ์
module.exports = (server) => {
const io = SocketIO(server, { path: '/socket.io' });
io.on('connection', (socket) => { // ์น์์ผ ์ฐ๊ฒฐ ์
const req = socket.request;
const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress;
console.log('์๋ก์ด ํด๋ผ์ด์ธํธ ์ ์!', ip, socket.id, req.ip);
socket.on('disconnect', () => { // ์ฐ๊ฒฐ ์ข
๋ฃ ์
console.log('ํด๋ผ์ด์ธํธ ์ ์ ํด์ ', ip, socket.id);
clearInterval(socket.interval);
});
socket.on('error', (error) => { // ์๋ฌ ์
console.error(error);
});
socket.on('reply', (data) => { // ํด๋ผ์ด์ธํธ๋ก๋ถํฐ ๋ฉ์์ง
console.log(data);
});
socket.interval = setInterval(() => { // 3์ด๋ง๋ค ํด๋ผ์ด์ธํธ๋ก ๋ฉ์์ง ์ ์ก
socket.emit('news', 'Hello Socket.IO');
}, 3000);
});
};
emit ๋ฉ์๋:
3์ด๋ง๋ค ํด๋ผ์ด์ธํธ ํ ๋ช ์๊ฒ ๋ฉ์์ง๋ฅผ ๋ณด๋ด๋ ๋ถ๋ถ. ์ด๋ฒคํธ ์ด๋ฆ, ๋ฐ์ดํฐ ๋ ๊ฐ์ง ์ธ์๋ฅผ ๋ณด๋ธ๋ค.
์๋๋ index.html ํ์ผ์ ์๋๋ค.
// index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>GIF ์ฑํ
๋ฐฉ</title>
</head>
<body>
<div>F12๋ฅผ ๋๋ฌ console ํญ๊ณผ network ํญ์ ํ์ธํ์ธ์.</div>
<script src="/socket.io/socket.io.js"></script>
<script>
const socket = io.connect('http://localhost:8005', {
path: '/socket.io',
transports: ['websocket'],
});
socket.on('news', function (data) {
console.log(data);
socket.emit('reply', 'Hello Node.JS');
});
</script>
</body>
</html>
๋ชฝ๊ณ ๋๋น๋ ๋ชฝ๊ณ ๋๋น ODM์ธ ๋ชฝ๊ตฌ์ค๋ฅผ ์ด์ฉํด gif๋ฅผ ํฌํจํ๋๋ก ์ถ๊ฐํด ์์ ๋ฐ ๋ณด์ํ๋ค.
npm i mongoose multer color-hash@2 ๋ช ๋ น์ด๋ฅผ ํตํด์
์ด๋ฏธ์ง ์ ๋ก๋์ ํ์ํ multer ์ ๋๋ค ์์์ ๊ตฌํํ color-hash ๋ชจ๋์ ์ค์นํ๋ค.
์ฑํ ๋ฐฉ ์คํค๋ง๋ ์๋์ ๊ฐ๋ค.
// room.js
const mongoose = require('mongoose');
const { Schema } = mongoose;
const roomSchema = new Schema({
title: { // ๋ฐฉ ์ ๋ชฉ
type: String,
required: true,
},
max: { // ์ธ์ ์
type: Number,
required: true,
default: 10, // ๊ธฐ๋ณธ ์ธ์
min: 2, // ์ต๋ ์ธ์
},
owner: { // ๋ฐฉ์ฅ
type: String,
required: true,
},
password: String, // ๋ฌธ์์ด
createdAt: { // ์์ฑ ์๊ฐ
type: Date,
default: Date.now,
},
});
module.exports = mongoose.model('Room', roomSchema);
๊ทธ๋ฆฌ๊ณ ์ฑํ ์คํค๋ง๋ ์๋์ ๊ฐ๋ค.
// chart.js
const mongoose = require('mongoose');
const { Schema } = mongoose;
const { Types: { ObjectId } } = Schema;
const chatSchema = new Schema({
room: {
type: ObjectId,
required: true,
ref: 'Room',
},
user: {
type: String,
required: true,
},
chat: String,
gif: String,
createdAt: {
type: Date,
default: Date.now,
},
});
module.exports = mongoose.model('Chat', chatSchema);
์๋๋ ๋ชฝ๊ณ ๋๋น๋ฅผ ์ฐ๊ฒฐํ๋ ์ฝ๋์ด๋ค.
// schemas/index.js
const mongoose = require('mongoose');
const { MONGO_ID, MONGO_PASSWORD, NODE_ENV } = process.env;
const MONGO_URL = `mongodb://${MONGO_ID}:${MONGO_PASSWORD}@localhost:27017/admin`;
const connect = () => {
if (NODE_ENV !== 'production') {
mongoose.set('debug', true);
}
mongoose.connect(MONGO_URL, {
dbName: 'gifchat',
useNewUrlParser: true,
}).then(() => {
console.log("๋ชฝ๊ณ ๋๋น ์ฐ๊ฒฐ ์ฑ๊ณต");
}).catch((err) => {
console.error("๋ชฝ๊ณ ๋๋น ์ฐ๊ฒฐ ์๋ฌ", err);
});
};
mongoose.connection.on('error', (error) => {
console.error('๋ชฝ๊ณ ๋๋น ์ฐ๊ฒฐ ์๋ฌ', error);
});
mongoose.connection.on('disconnected', () => {
console.error('๋ชฝ๊ณ ๋๋น ์ฐ๊ฒฐ์ด ๋๊ฒผ์ต๋๋ค. ์ฐ๊ฒฐ์ ์ฌ์๋ํฉ๋๋ค.');
connect();
});
module.exports = connect;
๊ทธ๋ฆฌ๊ณ ํ๊ฒฝ๋ณ์๋ ์๋์ ๊ฐ์๋ฐ, ๊ฐ์ ๋ง์ถฐ์ ๋น๋ฐ๋ฒํธ๋ฅผ ์ค์ ํ๋ฉด ๋๋ค
// .env
COOKIE_SECRET=gifchat
MONGO_ID=root
MONGO_PASSWORD=iamhungry
์๋ฒ์ ๋ชฝ๊ตฌ์ค๋ฅผ ์ฐ๊ฒฐํ๋ app.js์ด๋ค.
// app.js
const express = require('express');
const path = require('path');
const morgan = require('morgan');
const cookieParser = require('cookie-parser');
const session = require('express-session');
const nunjucks = require('nunjucks');
const dotenv = require('dotenv');
const ColorHash = require('color-hash').default;
dotenv.config();
const webSocket = require('./socket');
const indexRouter = require('./routes');
const connect = require('./schemas');
const app = express();
app.set('port', process.env.PORT || 8005);
app.set('view engine', 'html');
nunjucks.configure('views', {
express: app,
watch: true,
});
connect();
app.use(morgan('dev'));
app.use(express.static(path.join(__dirname, 'public')));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser(process.env.COOKIE_SECRET));
app.use(session({
resave: false,
saveUninitialized: false,
secret: process.env.COOKIE_SECRET,
cookie: {
httpOnly: true,
secure: false,
},
}));
app.use((req, res, next) => {
if (!req.session.color) {
const colorHash = new ColorHash();
req.session.color = colorHash.hex(req.sessionID);
console.log(req.session.color, req.sessionID);
}
next();
});
app.use('/', indexRouter);
app.use((req, res, next) => {
const error = new Error(`${req.method} ${req.url} ๋ผ์ฐํฐ๊ฐ ์์ต๋๋ค.`);
error.status = 404;
next(error);
});
app.use((err, req, res, next) => {
res.locals.message = err.message;
res.locals.error = process.env.NODE_ENV !== 'production' ? err : {};
res.status(err.status || 500);
res.render('error');
});
const server = app.listen(app.get('port'), () => {
console.log(app.get('port'), '๋ฒ ํฌํธ์์ ๋๊ธฐ์ค');
});
webSocket(server, app);
ํ๋ฉด์ ๋ ์ด์์์ ๋ํ ํ์ผ๊ณผ ์๋ฌ๊ฐ ๋์ฌ ๊ฒฝ์ฐ๋ฅผ ๋๋นํ ํ์ผ, css ํ์ผ ๋ฑ์ ๋ํ ๋ด์ฉ์ ์๋์ ๊ฐ๋ค.
// views.layout
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>{{title}}</title>
<link rel="stylesheet" href="/main.css">
</head>
<body>
{% block content %}
{% endblock %}
{% block script %}
{% endblock %}
</body>
</html>
// views/error.html
{% extends 'layout.html' %}
{% block content %}
<h1>{{message}}</h1>
<h2>{{error.status}}</h2>
<pre>{{error.stack}}</pre>
{% endblock %}
// public/ main.css
* { box-sizing: border-box; }
.mine { text-align: right; }
.system { text-align: center; }
.mine img, .other img {
max-width: 300px;
display: inline-block;
border: 1px solid silver;
border-radius: 5px;
padding: 2px 5px;
}
.mine div:first-child, .other div:first-child { font-size: 12px; }
.mine div:last-child, .other div:last-child {
display: inline-block;
border: 1px solid silver;
border-radius: 5px;
padding: 2px 5px;
max-width: 300px;
}
#exit-btn { position: absolute; top: 20px; right: 20px; }
#chat-list { height: 500px; overflow: auto; padding: 5px; }
#chat-form { text-align: right; }
label[for='gif'], #chat, #chat-form [type='submit'] {
display: inline-block;
height: 30px;
vertical-align: top;
}
label[for='gif'] { cursor: pointer; padding: 5px; }
#gif { display: none; }
table, table th, table td {
text-align: center;
border: 1px solid silver;
border-collapse: collapse;
}
// views/main.html
{% extends 'layout.html' %}
{% block content %}
<h1>GIF ์ฑํ
๋ฐฉ</h1>
<fieldset>
<legend>์ฑํ
๋ฐฉ ๋ชฉ๋ก</legend>
<table>
<thead>
<tr>
<th>๋ฐฉ ์ ๋ชฉ</th>
<th>์ข
๋ฅ</th>
<th>ํ์ฉ ์ธ์</th>
<th>๋ฐฉ์ฅ</th>
</tr>
</thead>
<tbody>
{% for room in rooms %}
<tr data-id="{{room._id}}">
<td>{{room.title}}</td>
<td>{{'๋น๋ฐ๋ฐฉ' if room.password else '๊ณต๊ฐ๋ฐฉ'}}</td>
<td>{{room.max}}</td>
<td style="color: {{room.owner}}">{{room.owner}}</td>
<td>
<button
data-password="{{'true' if room.password else 'false'}}"
data-id="{{room._id}}"
class="join-btn"
>์
์ฅ
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="error-message">{{error}}</div>
<a href="/room">์ฑํ
๋ฐฉ ์์ฑ</a>
</fieldset>
<script src="/socket.io/socket.io.js"></script>
<script>
const socket = io.connect('http://localhost:8005/room', { // ๋ค์์คํ์ด์ค
path: '/socket.io',
});
socket.on('newRoom', function (data) { // ์ ๋ฐฉ ์ด๋ฒคํธ ์ ์ ๋ฐฉ ์์ฑ
const tr = document.createElement('tr');
let td = document.createElement('td');
td.textContent = data.title;
tr.appendChild(td);
td = document.createElement('td');
td.textContent = data.password ? '๋น๋ฐ๋ฐฉ' : '๊ณต๊ฐ๋ฐฉ';
tr.appendChild(td);
td = document.createElement('td');
td.textContent = data.max;
tr.appendChild(td);
td = document.createElement('td');
td.style.color = data.owner;
td.textContent = data.owner;
tr.appendChild(td);
td = document.createElement('td');
const button = document.createElement('button');
button.textContent = '์
์ฅ';
button.dataset.password = data.password ? 'true' : 'false';
button.dataset.id = data._id; // ๋ฒํผ์ ๋ฐฉ ์์ด๋ ์ ์ฅ
button.addEventListener('click', addBtnEvent);
td.appendChild(button);
tr.appendChild(td);
tr.dataset.id = data._id; // tr์ ๋ฐฉ ์์ด๋ ์ ์ฅ
document.querySelector('table tbody').appendChild(tr); // ํ๋ฉด์ ์ถ๊ฐ
});
socket.on('removeRoom', function (data) { // ๋ฐฉ ์ ๊ฑฐ ์ด๋ฒคํธ ์ id๊ฐ ์ผ์นํ๋ ๋ฐฉ ์ ๊ฑฐ
document.querySelectorAll('tbody tr').forEach(function (tr) {
if (tr.dataset.id === data) {
tr.parentNode.removeChild(tr);
}
});
});
function addBtnEvent(e) { // ๋ฐฉ ์
์ฅ ํด๋ฆญ ์
if (e.target.dataset.password === 'true') { // ๋น๋ฐ๋ฐฉ์ด๋ฉด
const password = prompt('๋น๋ฐ๋ฒํธ๋ฅผ ์
๋ ฅํ์ธ์');
location.href = '/room/' + e.target.dataset.id + '?password=' + password;
} else {
location.href = '/room/' + e.target.dataset.id;
}
}
document.querySelectorAll('.join-btn').forEach(function (btn) {
btn.addEventListener('click', addBtnEvent);
});
</script>
{% endblock %}
{% block script %}
<script>
window.onload = () => {
if (new URL(location.href).searchParams.get('error')) {
alert(new URL(location.href).searchParams.get('error'));
}
};
</script>
{% endblock %}
// views/room.html
{% extends 'layout.html' %}
{% block content %}
<fieldset>
<legend>์ฑํ
๋ฐฉ ์์ฑ</legend>
<form action="/room" method="post">
<div>
<input type="text" name="title" placeholder="๋ฐฉ ์ ๋ชฉ">
</div>
<div>
<input type="number" name="max" placeholder="์์ฉ ์ธ์(์ต์ 2๋ช
)" min="2" value="10">
</div>
<div>
<input type="password" name="password" placeholder="๋น๋ฐ๋ฒํธ(์์ผ๋ฉด ๊ณต๊ฐ๋ฐฉ)">
</div>
<div>
<button type="submit">์์ฑ</button>
</div>
</form>
</fieldset>
{% endblock %}
// views/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="color: {{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="/socket.io/socket.io.js"></script>
<script>
const socket = io.connect('http://localhost:8005/chat', {
path: '/socket.io',
});
socket.emit('join', new URL(location).pathname.split('/').at(-1));
socket.on('join', function (data) {
const div = document.createElement('div');
div.classList.add('system');
const chat = document.createElement('div');
chat.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');
chat.textContent = data.chat;
div.appendChild(chat);
document.querySelector('#chat-list').appendChild(div);
});
</script>
{% endblock %}
์ดํ ์์ผ์ ๋ํ ์ฝ๋๋ฅผ ์์ฑํ๋ค.
app.set('io', io)๋ก ๋ผ์ฐํฐ์์ io ๊ฐ์ฒด๋ฅผ ์ฌ์ฉํ ์ ์๊ฒ ์ ์ฅํด ๋๋ค.
์ดํ rep.app.get('io')๋ก ์ ๊ทผ์ด ๊ฐ๋ฅํ๋ค.
of๋ฉ์๋๋ Socket.IO์ ๋ค์ ์คํ์ด์ค๋ฅผ ๋ถ์ฌํ๋ ๋ฉ์๋๋ก ๊ธฐ๋ณธ์ ์ผ๋ก ์ ๊ทผํ๋ ๊ณณ์ด ์๋ ๋ค๋ฅธ ๋ค์์คํ์ด์ค๋ฅผ ๋ง๋ค์ด ์ ์ํ ์ ์๊ฒ ๋ง๋ ๋ค.
๋ค์์คํ์ด์ค๋ ์๋ก ๊ฐ์ ๊ณณ๋ผ๋ฆฌ๋ง ๋ฐ์ดํฐ๋ฅผ ์ ๋ฌํ๋ค.
ํ์ฌ๋ /room, /chat ๋ ๊ฐ์ง์ด๋ค.
/room ๋ค์์คํ์ด์ค์ ์ด๋ฒคํธ ๋ฆฌ์ค๋๋ฅผ ๋ถ์ธ ์ฝ๋๊ฐ ์์ ๊ฐ๋ค.
connection ์ด๋ฒคํธ๋ ๋ค์์คํ์ด์ค ์ฐ๊ฒฐ ์ ๋ฐ์ํ๋ค.
disconnect ์ด๋ฒคํธ๋ ์ฐ๊ฒฐ ํด์ ์ ๋ฐ์ํ๋ค.
/chat ๋ค์์คํ์ด์ค์ ๋ถ์ธ ์ด๋ฒคํธ ๋ฆฌ์ค๋๊ณ connection๊ณผ disconnect๋ ์ฌ์ฉ์๊ฐ ์ง์ ๋ง๋ ์ด๋ฒคํธ์ด๋ค.
room ๋ฐฉ:
socket.io์ ์ด๋ ๋ค์์คํ์ด์ค๋ณด๋ค ๋ ์ธ๋ถ์ ์ธ ๊ฐ๋ ์ด๋ค.
๊ฐ์ ๋ค์์คํ์ค ์์์๋ ๊ฐ์ ๋ฐฉ์ ๋ค์ด ์๋ ์์ผ๋ผ๋ฆฌ๋ง ๋ฐ์ดํฐ๋ฅผ ์ฃผ๊ณ ๋ฐ์ ์ ์๋ค.
socket.emit('join', ๋ฐฉ์์ด๋) ์ด๋ฅผ ํตํด์ join์ด๋ฒคํธ์์ data ๋งค๊ฐ ๋ณ์๋ก ๋ฐฉ ์์ด๋๋ฅผ ์ ๋ฌ๋ฐ์ ๋ฐฉ์ ์ ์ํ ์ ์๋ค.
์๋๋ index.jsํ์ผ๋ก, ๋ผ์ฐํฐ์ ์ปจํธ๋กค๋ฌ๋ฅผ ๋ง๋ ๋ค.
// rountes/index.js
const express = require('express');
const { renderMain, renderRoom, createRoom, enterRoom, removeRoom } = require('../controllers');
const router = express.Router();
router.get('/', renderMain);
router.get('/room', renderRoom);
router.post('/room', createRoom);
router.get('/room/:id', enterRoom);
router.delete('/room/:id', removeRoom);
module.exports = router;
๊ทธ๋ฆฌ๊ณ ๋ชฝ๊ณ ๋๋น์ ์น ์์ผ ๋ชจ๋ ์ ๊ทผ ๊ฐ๋ฅํ ์ปจํธ๋กค๋ฌ๋ฅผ ์์ฑํ๋ค.
createToom ์ปจํธ๋กค๋ฌ๋ ์ฑํ ๋ฐฉ์ ๋ง๋๋ ๊ฒ์ด๊ณ , GET / ๋ผ์ฐํฐ์ ์ ์ํ ๋ชจ๋ ํด๋ผ์ด์ธํธ๊ฐ ์๋ก ์์ฑ๋ ์ฑํ ๋ฐฉ์ ๋ํ ๋ฐ์ดํฐ๋ฅผ ๋ฐ์ ์ ์๋ค.
enterToom ์ปจํธ๋กค๋ฌ๋ ์ฑํ ๋ฐฉ์ ์ ์ํด ์ฑํ ๋ฐฉ ํ๋ฉด์ ๋ ๋๋ง ํ๋ ์ปจํธ๋กค๋ฌ์ด๋ค.
if ๋ฌธ์ผ๋ก ๋น๋ฐ๋ฐฉ์ ์ฌ๋ถ, ๋๋๋ง ์ด์ ์ ๋ฐฉ์ ์กด์ฌ ์ฌ๋ถ, ํ์ฉ ์ธ์ ์ด๊ณผ ์ฌ๋ถ๋ฅผ ํ์ธํ๋ค.
removeRoom ์ปจํธ๋กค๋ฌ๋ ์ฑํ ๋ฐฉ์ ์ญ์ ํ๋ ๊ฒ์ผ๋ก, ๋ฐฉ๊ณผ ๋ด์ญ์ ๋ชจ๋ ์ญ์ ํ๋ค.
// controllers/index.js
const Room = require('../schemas/room');
const Chat = require('../schemas/chat');
exports.renderMain = async (req, res, next) => {
try {
const rooms = await Room.find({});
res.render('main', { rooms, title: 'GIF ์ฑํ
๋ฐฉ' });
} catch (error) {
console.error(error);
next(error);
}
};
exports.renderRoom = (req, res) => {
res.render('room', { title: 'GIF ์ฑํ
๋ฐฉ ์์ฑ' });
};
exports.createRoom = 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);
if (req.body.password) { // ๋น๋ฐ๋ฒํธ๊ฐ ์๋ ๋ฐฉ์ด๋ฉด
res.redirect(`/room/${newRoom._id}?password=${req.body.password}`);
} else {
res.redirect(`/room/${newRoom._id}`);
}
} catch (error) {
console.error(error);
next(error);
}
};
exports.enterRoom = async (req, res, next) => {
try {
const room = await Room.findOne({ _id: req.params.id });
if (!room) {
return res.redirect('/?error=์กด์ฌํ์ง ์๋ ๋ฐฉ์
๋๋ค.');
}
if (room.password && room.password !== req.query.password) {
return res.redirect('/?error=๋น๋ฐ๋ฒํธ๊ฐ ํ๋ ธ์ต๋๋ค.');
}
const io = req.app.get('io');
const { rooms } = io.of('/chat').adapter;
console.log(rooms, rooms.get(req.params.id), rooms.get(req.params.id));
if (room.max <= rooms.get(req.params.id)?.size) {
return res.redirect('/?error=ํ์ฉ ์ธ์์ด ์ด๊ณผํ์์ต๋๋ค.');
}
return res.render('chat', {
room,
title: room.title,
chats: [],
user: req.session.color,
});
} catch (error) {
console.error(error);
return next(error);
}
};
exports.removeRoom = async (req, res, next) => {
try {
await Room.deleteOne({ _id: req.params.id });
await Chat.deleteMany({ room: req.params.id });
res.send('ok');
} catch (error) {
console.error(error);
next(error);
}
};
ํ๋ก์ ํธ ์คํ์ ์์์ ๋ชฝ๊ณ ๋๋น๋ฅผ ๋จผ์ ์คํํด์ผ ํ๋ค.
์ดํ ์๋ฒ ์คํ ํ ๋ธ๋ผ์ฐ์ ๋ฅผ ๋ ๊ฐ ๋์ฐ๋ฉด ๋ ๋ช ์ด ์ ์ํ ๋ฏํ ๋ชจ์ต์ ๋ณผ ์ ์๋ค.
๋ฐฉ์์ ์ ์ฅํ ๋์ ํด์ฅํ ๋ ๋๊ฐ ์ ์ฅํ๊ณ ๋๊ฐ ํด์ฅํ๋์ง ์๋ฆฌ๋ ์์คํ ์๋ฆผ์ ๋ง๋ ๋ค.
๊ทธ๋ฆฌ๊ณ ์ฑํ ๋ฐฉ ์ ์์ ์๊ฐ 0๋ช ์ผ ๋ ๋ฐฉ์ ์ ๊ฑฐํ๋ ์ฝ๋๋ ๊ฐ์ด ๋ฃ๋๋ค.
์ด์ ๋ํ app.js์ socket.js๋ ์๋์ ๊ฐ๋ค.
// app.js
const express = require('express');
const path = require('path');
const morgan = require('morgan');
const cookieParser = require('cookie-parser');
const session = require('express-session');
const nunjucks = require('nunjucks');
const dotenv = require('dotenv');
const ColorHash = require('color-hash').default;
dotenv.config();
const webSocket = require('./socket');
const indexRouter = require('./routes');
const connect = require('./schemas');
const app = express();
app.set('port', process.env.PORT || 8005);
app.set('view engine', 'html');
nunjucks.configure('views', {
express: app,
watch: true,
});
connect();
const sessionMiddleware = session({
resave: false,
saveUninitialized: false,
secret: process.env.COOKIE_SECRET,
cookie: {
httpOnly: true,
secure: false,
},
});
app.use(morgan('dev'));
app.use(express.static(path.join(__dirname, 'public')));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser(process.env.COOKIE_SECRET));
app.use(sessionMiddleware);
app.use((req, res, next) => {
if (!req.session.color) {
const colorHash = new ColorHash();
req.session.color = colorHash.hex(req.sessionID);
console.log(req.session.color, req.sessionID);
}
next();
});
app.use('/', indexRouter);
app.use((req, res, next) => {
const error = new Error(`${req.method} ${req.url} ๋ผ์ฐํฐ๊ฐ ์์ต๋๋ค.`);
error.status = 404;
next(error);
});
app.use((err, req, res, next) => {
res.locals.message = err.message;
res.locals.error = process.env.NODE_ENV !== 'production' ? err : {};
res.status(err.status || 500);
res.render('error');
});
const server = app.listen(app.get('port'), () => {
console.log(app.get('port'), '๋ฒ ํฌํธ์์ ๋๊ธฐ์ค');
});
webSocket(server, app, sessionMiddleware);
// socket.io
const SocketIO = require('socket.io');
const { removeRoom } = require('./services');
module.exports = (server, app, sessionMiddleware) => {
const io = SocketIO(server, { path: '/socket.io' });
app.set('io', io);
const room = io.of('/room');
const chat = io.of('/chat');
const wrap = middleware => (socket, next) => middleware(socket.request, {}, next);
chat.use(wrap(sessionMiddleware));
room.on('connection', (socket) => {
console.log('room ๋ค์์คํ์ด์ค์ ์ ์');
socket.on('disconnect', () => {
console.log('room ๋ค์์คํ์ด์ค ์ ์ ํด์ ');
});
});
chat.on('connection', (socket) => {
console.log('chat ๋ค์์คํ์ด์ค์ ์ ์');
socket.on('join', (data) => {
socket.join(data);
socket.to(data).emit('join', {
user: 'system',
chat: `${socket.request.session.color}๋์ด ์
์ฅํ์
จ์ต๋๋ค.`,
});
});
socket.on('disconnect', async () => {
console.log('chat ๋ค์์คํ์ด์ค ์ ์ ํด์ ');
const { referer } = socket.request.headers; // ๋ธ๋ผ์ฐ์ ์ฃผ์๊ฐ ๋ค์ด์์
const roomId = new URL(referer).pathname.split('/').at(-1);
const currentRoom = chat.adapter.rooms.get(roomId);
const userCount = currentRoom?.size || 0;
if (userCount === 0) { // ์ ์ ๊ฐ 0๋ช
์ด๋ฉด ๋ฐฉ ์ญ์
await removeRoom(roomId); // ์ปจํธ๋กค๋ฌ ๋์ ์๋น์ค๋ฅผ ์ฌ์ฉ
room.emit('removeRoom', roomId);
console.log('๋ฐฉ ์ ๊ฑฐ ์์ฒญ ์ฑ๊ณต');
} else {
socket.to(roomId).emit('exit', {
user: 'system',
chat: `${socket.request.session.color}๋์ด ํด์ฅํ์
จ์ต๋๋ค.`,
});
}
});
});
};
remove ์๋น์ค๋ฅผ ์์ฑํ๋ ๋ฐ์ ์์ด์ ์๋์ index์ฝ๋๋ฅผ ์์ฑํ๊ณ , removeRoom ์๋น์ค๋ฅผ ์ปจํธ๋กคํ๋ index๋ ์๋์ ๊ฐ๋ค.
// service/index.js
const Room = require('../schemas/room');
const Chat = require('../schemas/chat');
exports.removeRoom = async (roomId) => {
try {
await Room.deleteOne({ _id: roomId });
await Chat.deleteMany({ room: roomId });
} catch (error) {
throw error;
}
};
// controllers/index.js
const Room = require('../schemas/room');
const Chat = require('../schemas/chat');
const { removeRoom: removeRoomService } = require('../services');
exports.renderMain = async (req, res, next) => {
try {
const rooms = await Room.find({});
res.render('main', { rooms, title: 'GIF ์ฑํ
๋ฐฉ' });
} catch (error) {
console.error(error);
next(error);
}
};
exports.renderRoom = (req, res) => {
res.render('room', { title: 'GIF ์ฑํ
๋ฐฉ ์์ฑ' });
};
exports.createRoom = 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);
if (req.body.password) { // ๋น๋ฐ๋ฒํธ๊ฐ ์๋ ๋ฐฉ์ด๋ฉด
res.redirect(`/room/${newRoom._id}?password=${req.body.password}`);
} else {
res.redirect(`/room/${newRoom._id}`);
}
} catch (error) {
console.error(error);
next(error);
}
};
exports.enterRoom = async (req, res, next) => {
try {
const room = await Room.findOne({ _id: req.params.id });
if (!room) {
return res.redirect('/?error=์กด์ฌํ์ง ์๋ ๋ฐฉ์
๋๋ค.');
}
if (room.password && room.password !== req.query.password) {
return res.redirect('/?error=๋น๋ฐ๋ฒํธ๊ฐ ํ๋ ธ์ต๋๋ค.');
}
const io = req.app.get('io');
const { rooms } = io.of('/chat').adapter;
console.log(rooms, rooms.get(req.params.id), rooms.get(req.params.id));
if (room.max <= rooms.get(req.params.id)?.size) {
return res.redirect('/?error=ํ์ฉ ์ธ์์ด ์ด๊ณผํ์์ต๋๋ค.');
}
return res.render('chat', {
room,
title: room.title,
chats: [],
user: req.session.color,
});
} catch (error) {
console.error(error);
return next(error);
}
};
exports.removeRoom = async (req, res, next) => {
try {
await removeRoomService(req.params.id);
res.send('ok');
} catch (error) {
console.error(error);
next(error);
}
};
์ดํ ๊ต์ฌ๋ฅผ ํตํด์ ์ฑํ ์ ํ๋ ๋ฐฉ๋ฒ๋ ํ์ธํ ์ ์๋ค.
[๋ ธ๋ 2] 17์ฅ. ํ์ ์คํฌ๋ฆฝํธ ๋ ธ๋ ๊ฐ๋ฐ (0) | 2024.01.19 |
---|---|
[๋ ธ๋ 2] 15์ฅ. AWS๋ก ๋ฐฐํฌํ๊ธฐ (0) | 2024.01.12 |
[๋ ธ๋ 2] 11์ฅ. ๋ ธ๋ ์๋น์ค ํ ์คํธํ๊ธฐ (1) | 2023.12.29 |
[๋ ธ๋ 2] 10์ฅ. ์น API ์๋ฒ ๋ง๋ค๊ธฐ (0) | 2023.12.22 |
[๋ ธ๋ 2] 9์ฅ. ์ต์คํ๋ ์ค๋ก SNS ์๋น์ค ๋ง๋ค๊ธฐ (1) | 2023.12.01 |