๐ koa
- express ํ์์ ๋ง๋ ํ๋ ์์ํฌ
- express ๋๋น ๋ ๊ฐ๋ณ๊ณ ๋น ๋ฆ
- ๋ฏธ๋ค์จ์ด ๋ ๋ฒจ์์๋ async/await๋ฅผ ์ ๊ณตํ์ฌ ๋น๋๊ธฐ ํ๋ก๊ทธ๋๋ฐ์ ๋ ํธ๋ฆฌํ๊ฒ ์ฌ์ฉ ๊ฐ๋ฅ
- ํ๋ ์์ํฌ ์ค์น: [ํฐ๋ฏธ๋] npm i koa
// @ts-check
const Koa = require('koa');
const app = new Koa();
const PORT = 4500;
app.use(async (ctx, next) => {
console.log(ctx.request);
console.log(ctx.response);
ctx.body = 'Hello, koa world!';
});
app.listen(PORT, () => {
console.log(`์๋ฒ๋ ${PORT}์์ ์๋ ์ค์
๋๋ค.`);
});
๐ฉ Pug
- node.js์ฉ view engine
- html ๋๋น ๊ฐ๋จํ๊ฒ ํ๊ทธ ์ฌ์ฉ
- ํ ํ๋ฆฟ ๊ธฐ๋ฅ ์ ๊ณต
- ์ค์น: [ํฐ๋ฏธ๋] npm i koa-pug -s
- app.js์ pug ๋ฏธ๋ค์จ์ด ์ ์ฉ
// @ts-check
const Koa = require('koa');
const Pug = require('koa-pug');
const path = require('path');
const app = new Koa();
const PORT = 4500;
const pug = new Pug({
viewPath: path.resolve(__dirname, './views'),
app,
});
app.use(async (ctx, next) => {
await ctx.render('chat');
});
app.listen(PORT, () => {
console.log(`์๋ฒ๋ ${PORT}์์ ์๋ ์ค์
๋๋ค.`);
});
- /views/chat.pug(+๋ถํธ์คํธ๋ฉ ์ ์ฉ)
html
head
link(href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-iYQeCzEYFbKjA/T2uDLTpkwGzCiq6soy8tYaI1GyVh/UjpbCx/TYkiZhlZB6+fzT" crossorigin="anonymous")
body.d-flex.flex-column.text-center.align-items-center
h1.w-100.p-3.bg-primary.text-white.font-bold Chat
div.w-50.h-75.p-5.bg-secondary.text-bottom.overflow-auto
p.p-2.bg-warning.fw-bold.text-white ์ฑํ
์์
form.w-50(action="/chat")
input.w-75.m-3.p-3(type="text" placeholder="์ฑํ
์ ์
๋ ฅํ์ธ์")
a.p-3.btn.btn-primary ๋ณด๋ด๊ธฐ
๐ฉ koa-web socket
- ๋ชจ๋ ์ค์น: [ํฐ๋ฏธ๋] npm i koa-websocket
- [ํฐ๋ฏธ๋] npm i koa-route
- /chat ์ด๋ผ๋ ๊ฒฝ๋ก๋ก ๋ค์ด์ค๋ฉด ์น ์์ผ ํต์ ์ ํตํด ํด๋ผ์ด์ธํธ๋ก ๋ฌธ๊ตฌ๋ฅผ ์ ์ก
- ํด๋ผ์ด์ธํธ์์ message๊ฐ ์ค๋ฉด ๋ฐ์์ ์๋ฒ์ ์ถ๋ ฅ
const websockify = require('koa-websocket');
const route = require('koa-route');
const app = websockify(new Koa());
app.ws.use(
route.all('./chat', (ctx) => {
ctx.websocket.send('์๋ฒ์
๋๋ค.');
ctx.websocket.on('message', (message) => {
console.log(message.toString());
});
})
);
๐ฉ /public/chat.js ๋ฅผ ์์ฑํ๊ณ ํด๋ผ์ด์ธํธ ์ฑํ ๊ด๋ จ ๊ธฐ๋ฅ์ ๊ตฌํ
- express๋ ๋ด์ฅ ๋ชจ๋๋ก๋ ํน์ ์ฃผ์์ ๋ํ static ํด๋ ์ค์ ๊ฐ๋ฅ
- koa๋ koa-static๊ณผ koa-mount ๋ชจ๋์ ์ค์นํด์ผ ๊ฐ๋ฅ
- [ํฐ๋ฏธ๋] npm i koa-static
- [ํฐ๋ฏธ๋] npm i koa-mount
- ํด๋ผ์ด์ธํธ์์ WebSocket ์๋ฒ๋ฅผ ์ฐ๊ฒฐํ๊ณ ์๋ฒ์ addEventListener-open์ ๋ฑ๋กํ์ฌ ์ด์ ์ ์ค๋นํด๋์๋ ์๋ฒ๋ก ํต์ ๋ณด๋ด๊ธฐ
๐ IIFE(Immediately Invoked Function Expression)
- ํจ์๊ฐ ์ ์๋์๋ง์ ์ฌ์ฉ๋๋ ์ฆ์ ์คํ ํจ์
- ํจ์์ ์ ์ ๋ถ๋ถ์ ์ธ๋ถ๋ก๋ถํฐ ๊ฐ์ถ๊ณ ์ถ์ ๋ ์ฌ์ฉ
// @ts-check
// IIFE
(() => {
const socket = new WebSocket(`ws://${window.location.host}/chat`);
socket.addEventListener('open', () => {
socket.send('ํด๋ผ์ด์ธํธ ์
๋๋ค.');
});
})();
๐ Broadcast
- ์น ์์ผ ์๋ฒ๋ app.ws์ ํ ๋น๋์ด ์์ผ๋ฏ๋ก server๋ผ๋ ๋ณ์์ ๊ตฌ์กฐ ๋ถํด ํ ๋น ๋ฌธ๋ฒ์ผ๋ก ๋ฐ์์ค๊ธฐ
app.ws.use(
route.all('/chat', (ctx) => {
const { server } = app.ws;
server?.clients.forEach((client) => {
client.send('๋ชจ๋ ํด๋ผ์ด์ธํธ์๊ฒ ๋ฉ์์ง ๋ณด๋ด๊ธฐ');
});
ctx.websocket.on('message', (message) => {
console.log(message.toString());
});
})
);
๐ ์ค์ ์ฑํ ์ ํ๋ฆ
- ํด๋ผ์ด์ธํธ์ form์ ๋ด์ฉ์ ์ ๋ ฅํ๊ณ ๋ณด๋ด๊ธฐ๋ฅผ ๋๋ฅด๋ฉด ์๋ฒ๋ก ๋ฐ์ดํฐ ์ ๋ฌ
- ์๋ฒ๋ ํด๋น ์ ๋ณด๋ฅผ ๋ค์ ๋ชจ๋ ํด๋ผ์ด์ธํธ์๊ฒ ๋ณด๋ด๋ ํํ
- chat.pug์์ id ๋ถ์ฌ
html
head
link(href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous")
body.d-flex.flex-column.text-center.align-items-center
h1.w-100.p-3.bg-primary.text-white.font-bold Chat
div#chat.w-50.h-75.p-5.bg-secondary.text-bottom.overflow-auto
p.p-2.bg-warning.fw-bold.text-white ์ฑํ
์์
form(action="/chat" onsubmit="return false;")#form.w-50
input(type="text" placeholder="์ฑํ
์ ์
๋ ฅํ์ธ์")#input.w-75.m-3.p-3
a#btn.p-3.btn.btn-primary ๋ณด๋ด๊ธฐ
script(src="public/chat.js")
- /public/chat.js
// @ts-check
// IIFE
(() => {
const socket = new WebSocket(`ws://${window.location.host}/chat`);
const btnEl = document.getElementById('btn');
const inputEl = document.getElementById('input');
const chatEl = document.getElementById('chat');
btnEl?.addEventListener('click', () => {
const msg = inputEl?.value;
const data = {
name: 'ebulsok',
msg: msg,
};
socket.send(JSON.stringify(data));
inputEl.value = '';
});
socket.addEventListener('open', () => {
// socket.send('ํด๋ผ์ด์ธํธ ์
๋๋ค.');
});
socket.addEventListener('message', (event) => {
const { name, msg } = JSON.parse(event.data);
const msgEl = document.createElement('p');
msgEl.innerText = `${name}: ${msg}`;
msgEl.classList.add('p-2');
msgEl.classList.add('bg-warning');
msgEl.classList.add('fw-bold');
// msgEl.classList.add('text-white');
chatEl?.appendChild(msgEl);
chatEl.scrollTop = chatEl.scrollHeight - chatEl.clientHeight;
});
})();
- app.js
// @ts-check
const Koa = require('koa');
const websockify = require('koa-websocket');
const route = require('koa-route');
const serve = require('koa-static');
const mount = require('koa-mount');
const Pug = require('koa-pug');
const path = require('path');
const app = websockify(new Koa());
const PORT = 4500;
app.use(mount('/public', serve('public')));
const pug = new Pug({
viewPath: path.resolve(__dirname, './views'),
app,
});
app.ws.use(
route.all('/chat', (ctx) => {
const { server } = app.ws;
server?.clients.forEach((client) => {
// client.send('๋ชจ๋ ํด๋ผ์ด์ธํธ์๊ฒ ๋ฉ์์ง ๋ณด๋ด๊ธฐ');
});
ctx.websocket.on('message', (message) => {
server?.clients.forEach((client) => {
client.send(message.toString());
});
});
})
);
app.use(async (ctx, next) => {
await ctx.render('chat');
});
app.listen(PORT, () => {
console.log(`์๋ฒ๋ ${PORT}์์ ์๋ ์ค์
๋๋ค.`);
});
๐ฉ app.js์ ์๋ก์ด ์ ์ ๊ฐ ์ ์ํ๋ค๋ ๋ฉ์์ง, ๋๊ฐ๋ค๋ ๋ฉ์์ง ์๋ด ์ฝ๋ ์ถ๊ฐ
// @ts-check
const Koa = require('koa');
const websockify = require('koa-websocket');
const route = require('koa-route');
const serve = require('koa-static');
const mount = require('koa-mount');
const Pug = require('koa-pug');
const path = require('path');
const app = websockify(new Koa());
const PORT = 4500;
app.use(mount('/public', serve('public')));
const pug = new Pug({
viewPath: path.resolve(__dirname, './views'),
app,
});
app.ws.use(
route.all('/chat', (ctx) => {
const { server } = app.ws;
server?.clients.forEach((client) => {
client.send(
JSON.stringify({
name: 'system',
msg: `์๋ก์ด ์ ์ ๊ฐ ์ฐธ์ฌํ์ต๋๋ค. ํ์ฌ ์ ์ ์: ${server.clients.size}`,
bg: 'bg-danger',
textColor: 'text-white',
})
);
});
ctx.websocket.on('message', (message) => {
server?.clients.forEach((client) => {
client.send(message.toString());
});
});
ctx.websocket.on('close', (message) => {
server?.clients.forEach((client) => {
client.send(
JSON.stringify({
name: 'server',
msg: `์ ์ ๊ฐ ํด์ฅํ์ต๋๋ค. ํ์ฌ ์ ์ ์: ${server.clients.size}`,
bg: 'bg-dark',
textColor: 'text-white',
})
);
});
});
})
);
app.use(async (ctx, next) => {
await ctx.render('chat');
});
app.listen(PORT, () => {
console.log(`์๋ฒ๋ ${PORT}์์ ์๋ ์ค์
๋๋ค.`);
});
๐ฉ chat.js ๋๋คํ ๋๋ค์ ์ ํ๊ธฐ
// @ts-check
const adj = [
'๋ฉ์ง',
'์์๊ธด',
'์์',
'์กธ๋ฆฐ',
'์ฐ์ํ',
'ํํ',
'๋ฐฐ๊ณ ํ',
'์ง์ ๊ฐ๊ธฐ ์ซ์',
'์ง์ ๊ฐ๊ณ ์ถ์',
'๊ท์ฌ์ด',
'์คํํ',
'๋๋ํ',
'์ด๊ฒ ๋ญ๊ฐ ์ถ์',
'๊น๋ฆฌํ',
'ํ๋ก ํธ๊ฐ ํ๊ณ ์ถ์',
'๋ฐฑ์๋๊ฐ ์ฌ๋ฏธ ์๋',
'๋ชฝ๊ณ ๋๋น ๋ ๋ ค ๋จน์',
'์ด์ฌํํ๋',
'ํผ๊ณคํ',
'๋๋น์ด ์ด๋กฑ์ด๋กฑํ',
'์นํจ์ด ๋ก๊ธฐ๋',
'์ ์ด ๋ก๊ธฐ๋',
];
const member = [
'a๋',
'b๋',
'c๋',
'd๋',
'e๋',
'f๋',
'g๋',
'h๋',
'i๋',
'j๋',
'k๋',
'l๋',
'm๋',
'n๋',
'o๋',
'์ธ์๋',
'์์ง๋',
'์น์๋',
'ํด์ฑ๋',
'ํ์๋',
];
const bootColor = [
{ bg: 'bg-primary', text: 'text-white' },
{ bg: 'bg-success', text: 'text-white' },
{ bg: 'bg-warning', text: 'text-black' },
{ bg: 'bg-info', text: 'text-white' },
{ bg: 'alert-primary', text: 'text-black' },
{ bg: 'alert-secondary', text: 'text-black' },
{ bg: 'alert-success', text: 'text-black' },
{ bg: 'alert-danger', text: 'text-black' },
{ bg: 'alert-warning', text: 'text-black' },
{ bg: 'alert-info', text: 'text-black' },
];
// IIFE
(() => {
const socket = new WebSocket(`ws://${window.location.host}/chat`);
const btnEl = document.getElementById('btn');
const inputEl = document.getElementById('input');
const chatEl = document.getElementById('chat');
function pickRandom(arr) {
const index = Math.floor(Math.random() * arr.length);
console.log(index);
return arr[index];
}
const nickName = pickRandom(adj) + ' ' + pickRandom(member);
const theme = pickRandom(bootColor);
btnEl?.addEventListener('click', () => {
const msg = inputEl?.value;
const data = {
name: nickName,
msg: msg,
bg: theme.bg,
textColor: theme.text,
};
socket.send(JSON.stringify(data));
inputEl.value = '';
});
socket.addEventListener('open', () => {
// socket.send('ํด๋ผ์ด์ธํธ ์
๋๋ค.');
});
socket.addEventListener('message', (event) => {
const { name, msg, bg, textColor } = JSON.parse(event.data);
const msgEl = document.createElement('p');
msgEl.innerText = `${name}: ${msg}`;
msgEl.classList.add('p-2');
msgEl.classList.add(bg);
msgEl.classList.add('fw-bold');
msgEl.classList.add(textColor);
chatEl?.appendChild(msgEl);
chatEl.scrollTop = chatEl.scrollHeight - chatEl.clientHeight;
});
})();
๐ฉ MongoDB ์ฐ๊ฒฐ
- ๋ชจ๋ ์ค์น: [ํฐ๋ฏธ๋] npm i mongodb
- public/mongo.js
// @ts-check
const { MongoClient, ServerApiVersion } = require('mongodb');
const uri =
'mongodb://ebulsok:<password>@localhost:27017/?authMechanism=DEFAULT';
const client = new MongoClient(uri, {
useNewUrlParser: true,
useUnifiedTopology: true,
serverApi: ServerApiVersion.v1,
});
module.exports = client;
๐ฉ ์ฑํ ๋ด์ญ DB ์ ์ฅ
- ํด๋ผ์ด์ธํธ์์ ์ ์ก๋ ๋ฉ์์ง๊ฐ ๋ค์ด์ค๋ฉด DB์ ์ ์ฅ
- ์ ๋ฌ๋ฐ์ ๋ฉ์์ง๋ฅผ JSON.parseํ์ฌ ์ค๋ธ์ ํธ๋ก ๋ณ๊ฒฝํ ๋ค์ ๋ณ์์ ๋ฃ๊ธฐ
- chat์ ์ ๊ฐ์ฐ์ฐ์๋ก ํ์ด์ DB์ ๋ฃ๊ธฐ
- ์ฑํ ๋ด์ญ ์ฝ์ ์ ์๊ฐ ํญ๋ชฉ ์ถ๊ฐ
ctx.websocket.on('message', async (message) => {
const chat = JSON.parse(message.toString());
const insertClient = await _client;
const chatCursor = insertClient.db('KDT-1').collection('chats');
await chatCursor.insertOne({
...chat,
createdAt: new Date(),
});
server?.clients.forEach((client) => {
client.send(
JSON.stringify({
type: 'chat',
data: { ...chat },
})
);
});
});
๐ฉ ์ฑํ ๋ด์ญ ๋ถ๋ฌ์ค๊ธฐ
- ์๋ก์ด ํด๋ผ์ด์ธํธ๊ฐ ์ ์ํ๋ฉด DB๋ก๋ถํฐ ๋ชจ๋ ์ฑํ ๋ด์ญ์ ๋ฐ์์ ํด๋ผ์ด์ธํธ๋ก ์ ๋ฌ
- ํด๋ผ์ด์ธํธ๊ฐ ์๋ฒ ์ ์ ์ ์ต์ด์ ๋ฐ์ํด์ผ ํจ, ์ ์ํ ํด๋ผ์ด์ธํธ์๊ฒ๋ง ์ ๋ฌ
- ์ฌ์ฉ์๊ฐ ๋ณด๋ธ ์ฑํ ๊ณผ ๊ตฌ๋ถ๋ ํ์๊ฐ ์์(type ํค ์ถ๊ฐ)
app.ws.use(
route.all('/chat', async (ctx) => {
const { server } = app.ws;
const client = await _client;
const cursor = client.db('KDT-1').collection('chats');
const chats = cursor.find({}, { sort: { createdAt: 1 } });
const chatsData = await chats.toArray();
ctx.websocket.send(
JSON.stringify({
type: 'sync',
data: { chatsData },
})
);
server?.clients.forEach((client) => {
client.send(
JSON.stringify({
type: 'chat',
data: {
name: 'system',
msg: `์๋ก์ด ์ ์ ๊ฐ ์ฐธ์ฌํ์ต๋๋ค. ํ์ฌ ์ ์ ์: ${server.clients.size}`,
bg: 'bg-danger',
textColor: 'text-white',
},
})
);
});
ctx.websocket.on('message', async (message) => {
const chat = JSON.parse(message.toString());
const insertClient = await _client;
const chatCursor = insertClient.db('KDT-1').collection('chats');
await chatCursor.insertOne({
...chat,
createdAt: new Date(),
});
server?.clients.forEach((client) => {
client.send(
JSON.stringify({
type: 'chat',
data: { ...chat },
})
);
});
});
ctx.websocket.on('close', (message) => {
server?.clients.forEach((client) => {
client.send(
JSON.stringify({
type: 'chat',
data: {
name: 'server',
msg: `์ ์ ๊ฐ ํด์ฅํ์ต๋๋ค. ํ์ฌ ์ ์ ์: ${server.clients.size}`,
bg: 'bg-dark',
textColor: 'text-white',
},
})
);
});
});
})
);
๐ฉ chat.js์์ ์ฑํ ๋ด์ญ๊ณผ ์ค์ ์ฑํ ์ ๊ตฌ๋ถํ์ฌ ์ฒ๋ฆฌ
- msgData์ type์ด sync์ด๋ฉด ์ด์ ์ฑํ ๋ด์ญ์ด๋ฏ๋ก ํด๋น ๋ฐ์ดํฐ๋ฅผ ์ ๋ถ chats ๋ฐฐ์ด์ ํธ์ฌ
- chat์ด๋ฉด ํด๋ผ์ด์ธํธ๋ก๋ถํฐ ์ ๋ฌ๋ ์ค์ ์ฑํ ์ด๋ฏ๋ก chats ๋ฐฐ์ด์ ํธ์ฌ
- ๋ถ๊ธฐ ๋ณ๋ก ์ฑํ ๋ด์ญ์ ๊ทธ๋ ค์ฃผ๋ drawChats ํจ์ ๊ตฌํ
function drawChats(type, data) {
if (type === 'sync') {
chatEl.innerHTML = '';
chats.forEach(({ name, msg, bg, textColor }) => {
const msgEl = document.createElement('p');
msgEl.innerText = `${name}: ${msg}`;
msgEl.classList.add('p-2');
msgEl.classList.add(bg);
msgEl.classList.add('fw-bold');
msgEl.classList.add(textColor);
chatEl?.appendChild(msgEl);
chatEl.scrollTop = chatEl.scrollHeight - chatEl.clientHeight;
});
} else if (type === 'chat') {
const msgEl = document.createElement('p');
msgEl.innerText = `${data.name}: ${data.msg}`;
msgEl.classList.add('p-2');
msgEl.classList.add(data.bg);
msgEl.classList.add('fw-bold');
msgEl.classList.add(data.textColor);
chatEl?.appendChild(msgEl);
chatEl.scrollTop = chatEl.scrollHeight - chatEl.clientHeight;
}
}
const nickName = pickRandom(adj) + ' ' + pickRandom(member);
const theme = pickRandom(bootColor);
btnEl?.addEventListener('click', () => {
const msg = inputEl?.value;
const data = {
name: nickName,
msg: msg,
bg: theme.bg,
textColor: theme.text,
};
socket.send(JSON.stringify(data));
inputEl.value = '';
});
socket.addEventListener('message', (event) => {
const msgData = JSON.parse(event.data);
const { type, data } = msgData;
if (type === 'sync') {
const oldChats = data.chatsData;
chats.push(...oldChats);
drawChats(type, data);
} else if (type === 'chat') {
chats.push(data);
drawChats(type, data);
}
});
๐ฉ ์ํฐํค๋ฅผ ์ณค์ ๋๋ ์ฑํ ์ด ์ ์ก๋๊ฒ ์์
// chat.pug
form(action="/chat" onsubmit="return false;")#form.w-50
// chat.js
inputEl?.addEventListener('keyup', (event) => {
if (event.keyCode === 13) btnEl.click();
});