프로그래밍 공부하기

socket.io / 실시간 채팅 본문

Project/Practice

socket.io / 실시간 채팅

ihl 2021. 2. 9. 23:35
동작 영상

 

간단한 실시간 채팅 사이트의 서버와 클라이언트를 만들어 보았다. 처음에는 메시지를 보낼 때마다 refresh하여 서버에서 전체 메시지를 다시 가져와 뿌려주도록 작성하였는데, socket.io 라는 모듈을 알게되어 사용하였더니 새로운 메시지가 생길 때마다 자동으로 클라이언트에게 보내줄 수 있었다.

메시지 흐름

  간단히 표현한 흐름은 위와 같다. client가 접속하여 로그인 하면 전체 채팅 메시지를 보내주고, 채팅을 입력하면 다른 클라이언트에게 채팅을 전달함으로써 실시간으로 채팅할 수 있는 것이다. 주요코드는 다음과 같다.

 

1. Client

 1) html

<form action="" class="submit">
  <input type="text" class="inputUser" name="username" />
  <textarea class="inputChat" name="text"></textarea>
  <button id="btn" type="submit">post</button>
</form>
...
<div id="chats"></div>
...
<script src="http://127.0.0.1:3000/socket.io/socket.io.js"></script>
<script src="scripts/app.js"></script>

  클라이언트는 username과 text를 입력하고 post 버튼을 눌러서 메시지를 보낼 수 있다. 메시지들은 id가 chats인 div 안에 표시된다. 위 코드에서 중요한 부분은 script에 socket.io.js를 추가하는 것이다. 서버의 socket.io폴더 내의 socket.io.js를 가져온다는 의미이다. 사실 이 부분은 socket.io를 사용하면 자동으로 생성되고 앞의 localhost 부분을 쓸 필요가 없다고들 하는데 나의 경우 자동으로 생성된 것 같지도 않고 client를 단순히 브라우저 창에 파일 경로를 입력하여 사용하고 있기 때문에 접근이 안되는 것 같아서 위와 같이 localhost를 명시하였다.

 

 2) app.js

// 접속하기
const socket = io('http://127.0.0.1:3000', { transports: ['websocket'] });

// 로그인 정보 보내기
socket.emit("login", {
  name: makeRandomName(),
  userid: "imhyelim1091@gmail.com"
});

// allChats 이벤트를 서버로부터 받는다. -> 모든 메시지 렌더링
socket.on("allChats", function (data) {
  data = JSON.parse(data)
  data.results.forEach(renderMessage);
});

// chat 이벤트를 받는다. -> 하나의 메시지 렌더링
socket.on("chat", function (data) {
  renderMessage(data);
});

// post 버튼 선택 시
btn.addEventListener("click", handleSubmit);
function handleSubmit(event) {
  event.preventDefault(); //form 태그 refresh 방지
  
  //xss 공격을 막기위해 몇가지 문자를 안전한 문자로 변환
  let username = convertSafeStr(window.document.querySelector(".inputUser").value);
  let chat = convertSafeStr(window.document.querySelector(".inputChat").value);

  let message = {
    username: username,
    text: chat
  }
  renderMessage(message);
  socket.emit("chat", message);
}

  client에서 가장 먼저 할 일은 socket을 통해 서버에 접속하는 것이다. 첫 번째 문장을 통해 서버에 접속할 수 있다. 이 때 transports 옵션을 통해 전송 요청 방식을 설정할 수 있다. 설정하지 않으면 기본 값은 polling인데 polling 방식은 클라이언트가 서버에게 계속 응답이 있는지 확인한다. 따라서 과부하가 걸릴 수 있으므로 보편적으로 사용되는 Websocket 방식이나 용도에 따라 Streaming 방식으로 설정해주는 것이 좋다. 또한 클라이언트에서 보내는 방식과 서버에서 받는 방식이 서로 일치하여야 한다는 것에 주의하자.

 

  서버에 접속된 socket으로 클라이언트와 서버는 상호작용할 수 있다. socket의 emit 메소드는 상대를 향해 보내는 메시지 이벤트이고, on은 상대로 부터 받은 메시지 이벤트이며 콜백을 통해 이벤트를 처리할 수 있다. 나의 경우 접속하자마자 'login' 메시지 이벤트를 보낸다. 서버는 login 메시지를 처리하면 바로 'allChats' 라는 이벤트와 함께 서버에 저장된 전체 채팅메시지를 해당 클라이언트에 송신한다. 클라이언트는 allChats를 받으면 모든 메시지를 html로 렌더링 한다. 만약 post 버튼을 통해 클라이언트가 메시지를 보냈다면 메시지 정보와 함께 'chat'이라는 이벤트를 보낸다. 서버는 받은 메시지를 'chat'라는 이벤트로 다른 클라이언트에게 송신한다.

 

 

