본문 바로가기
Node.js

웹소켓을 통한 실시간 인터랙션 구현

by 오늘만 사는 여자 2021. 9. 26.
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
반응형

'Node.js' 카테고리의 다른 글

MongoDb  (0) 2021.09.20
NoSQL  (0) 2021.09.20
NoSQL  (0) 2021.09.20
Express  (0) 2021.09.15
리팩토링 프로젝트  (0) 2021.09.15

댓글