개발자취

TIL | 토이프로젝트 1 - 채팅서비스 본문

개발/Dev | 웹개발

TIL | 토이프로젝트 1 - 채팅서비스

hnhnhun 2022. 7. 27. 15:26

1. 프로젝트 설계

1.1 요구사항 설정

-  실시간 채팅 서비스를 간단한 형태로 제작
-  인증처리는 없음
-  채팅 내역은 데이터베이스에 저장하고, 이를 활용할 수 있도록 함.

1.2 Frontend UI 디자인

  • 채팅 내역은 polling(폴링) 형태로 처리하지 않고, 실시간으로 채팅내역을 저장할 수 있는 구조로 한다.
    • *폴링 : 실시간으로 장치 또는 프로그램의 처리상태를 주기적으로 체크하는 자료처리방식
  • Pug : Template engine
  • TailwindCss : css framework

1.3 Backend

  • Web Socket : 서버-클라이언트가 실시간으로 정보를 주고받을 수 있는 통신 프로토콜(웹 표준에 의하면 2015년부터 완벽하게 사용하게 되었음, 모든 브라우저에서 지원하지 않았을 때는 socket IO나 특수한 방식으로 사실상 polling방식으로 처리하였음), Live networking
  • Koa : express를 만들었던 팀이 atomic하게 미니멀하여 필요한 메서드만 담은 웹 프레임워크, 좀 더 현대적(?)이다.
  • MongoDB : 채팅 기록을 브로드캐스팅하게 채팅 창에 존재하는 모든 client에게 동일한 정보를 전달하도록 함.

2. 프론트앤드 디자인

2.1 UI 구현

  • main.pug
  • chat, form, input, send로 Id 값을 붙이고, client.js에서 Id에 따른 eventlistener를 설정함.
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 나의 채팅 서비스
        div.p-16.space-y-8
            div#chats.bg-gray-100.p-2 채팅 목록
            form#form.space-x-4.flex
                input#input.flext-1.border.border-gray-200.p-4.rounded(placeholder="채팅을 입력해 보세요.")
                button#send.bg-blue-600.text-white.p-2.rounded 보내기
        script(src="/public/client.js")
  • client.js
// @ts-check
//IIFE
;(() => {
  const socket = new WebSocket(`ws://${window.location.host}/ws`)
  const formEl = document.getElementById('form')
  const chatsEl = document.getElementById('chats')
  /** @type {HTMLInputElement | null} */
  // @ts-ignore
  const inputEl = document.getElementById('input')    // 채팅 창에 입력하는 값

  if (!formEl || !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 array[randomIdx]
  }

  const myNickname = `${pickRandom(adjectives)} ${pickRandom(animals)}`

  formEl.addEventListener('submit', (event) => {    // 서버로 채팅내용 전송
    event.preventDefault()
    socket.send(
      JSON.stringify({
        nickname: myNickname,
        message: inputEl.value,
      })
    )
    inputEl.value = ''
  })

  const drawChats = () => {
    chatsEl.innerHTML = ''
    chats.forEach(({ nickname, message }) => {
      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()

    //alert(event.data)
  })
})()
  • IIFE ; 즉시 실행 함수 표현(IIFE, Immediately Invoked Function Expression)은 정의되자마자 즉시 실행되는 Javascript Function

3. Backend 구현

3.1 Database

3.1.1 DB 관련 환경변수

  • DB에 접근하기 위해서는 user id와 pw가 필수적인데, 이를 변수로 비공개하여 node에서 관리할 필요가 있다.
  • 환경변수 설정 방법2) root에 .env 파일 생성4) .js파일에서 require('dotenv').config()를 import한 후, ${require('dotenv').config().env.MONGO_PASSWORD}로 사용.
    // 변수를 할당해서 적당히 예쁘게 사용할 것(?)
  • 3) .env 파일에 ex) MONGO_PASSWORD = coconut 로 환경변수 생성
  • 1) $ npm i dotenv --save

3.1.2 DB 연동

  • mongo.js는 main.js에서 유저의 채팅메세지를 담는 미들웨어로 사용됨.

const config = require('dotenv').config()  
const { MongoClient } = require('mongodb')

const uri = `mongodb+srv://${config.parsed.MONGO_USER}:${config.parsed.MONGO_PASSWORD}@${config.parsed.MONGO_CLUSTER}.gwyplzx.mongodb.net/?retryWrites=true&w=majority`  
const client = new MongoClient(uri, {  
	useNewUrlParser: true,  
	useUnifiedTopology: true,  
})

module.exports = client

3.2 WebSocket Chatting

  • client.js, mongo.js, main.pug 를 모듈로 불러와서 사용.
// @ts-check

const Koa = require('koa')  
const path = require('path')  
const Pug = require('koa-pug')  
const route = require('koa-route')  
const serve = require('koa-static')  
const websockify = require('koa-websocket')  
const mount = require('koa-mount')  
const mongoClient = require('./mongo')

const app = websockify(new Koa())

// @ts-ignore  
// eslint-disable-next-line no-new  
new Pug({  
	viewPath: path.resolve(\_\_dirname, './views'),  
	app,  
})

app.use(mount('/public', serve('src/public')))

app.use(async (ctx) => {  
	await ctx.render('main')  
})

const \_client = mongoClient.connect()

async function getChatsCollection() {  
const client = await \_client  
	return client.db('chat').collection('chats') // 채팅 내용을 콜렉션에 담음  
}

// Using routes  
app.ws.use(  
route.all('/ws', async (ctx) => { // chatsCollection 안에 chat을 넣는 형태  
	const chatsCollection = await getChatsCollection()  
	const chatsCursor = chatsCollection.find(  
		{},  
		{  
			sort: {  
				createdAt: 1, // 오름차순 정렬  
			},  
		}  
	)


const chats = await chatsCursor.toArray()        //채팅을 다 긁어모아서 array로 만들어서 메세지로 채팅목록에 보내줌
ctx.websocket.send(                                
  JSON.stringify({
    type: 'sync',
    payload: {
      chats,
    },
  })
)
//ctx.websocket.send('Hello, Client!') // string
ctx.websocket.on('message', async (data) => { //websocket을 사용하여 message를 전송한다.
  // @ts-ignore
  //if (typeof data !== 'string') {
  //  return
  //}

  /** @type {Chat} */
  const chat = JSON.parse(data.toString())  // 서버가 재시작 했을 때 이전 데이터가 그대로 보존되기 위해 추가하는
  await chatsCollection.insertOne({            // 필자의 작업 환경에서는 입력 문자들이 buffer로 출력되어, toString()을 추가함. 
    ...chat,                                
    createdAt: new Date(),
  })
  const { message, nickname } = chat
  const { server } = app.ws

  if (!server) {
    return
  }

  server.clients.forEach((client) => {        //broadcast : 주소에 접속한 user들이 같은 내용을 보도록함.    
    client.send(
      JSON.stringify({
        type: 'chat',                        //서버->클라이언트 보내는 메세지
        payload: {
          message,
          nickname,
        },
      })
    )
  })

  console.log(`${nickname} : ${message}`)      // buffer to string
})

})  
)

app.listen(5000)

4. 구현 결과

구현 결과

  • 구현된 결과는 페이지가 업데이트 되어도 db 내용을 그대로 출력한다.
  • User는 새창에서 진입할 때도 broadcast 방식으로 동일한 채팅 내용을 확인할 수 있다.

*패스트캠퍼스 강의를 수강한 내용을 토대로 정리하였습니다.

Comments