2. Server

1) server.js

const http = require("http");
const FileHandler = require('./file-handler');
const fileHandler = new FileHandler(__dirname + "/savedata.txt");
let responseObj = { results: [] };

const server = http.createServer();

const io = require('socket.io')(server);

//연결되었다면 연결된 소켓에 이벤트 등록
io.on('connection', socket => {
    socket.on('login', function (data) {
    
        // socket에 클라이언트 정보를 저장
        socket.name = data.name;
        socket.userid = data.userid;

        //연결된 클라이언트에게 전체 채팅 메시지를 송신
        fileHandler.read().then((data) => {
            responseObj = JSON.parse(data);
            socket.emit('allChats', data);
        })

        // 클라이언트로부터 메시지 수신 시 데이터 저장
        socket.on('chat', function (data) {
            //console.log(`${socket.name} + ": " + ${data}`);
            responseObj.results.push(data);
            fileHandler.write(JSON.stringify(responseObj));
            
            // 나머지 클라이언트에게 메시지 전달
            socket.broadcast.emit('chat', data);
        })

        // 강제 종료 시 처리
        socket.on('forceDisconnect', function () {
            socket.disconnect();
        })

		//접속 종료 시 처리
        socket.on('disconnect', function () {
            console.log('Disconnect: ' + socket.name);
        });
    });
    socket.on('disconnect', () => { console.log("disconnect!") });
});
server.listen(3000, "127.0.0.1");

  io에 'connection' 이벤트 콜백을 등록하여 연결된 각 소켓에 대해 이벤트를 등록하고 소켓을 통해 상호작용을 수행할 수 있다. 즉, io는 하나고 socket은 클라이언트 수만큼 있을 수 있는 것이다. 따라서 io.emit()을 하면 접속한 모든 클라이언트에게 메시지를 보낼 수 있다. 반대로 socket.emit()은 해당 소켓을 사용하는 특정 클라이언트에게만 메시지를 보내는 것이다. 예를 들어 위 코드는 io에 'connection'이벤트 발생 시 이벤트를 발생시킨 접속한 클라이언트에게만 전체 채팅 메시지를 송신한다. 불가피하게 io에서 특정 클라이언트에게 메시지를 전송해야한다면 io.to(id).emit() 을 사용할 수 있다. 이 때 id는 socket 객체의 id 속성값이다.

 

  server의 socket은 추가적으로 socket.broadcast.emit()을 사용할 수가 있다. 이 메시지는 해당 소켓을 제외한 다른 모든 클라이언트에게 메시지를 전송하는 것이다. 위 코드에서 클라이언트로부터 'chat' 이벤트 메시지가 왔다면 socket.broadcast.emit()을 통해 나머지 클라이언트에게 'chat' 이벤트 메시지를 보내는 부분이 그것이다.

2) socket.io.js

const io = require('socket.io');

  클라이언트 부분에서 서버의 socket.io.js의 스크립트를 로드했었기 때문에 해당 파일을 만들어주어야한다. 파일의 내용은 위와같이 쓰면 된다.

 

 3)file-handler.js

const fs = require("fs");

class FileHandler {
  constructor(path) {
    this.path = path;
  }

  async read() {
    const data = await new Promise((resolve, reject) => {
      fs.readFile(this.path, "utf-8", (err, data) => {
        if (err) reject(err);
        else resolve(data);
      })
    })
    return data;
  }

  async write(message) {
    const result = await new Promise((resolve, reject) => {
      fs.writeFile(this.path, message, { encoding: "utf8", flag: "w" }, (err, data) => {
        if (err) reject(err);
        else resolve({ id: "success" });
      })
    })
    return result;
  }
}

 

  이 프로젝트에서는 File system을 사용하여 지금까지 입력한 채팅메시지들을 저장해놓았다. 파일 시스템의 경우 비동기적으로 작동하기 때문에 async-await를 사용하여 메소드 호출 후 .then() 으로 연결하여 처리할 수 있게 작성하였다.

 


참고: 

 

Node.js(Express)와 Socket.io | PoiemaWeb

WebSocket, Socket.io를 사용한 실시간 채팅 애플리케이션

poiemaweb.com

 

'Project > Practice' 카테고리의 다른 글

TypeScript / Project Timer  (0) 2021.04.05
GraphQL / Tour Sight Search  (0) 2021.02.21
JSAnimation / BrawlStars GunFight!  (0) 2021.01.18
DOM / Twittller  (0) 2020.12.30
JS / 계산기  (0) 2020.12.17
Comments