728x90
반응형
요구사항 정의서
* 간단한 실시간 채팅 서비스를 만든다.
* 인증 처리는 하지 않는다.
* 디비에 채팅 내역을 저장해 활용합니다.
Frontend
* Template engine: Pug
* CSS framework: TailwindCSS
Backend
* Web Framework: Koa
* Live networking: koa-websocket
* Database: MongoDB
main.js
// @ts-check
const Koa = require('koa')
const Pug = require('koa-pug')
const path = require('path')
const websockify = require('koa-websocket')
const route = require('koa-route')
const serve = require('koa-static')
const mount = require('koa-mount')
//데이터베이스 js 파일 연결
const mongoClient = require('./mongo')
//koa-websocket 연결
const app = websockify(new Koa())
// @ts-ignore
// eslint-disable-next-line no-new
new Pug({
//views폴더 하단에 pug 가져오기
viewPath: path.resolve(__dirname, './views'),
app,
})
//clietn.js 파일 가져오기
app.use(mount('/public', serve('src/public')))
app.use(async (ctx) => {
//main.pug 파일 render
await ctx.render('main')
})
/* eslint-disable-next-line no-underscore-dangle */
//디비 연결
const _client = mongoClient.connect()
async function getChatsCollection() {
const client = await _client
return client.db('chat').collection('chats')
}
// Using routes
// ws: websocket
app.ws.use(
route.all('/ws', async (ctx) => {
const chatsCollection = await getChatsCollection()
const chatCursor = chatsCollection.find(
{},
{
sort: {
//날짜 오래된거 순서
createdAt: 1,
},
}
)
const chats = await chatCursor.toArray()
ctx.websocket.send(
JSON.stringify({
type: 'sync',
payload: {
chats,
},
})
)
ctx.websocket.on('message', async (data) => {
if (typeof data !== 'string') {
return
}
const chat = JSON.parse(data)
await chatsCollection.insertOne({
...chat,
createdAt: new Date(),
})
const { nickname, message } = chat
const { server } = app.ws
if (!server) {
return
}
server.clients.forEach((client) => {
client.send(
JSON.stringify({
type: 'chat',
payload: {
message,
nickname,
},
})
)
})
})
})
)
app.listen(5000)
mongo.js
//mongoDB 연결
// @ts-check
const { MongoClient } = require('mongodb')
const uri =
'mongodb+srv://아이디:비번@cluster0.k3jz5.mongodb.net/myFirstDatabase?retryWrites=true&w=majority'
const client = new MongoClient(uri, {
// @ts-ignore
useNewUrlParser: true,
useUnifiedTopology: true,
})
module.exports = client
src/views/main.pug
html
head
link(href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" rel="stylesheet")
body
h1.bg-gradient-to-r.bg.from-purple-100.to-gray-200.p-16.text-4xl.font-bold 나의 채팅 서비스
div.p-16.space-y-8
div#chats.bg-gray-100.p-2 채팅 목록
form#form.space-x-4.flex
input#input.flex-1.border.border-gray-200.p-4.rounded(placeholder="채팅을 입력해 보세요.")
button#send.bg-blue-600.text-white.p-2.rounded 보내기
script(src="/public/client.js")
src/public/client.js
// @ts-check
//IIFE
;(() => {
const socket = new WebSocket(`ws://${window.location.host}/ws`)
const form = document.getElementById('form')
const chatsEl = document.getElementById('chats')
/** @type {HTMLInputElement | null} */
//@ts-ignore
const inputEl = document.getElementById('input')
if (!form || !inputEl || !chatsEl) {
throw new Error('Init failed!')
}
/**
* @typedef Chat
* @property {string} nickname
* @property {string} message
*/
/** @type {Chat[]} */
const chats = []
const adjectives = ['멋진', '훌륭한', '친절한', '새침한']
const animals = ['물범', '사자', '사슴', '돌고래', '독수리']
/**
* @param {string[]} array
* @returns {string}
*/
function pickRandom(array) {
const randomIdx = Math.floor(Math.random() * array.length)
const result = array[randomIdx]
if (!result) {
throw new Error('array length is 0.')
}
return result
}
const myNickname = `${pickRandom(adjectives)} ${pickRandom(animals)}`
form.addEventListener('submit', (event) => {
//페이지를 refresh 하지 않고 보내기
event.preventDefault()
socket.send(
JSON.stringify({
nickname: myNickname,
message: inputEl.value,
})
)
inputEl.value = ''
})
/* socket.addEventListener('open', () => {
socket.send('Hello, server')
})*/
const drawChats = () => {
chatsEl.innerHTML = ''
chats.forEach(({ message, nickname }) => {
const div = document.createElement('div')
div.innerText = `${nickname}: ${message}`
chatsEl.appendChild(div)
})
}
socket.addEventListener('message', (event) => {
const { type, payload } = JSON.parse(event.data)
if (type === 'sync') {
const { chats: syncedChats } = payload
chats.push(...syncedChats)
} else if (type === 'chat') {
const chat = payload
chats.push(chat)
}
drawChats()
})
})()
728x90
반응형
댓글