ABOUT ME

chanho Yoon
chyoon0512@gmail.com


깃허브


블로그 이사!

이 블로그로 이사했어요!!


Today
-
Yesterday
-
Total
-
  • socket.io 이용한 채팅
    Programming/NodeJS 2020. 4. 2. 15:48
    반응형

    socket.io를 이용한 채팅 기능을 만들면서 복습겸 정리, 아 이런거구나 감을 잡는데 도움이 되었으면 좋겠습니다.

    좌 - 닉네임 입력전 , 우 - 닉네임 입력 하면 채팅 view 보이게
    누군가 입장시 알림


    // home.pug
    
    <!DOCTYPE html>
    html(lang="ko")
      head
        meta(charset="UTF-8")
        meta(name="viewport", content="width=device-width, initial-scale=1.0")
        link(rel="stylesheet", href="styles/styles.css")
    
        title Chat
      body
        #jsNotifications // 유저입장시 입장알림 
        .loginBox // 닉네임 입력 안했을 때 보이는 창
          form#jsLogin
            input(placeholder="닉네임!", type="text")
        .chatContainer#jsChat // 닉네임 입력 했을때 보이는 채팅창 
          .chat
            ul.chat__messages#jsMessages
            form.chat__form#jsSendMessage
              input(placeholder="메시지 입력" type="text")
    
        script.
          window.events = !{events}
        script(src="/socket.io/socket.io.js")
        script(src="/js/main.js")
        
    // styles.scss 
    body {
      background-color: $greyColor;
      font-family: 'Lato', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
      display: flex;
      justify-content: center;
      align-items: center;
      min-height: 100%;
      .loginBox,
      .chatContainer {
        display: none;
      }
      &.loggedOut {
        .loginBox {
          display: block;
        }
      }
      &.loggedIn {
        .chatContainer {
          display: block;
        }
      }
    }
    

    server.js

    import { join } from 'path';
    import express from 'express';
    import socketIO from 'socket.io';
    import morgan from 'morgan';
    import socketController from './socketController';
    import events from './events';
    
    const PORT = 4000;
    const app = express();
    
    app.set('view engine', 'pug');
    app.use(morgan('dev'));
    app.set('views', join(__dirname, 'views'));
    app.use(express.static(join(__dirname, 'static')));
    
    app.get('/', (req, res) => res.render('home', { events: JSON.stringify(events) }));
    
    const handleListening = () => console.log(`✅ Server running: http://localhost:${PORT}`);
    const server = app.listen(PORT, handleListening);
    // socket io
    const io = socketIO.listen(server);
    
    io.on('connection', socket => socketController(socket));
    

    home.pug : 기본적으로 styles.scss에서 .loginBox와 chatContainer에 기본 display:none을 주고, body class 로 로그인창을 숨기고 채팅창을 보이게하고 함

      .loginBox,
      .gameContainer {
        display: none;
      }
      &.loggedOut {
        .loginBox {
          display: block;
        }
      }
      &.loggedIn {
        .gameContainer {
          display: block;
        }
      }

    위의 scss에서 body 클래스에 이름을 주는 처리는 아래 login.js에서 처리

    // login.js
    import { initSockets } from './sockets';
    
    const body = document.querySelector('body');
    const loginForm = document.getElementById('jsLogin');
    
    const NICKNAME = 'nickname';
    const LOGGED_OUT = 'loggedOut';
    const LOGGED_IN = 'loggedIn';
    const nickname = localStorage.getItem(NICKNAME);
    
    const logIn = nickname => {
      const socket = io('/');            //소켓 연결
      socket.emit(window.events.setNickname, { nickname }); //socketController로 보냄
      initSockets(socket); //소켓 저장
    };
    
    if (nickname === null) {
      body.className = LOGGED_OUT;
    } else {
      body.className = LOGGED_IN;
      logIn(nickname);
    }
    
    const handleFromSubmit = event => {
      event.preventDefault();
      const input = loginForm.querySelector('input'); // 입력한 value 값 
      const { value } = input;
      input.value = '';
      localStorage.setItem(NICKNAME, value); // 입력한 value값 localStorage에 저장
      body.className = LOGGED_IN;  // body 클래스명 loggedIn으로 변경 -> 채팅창 보이고, 닉네임 창 사라짐
      logIn(value);
    };
    
    if (loginForm) {
      loginForm.addEventListener('submit', handleFromSubmit); //닉네임 submit 이벤트 발생
    }
    
    

    events.js : socket 전역 이벤트를 설정하기 위한 events 객체 생성 및 export 

    const events = {
      setNickname: 'setNickname',
      newUser: 'newUser',
      byeUser: 'byeUser',
      disconnect: 'disconnect',
      setMessage: 'setMessage',
      receiveMessage: 'receiveMessage'
    };
    
    export default events;
    

    socketController.js : socket 이벤트를 받아 socket.js로 보내고 socket.js에서 해당하는 이벤트를 찾아 handler를 실행하여 처리

    // socketController.js
    import events from './events';
    
    const socketController = socket => {
      const broadcast = (event, data) => socket.broadcast.emit(event, data);
    
      socket.on(events.setNickname, ({ nickname }) => {
        broadcast(events.newUser, { nickname });
        socket.nickname = nickname;
      });
      socket.on(events.disconnect, ({ nickname }) => {
        broadcast(events.byeUser, { nickname: socket.nickname });
      });
      // 유저가 메시지 입력하면 자신 이외 사용자에게
      socket.on(events.setMessage, ({ message }) => {
        broadcast(events.receiveMessage, { message, nickname: socket.nickname });
      });
    };
    
    export default socketController;
    

    sockets.js : socketController.js에서 보내온 이벤트를 받아서 handler 처리

    // sockets.js
    import { handleNewUser, handleByeUser } from './notifications';
    import { handleReceiveMessage } from './chat';
    
    let socket = null;
    
    export const getSocket = () => socket; // 이후에 socket 가졍오기 위한 함수
    export const updateSocket = aSocket => {
      socket = aSocket;
    };
    // 로그인 한 시점에 socket을 시작하도록
    export const initSockets = aSocket => {
      const { events } = window;
      updateSocket(aSocket);
      aSocket.on(events.newUser, handleNewUser);
      aSocket.on(events.byeUser, handleByeUser);
      aSocket.on(events.receiveMessage, handleReceiveMessage); //본인 이외 다른 유저에게 보여질 메시지 소켓
    };

    notifications.js : 유저 입장시 좌측 하단에 알림 발생

    const notifications = document.getElementById('jsNotifications');
    
    const fireNotification = (text, color) => {
      const notification = document.createElement('div');
      notification.innerText = text;
      notification.style.backgroundColor = color;
      notification.className = 'notification';
      notifications.appendChild(notification);
    };
    
    export const handleNewUser = ({ nickname }) => {
      fireNotification(`${nickname} 님 입장!`, 'rgb(0, 122, 255)');
    };
    
    export const handleByeUser = ({ nickname }) => {
      fireNotification(`${nickname} 님 퇴장!`, 'rgb(255, 149, 0)');
    };
    

    notifications.scss

    .notification {
      position: absolute;
      left: 50px;
      bottom: 0px;
      opacity: 0;
      box-shadow: 0 4px 6px rgba(50, 50, 93, 0.11), 0 1px 3px rgba(0, 0, 0, 0.08);
      border-radius: 20px;
      padding: 10px 20px;
      background-color: white;
      font-size: 20px;
      color: white;
      animation: notifi 0.5s ease-out forwards, dissapear 5s ease-in forwards;
    }
    
    @keyframes notifi {
      0% {
        transform: translateY(30px);
        opacity: 0;
      }
      10% {
        transform: translateY(-45px);
        opacity: 1;
      }
      100% {
        transform: translateY(-30px);
        opacity: 1;
      }
    }
    @keyframes dissapear {
      from {
        opacity: 1;
      }
      to {
        opacity: 0;
      }
    }
    

    chat.js : 채팅 submit 이벤트 발생시 처리

    import { getSocket } from './sockets';
    
    const messages = document.getElementById('jsMessages');
    const sendMessage = document.getElementById('jsSendMessage');
    
    const appendMessage = (text, nickname) => {
      const li = document.createElement('li');
      li.className = `${nickname ? 'out' : 'self'}`;
      li.innerHTML = `
      <span class="author ${nickname ? 'out' : 'self'}">${nickname ? nickname + ' : ' + text : text} </span> 
      `;
      messages.appendChild(li);
      messages.scrollTop = messages.scrollHeight;
    };
    
    const handleSendMessage = event => {
      event.preventDefault();
      const input = sendMessage.querySelector('input');
      const message = input.value;
      input.value = '';
      appendMessage(message);
    
      // 나 이외 전체 사용자에게 broadcast
      const socket = getSocket();
      socket.emit(window.events.setMessage, { message: message });
    };
    
    if (sendMessage) {
      sendMessage.addEventListener('submit', handleSendMessage);
    }
    
    // 본인 이외에 유저가 보낸 메시지를 받음
    export const handleReceiveMessage = ({ nickname, message }) => {
      appendMessage(message, nickname);
    };
    

    chat.scss

    .chat {
      display: flex;
      flex-direction: column;
      align-items: center;
      position: relative;
      .chat__messages {
        padding: 10px 8px;
        display: flex;
        flex-direction: column;
        width: 240px;
        height: 70%;
        height: 55vh;
        overflow: scroll;
        box-shadow: 0px 0px 20px rgba(50, 50, 93, 0.11), 0 1px 3px rgba(0, 0, 0, 0.08);
        background-color: white;
        li:not(:last-child) {
          margin-bottom: 3px;
        }
        li.self {
          display: flex;
          justify-content: flex-end;
        }
        li.out {
          display: flex;
          justify-content: flex-start;
        }
        .author {
          display: flex;
          padding: 5px 10px;
          border-radius: 15px;
          max-width: 200px;
          font-size: 13px;
          color: white;
        }
        .author.self {
          justify-content: flex-end;
          box-shadow: 0px 0px 20px rgba(50, 50, 93, 0.11), 0 1px 3px rgba(0, 0, 0, 0.08);
          background-color: rgb(0, 122, 255);
        }
        .author.out {
          justify-content: flex-start;
          box-shadow: 0px 0px 20px rgba(50, 50, 93, 0.11), 0 1px 3px rgba(0, 0, 0, 0.08);
          background-color: rgb(90, 200, 250);
        }
      }
      .chat__form {
        position: relative;
        input {
          width: 250px;
          background-color: white;
          box-shadow: 0px 8px 20px rgba(50, 50, 93, 0.11), 0 1px 3px rgba(0, 0, 0, 0.08);
          padding-left: 20px;
          border-radius: 5px;
        }
      }
    }
    

    li 태그와 span 태그에 self(본인), out(다른유저) 클래스를 구분지어서 appendMessage()에 text하나의 값만 전달되면 본인의 메시지 text만  text와 nickname이 들어온다면 다른 유저의 닉네임과 메시지를 보여줌으로 본인 메시지인지 아닌지 구분하여 클래스 이름을 self,out으로 구분지어주고 본인 메시지 작성시 채팅창 오른쪽에 표시 다른 유저의 메시지일 경우 왼쪽에 표시되게끔 


     

    댓글

Designed by Tistory.