728x90
728x90

#3.1 Call Controls

stream은 track 이라는 기능을 제공해준다. 비디오, 오디오, 자막 등이 각각 하나의 track 이 될 수 있다.  

handleMuteClick을 클릭했을 때, sream 을 가져와보자. (Line 2)

function handleMuteClick() {
  console.log(myStream.getAudioTracks());
  if (!muted) {
    muteBtn.innerText = "Unmute";
    muted = true;
  } else {
    muteBtn.innerText = "Mute";
    muted = false;
  }
}

console 창에서 enabled:true 임을 확인할 수 있다. 

Mute Button, Camera Button을 클릭했을 때, 해당 속성을 true<->false 로 변환하기 위해 아래와 같이 작성한다. 

 

이제 유저가 가지고 있는 카메라들의 목록을 만들어보자.

getUserMedia() 를 활용하여 유저의 카메라와 오디오를 가져온다. 

enumateDevces() : 모든 장치와 미디어를 알려준다. (컴퓨터에 연결된 장치이거나 모바일 장치 등) -> 해당 함수를 활용해서 리스틀 가져와보자. 

https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/enumerateDevices

 

MediaDevices.enumerateDevices() - Web APIs | MDN

The MediaDevices method enumerateDevices() requests a list of the available media input and output devices, such as microphones, cameras, headsets, and so forth. The returned Promise is resolved with a MediaDeviceInfo array describing the devices.

developer.mozilla.org

 

 

위 레퍼런스를 참고하여 getCameras() 메소드를 신규 생성한다. 

const socket = io();

const myFace = document.getElementById("myFace");
const muteBtn = document.getElementById("mute");
const cameraBtn = document.getElementById("camera");

let myStream;
let muted = false;
let cameraOff = false;

async function getCameras(){
  try{
    const devices = await navigator.mediaDevices.enumerateDevices();
    console.log(devices);
  } catch(e) {
    console.log(e)
  }
}

async function getMedia() {
  try {
    myStream = await navigator.mediaDevices.getUserMedia({
      audio: true,
      video: true,
    });
    myFace.srcObject = myStream;
    await getCameras;
  } catch (e) {
    console.log(e);
  }
}

getMedia();

function handleMuteClick() {
  myStream.getAudioTracks().forEach((track) => (track.enabled = !track.enabled));
  if (!muted) {
    muteBtn.innerText = "Unmute";
    muted = true;
  } else {
    muteBtn.innerText = "Mute";
    muted = false;
  }
}
function handleCameraClick() {
  myStream.getVideoTracks().forEach((track) => (track.enabled = !track.enabled));
  if (cameraOff) {
    cameraBtn.innerText = "Turn Camera Off";
    cameraOff = false;
  } else {
    cameraBtn.innerText = "Turn Camera On";
    cameraOff = true;
  }
}

muteBtn.addEventListener("click", handleMuteClick);
cameraBtn.addEventListener("click", handleCameraClick);

우리는 console 창의 리스트에서 pc의 모니터 및 내장 스피커 등에 대한 정보를 얻을 수 있고, 우리는 해당 array를 통해서 video input 만 선택해야한다.

(Line 15 수정)

const socket = io();

const myFace = document.getElementById("myFace");
const muteBtn = document.getElementById("mute");
const cameraBtn = document.getElementById("camera");

let myStream;
let muted = false;
let cameraOff = false;

async function getCameras(){
  try{
    const devices = await navigator.mediaDevices.enumerateDevices();
    cameras = devices.filter(device => device.kind === "videoinput");
    //console.log(devices);
  } catch(e) {
    console.log(e)
  }
}

async function getMedia() {
  try {
    myStream = await navigator.mediaDevices.getUserMedia({
      audio: true,
      video: true,
    });
    myFace.srcObject = myStream;
    await getCameras;
  } catch (e) {
    console.log(e);
  }
}

getMedia();

function handleMuteClick() {
  myStream.getAudioTracks().forEach((track) => (track.enabled = !track.enabled));
  if (!muted) {
    muteBtn.innerText = "Unmute";
    muted = true;
  } else {
    muteBtn.innerText = "Mute";
    muted = false;
  }
}
function handleCameraClick() {
  myStream.getVideoTracks().forEach((track) => (track.enabled = !track.enabled));
  if (cameraOff) {
    cameraBtn.innerText = "Turn Camera Off";
    cameraOff = false;
  } else {
    cameraBtn.innerText = "Turn Camera On";
    cameraOff = true;
  }
}

muteBtn.addEventListener("click", handleMuteClick);
cameraBtn.addEventListener("click", handleCameraClick);

 

 

다음 단계로는 화면에서 우리가 다른 유저의 카메라를 선택할 수 있도록 하는 기능을 추가해주자.

home.pug에 line 16을 추가하자.

 

다음 app.js에 line6과 같이 ID를 통해 Element를 가져온다.

getCameras 메소드 안에, 현재 우리의 디바이스 내 camera 에 대해 각각 option설정을 해주고 deviceID를 value에 저장한다. 

const socket = io();

const myFace = document.getElementById("myFace");
const muteBtn = document.getElementById("mute");
const cameraBtn = document.getElementById("camera");
const camerasSelect = document.getElementById("cameras");

let myStream;
let muted = false;
let cameraOff = false;

async function getCameras() {
  try {
    const devices = await navigator.mediaDevices.enumerateDevices();
    const cameras = devices.filter((device) => device.kind === "videoinput");
    cameras.forEach((camera) => {
      const option = document.createElement("option");
      option.value = camera.deviceId;
      option.innerText = camera.label;
      camerasSelect.appendChild(option);
    });
  } catch (e) {
    console.log(e);
  }
}

async function getMedia() {
  try {
    myStream = await navigator.mediaDevices.getUserMedia({
      video: true,
      audio: true,
    });
    myFace.srcObject = myStream;
    await getCameras();
  } catch (e) {
    console.log(e);
  }
}

getMedia();

function handleMuteClick() {
  myStream
    .getAudioTracks()
    .forEach((track) => (track.enabled = !track.enabled));
  if (!muted) {
    muteBtn.innerText = "Unmute";
    muted = true;
  } else {
    muteBtn.innerText = "Mute";
    muted = false;
  }
}
function handleCameraClick() {
  myStream
    .getVideoTracks()
    .forEach((track) => (track.enabled = !track.enabled));
  if (cameraOff) {
    cameraBtn.innerText = "Turn Camera Off";
    cameraOff = false;
  } else {
    cameraBtn.innerText = "Turn Camera On";
    cameraOff = true;
  }
}

muteBtn.addEventListener("click", handleMuteClick);
cameraBtn.addEventListener("click", handleCameraClick);

 

결과이다.

내 디바이스 내 카메라 중 하나를 option으로 선택할 수 있다.

현재 내 디바이스에는 내장 카메라 하나가 있기 때문에 아래 Elements창에서 option 아이템을 한 개만 확인할 수 있다.

 

 

 

 

728x90
728x90

#2.6 Room Notifications

이제 사용자가 방을 떠날 때 이를 알리는 기능을 구현해보자.

여기서 disconnecting 과 disconnected 를 구별해야한다.

disconnecting 은 완전히 disconnected 되기 전의 상태이기 때문에 메세지를 보낼 수 있는 것이다.  

 

socket.rooms 를 하면 아래와 같이 Set 을 의미한다. Set 이기 때문에 forEach 즉, 반복이 가능하다. 

qay... 는 room.id 이며 myroom~ 은 roomName 이다. 

그리고 server.js 에 아래의 line 31~33 과 같이 disconnecting 이벤트에 대해 코드를 추가해준다.

import http from "http";
import SocketIO from "socket.io"
import WebSocket from "ws";
import express from "express";

const app = express();

//set the view
app.set("view engine", "pug");
app.set("views", __dirname + "/views");
app.use("/public", express.static(__dirname + "/public")); //user가 public 으로 이동하면 __dirname+/public 폴더를 보여주는 것. 
app.get("/", (req, res) => res.render("home"));//render 
app.get("/*", (req, res) => res.redirect("/"));

const handleListen = () => console.log('Listening on http://localhost:3000');
//app.listen(3000, handleListen);

//http의 서버
const httpServer = http.createServer(app); //app은 requestListener 의 경로. express application으로부터 서버 생성. 
const wsServer  = SocketIO(httpServer);

wsServer.on("connection", (socket) => {
    socket.onAny((event) => {   //socket 에 있는 모든 이벤트 모니터링 가능
        console.log(`Socket Event: ${event}`);
    });
    socket.on("enter_room", (roomName, done) => {
        socket.join(roomName);
        done();
        socket.to(roomName).emit("welcome");    //모든 사람에게 welcome 이벤트 Emit
    });
    socket.on("disconnecting", () => {
        socket.rooms.forEach(room => socket.to(room).emit("bye"));
    })
});

httpServer.listen(3000, handleListen);

 

app.js 에도 앞서 했던 welcome 과 동일한 방식으로 bye 도 추가해준다.

socket.on("bye", () => {
    addMessage("Someone Left!");  
});

 

방에 들어갔다가 나갔음을 알 수 있다. 

 

 

이제 메세지를 보내보자. 

room 안에 있는 message 를 from 에서 찾아서 -> addEventListener 를 추가한뒤 -> new_message event 를 보내면 된다. 

먼저 방에 들어왔음을 보여주는 showRoom 메서드 안에서 from 을 찾아보자. (line 32~33)

 

그 후 line 17~25 와 같이 handleMessageSubmit 을 작성한다. 이 메소드는 우리가 브라우저에서 입력한 메세지 내용을 backend로 전송해주는 역할을 한다.  전송 시 roomName도 함께 인자로 보내줘서 backend 가 어디 room 으로 보낼지 식별 가능하도록 한다. 

 

handleMessageSubmit 에서 input.value 를 초기화 시켜줄때 주의해야한다.

왜냐하면 emit 을 통해 backend 로 보내고 다시 메세지를 front 로 받는 과정에서 

input.value = "" 를 통해 초기화 되어버리기 때문에 브라우저에 출력이 되지 않게 되는 문제가 있기 때문이다. 

그렇기 때문에 value 라는 변수 선언을 통해 값을 저장해놓고 (line20)

line22 에서 그 저장된 값을 쓰도록 함으로써 line 24에 영향을 받지 않도록 한다. 

 

//app.js

const socket = io();

const welcome = document.getElementById("welcome");
const form = welcome.querySelector("form");
const room = document.getElementById("room");

room.hidden = true;
let roomName;

function addMessage(message){
    const ul = room.querySelector("ul");
    const li = document.createElement("li");
    li.innerText = message;
    ul.appendChild(li);
}

function handleMessageSubmit(event){
    event.preventDefault();
    const input = room.querySelector("input");
    const value = input.value;
    socket.emit("new_message", input.value, roomName, () => {     //백엔드에 입력한 메세지 전송 
        addMessage(`You: ${value}`);  //대화창에 메세지 출력 
    });
    input.value = "";
}

function showRoom(){
    welcome.hidden = true;
    room.hidden = false; 
    const h3 = room.querySelector("h3");
    h3.innerText = `Room ${roomName}`;
    const form = room.querySelector("form");
    form.addEventListener("submit", handleMessageSubmit);
}

function handleRoomSubmit(event){
    event.preventDefault();
    const input = form.querySelector("input");
    socket.emit("enter_room", input.value, showRoom);
    roomName = input.value;
    input.value = "";
}
form.addEventListener("submit", handleRoomSubmit);

socket.on("welcome", () => {
    addMessage("Someone Joined!");  
}); 

socket.on("bye", () => {
    addMessage("Someone Left!");  
});

socket.on("new_message", (addMessage)); 
// = socket.on("new_message", (msg) => {addMessage});

front 에서 보낸 메세지를 이제 backend 에서 받아보자. 

(line 34~37 추가)

import http from "http";
import SocketIO from "socket.io"
import WebSocket from "ws";
import express from "express";

const app = express();

//set the view
app.set("view engine", "pug");
app.set("views", __dirname + "/views");
app.use("/public", express.static(__dirname + "/public")); //user가 public 으로 이동하면 __dirname+/public 폴더를 보여주는 것. 
app.get("/", (req, res) => res.render("home"));//render 
app.get("/*", (req, res) => res.redirect("/"));

const handleListen = () => console.log('Listening on http://localhost:3000');
//app.listen(3000, handleListen);

//http의 서버
const httpServer = http.createServer(app); //app은 requestListener 의 경로. express application으로부터 서버 생성. 
const wsServer  = SocketIO(httpServer);

wsServer.on("connection", (socket) => {
    socket.onAny((event) => {   //socket 에 있는 모든 이벤트 모니터링 가능
        console.log(`Socket Event: ${event}`);
    });
    socket.on("enter_room", (roomName, done) => {
        socket.join(roomName);
        done();
        socket.to(roomName).emit("welcome");    //모든 사람에게 welcome 이벤트 Emit
    });
    socket.on("disconnecting", () => {
        socket.rooms.forEach(room => socket.to(room).emit("bye"));
    })
    socket.on("new_message" , (msg, room, done) => {
        socket.to(room).emit("new_message", msg);
        done();
    })
});

httpServer.listen(3000, handleListen);

 

그 후 backend 에서 front 로 보내는 socket.to(room).emit("new_message", msg) 부분을 

front 에 출력하기 위해 아래와 같이 함수를 추가해준다. 

socket.on("new_message", (addMessage));

실행해보자. 

You 를 통해 어디서 보낸 메세지인지 식별이 가능해졌다.

728x90
728x90

#2.5 Room Message

방에 처음 들어갔을 때 그 방 모든 사람들에게 메세지를 전송해보자. 

line 29 와 같이 backend 쪽에 코드를 추가한다. 

//server.js

import http from "http";
import SocketIO from "socket.io"
import WebSocket from "ws";
import express from "express";

const app = express();

//set the view
app.set("view engine", "pug");
app.set("views", __dirname + "/views");
app.use("/public", express.static(__dirname + "/public")); //user가 public 으로 이동하면 __dirname+/public 폴더를 보여주는 것. 
app.get("/", (req, res) => res.render("home"));//render 
app.get("/*", (req, res) => res.redirect("/"));

const handleListen = () => console.log('Listening on http://localhost:3000');
//app.listen(3000, handleListen);

//http의 서버
const httpServer = http.createServer(app); //app은 requestListener 의 경로. express application으로부터 서버 생성. 
const wsServer  = SocketIO(httpServer);

wsServer.on("connection", (socket) => {
    socket.onAny((event) => {   //socket 에 있는 모든 이벤트 모니터링 가능
        console.log(`Socket Event: ${event}`);
    });
    socket.on("enter_room", (roomName, done) => {
        socket.join(roomName);
        done();
        socket.to(roomName).emit("welcome");    //모든 사람에게 welcome 이벤트 Emit
    });
});

참고) socket.to

io.on("connection", (socket) => {

  // to one room
  socket.to("others").emit("an event", { some: "data" });

  // to multiple rooms
  socket.to("room1").to("room2").emit("hello");

  // or with an array
  socket.to(["room1", "room2"]).emit("hello");

  // a private message to another socket
  socket.to(/* another socket id */).emit("hey");

  // WARNING: `socket.to(socket.id).emit()` will NOT work
  // Please use `io.to(socket.id).emit()` instead.
});

 

//server.js

backend 에 Line 29와 같이 작성한다.

해당 내용은 같은 roomName을 가진 room 에 있는 socket 들 중 나를 제외한 모든 socket 에 "welcome" 이벤트를 emit 하는 것이다.

backend 에서 보낸 "welcome" 이벤트를 front 에서 받아보자.

//app.js

line 33-35에 "welcome" 이벤트에 대한 액션을 취한다.

액션을 위해 10~15에 addMessage 메소드를 작성한다. 

"Someone Joined!" 라는 message 를 li 에 담고,

그 message 들을 append 함으로써 계속 아래로 달리게 한다. 

const socket = io();

const welcome = document.getElementById("welcome");
const form = welcome.querySelector("form");
const room = document.getElementById("room");

room.hidden = true;
let roomName;

function addMessage(message){
    const ul = room.querySelector("ul");
    const li = document.createElement("li");
    li.innerText = message;
    ul.appendChild(li);
}

function showRoom(){
    welcome.hidden = true;
    room.hidden = false;
    const h3 = room.querySelector("h3");
    h3.innerText = `Room ${roomName}`;
}

function handleRoomSubmit(event){
    event.preventDefault();
    const input = form.querySelector("input");
    socket.emit("enter_room", input.value, showRoom);
    roomName = input.value;
    input.value = "";
}
form.addEventListener("submit", handleRoomSubmit);

socket.on("welcome", () => {
    addMessage("Someone Joined!");  
})

 

실행해보자.

왼쪽에서 My Room1 이라는 방을 생성한다. 왼쪽은 방에 들어가진 모습이다. 

이제 오른쪽에서 생성한 방인 My Room1 에 들어가보자.

들어가는 순간 왼쪽에 Someone Joined! 라는 메세지가 출력된다. (오른쪽은 방에 새로 들어간 본인이라 출력 안됨 )

 

 

크롬 시크릿 창을 두개 더 띄워서 테스트 해본 화면이다.

동일하게 본인을 제외하고 메세지가 출력됨을 확인할 수 있다. 

728x90
728x90

#2.4 Rooms

Room 이란? : 서로 소통 가능한 socket 들의 그룹

 

모든 websocket이 서로 대화할 필요는 없다. 그저 room 안에 몇개의 websocket 들끼리만 대화하면 된다.

꼭 Chat room 뿐 아니라 websocket 들은 그룹으로 묶일 필요가 있는 것이다. 

예를 들어 배달 앱이라고 하더라도, 배달 기사의 위치를 나한테 알려주기 위해서는 

배달기사 <-> 나 와의 socket 이 room 안에 따로 있어야하는 것이다. 

socketIO는 join 메소드를 통해 room 기능을 제공하고 있다.

 

참고)socket.id

https://socket.io/docs/v4/server-api/#socketid

 

Server API | Socket.IO

Server

socket.io

 

참고) socket. onAny : 임의의 이벤트에서든 실행됨으로써 모든 socket 에 대한 이벤트를 모니터링 할 수 있음 

https://socket.io/docs/v4/server-api/#socketonanycallback

 

Server API | Socket.IO

Server

socket.io

 

//server.js

line 23~30 을 수정해주었다. 

  • socket이 어느 방에 있는지 알기 -> socket.rooms
  • 방에 들어가기 -> socket.join(room1) , socket.join(room1, room2) -> 동시에 여러 방 접속 가능
  • socket 구분 : socket.id
import http from "http";
import SocketIO from "socket.io"
import WebSocket from "ws";
import express from "express";

const app = express();

//set the view
app.set("view engine", "pug");
app.set("views", __dirname + "/views");
app.use("/public", express.static(__dirname + "/public")); //user가 public 으로 이동하면 __dirname+/public 폴더를 보여주는 것. 
app.get("/", (req, res) => res.render("home"));//render 
app.get("/*", (req, res) => res.redirect("/"));

const handleListen = () => console.log('Listening on http://localhost:3000');
//app.listen(3000, handleListen);

//http의 서버
const httpServer = http.createServer(app); //app은 requestListener 의 경로. express application으로부터 서버 생성. 
const wsServer  = SocketIO(httpServer);

wsServer.on("connection", (socket) => {
    socket.onAny((event) => {   //socket 에 있는 모든 이벤트 모니터링 가능
        console.log(`Socket Event: ${event}`);
    });
    socket.on("enter_room", (roomName, done) => {
        console.log(socket.id);
        console.log(socket.rooms); // Set { <socket.id> 
        socket.join(roomName);
        console.log(socket.rooms); // Set { <socket.id>, "room1" }
        
        setTimeout(() => {
            done();
        }, 15000);
    });
});


//websocket 서버
//const wss = new WebSocket.Server({ httpServer });


// function handleConnection(socket) {
//     console.log(socket)
// }

// const sockets = [];

// wss.on("connection", (socket) => {
//     sockets.push(socket);
//     socket["nickname"] = "Anon";    //처음엔 익명으로 설정 
//     console.log("Connected to Browser ✅");
//     socket.on("close", () => {
//         console.log("Disconnected from the Browser ❌");
//     });
//     socket.on("message", (msg) => { 
//         const message = JSON.parse(msg);        //string -> Object
//         //console.log(message, message.toString('utf-8'));
//         switch(message.type){
//             case "new_message" :
//                 //sockets.forEach((aSocket) => aSocket.send(message.payload.toString('utf-8'))); //채팅 메세지만 출력하기 위해 이것만 브라우저에 보냄 
//                 sockets.forEach((aSocket) => aSocket.send(`${socket.nickname}: ${message.payload}`)); //누가 보냈는지, 뭘 보냈는지 
//             case "nickname" :
//                 //console.log(message.payload.toString('utf-8'));     
//                 socket["nickname"] = message.payload;
//         }
//     });
    
// });

httpServer.listen(3000, handleListen);

 

코드 수정 후 room name 을 브라우저에서 입력했더니 backend에서 아래와 같이 확인할 수 있었다.

qay~ 부분이 socket.id 이며,

Set(1) 은 모든 socket 은 처음에 private room 을 한개씩 가지고 있기 때문에 RoomName 이 설정되지 않은 상태의 room 이며

Set(2)는 사용자가 브라우저에 myroom~이라고 RoomName을 지정했을 때의 Room 이다.

 

 

 

 

참고)

  • 방 떠나기 : socket.leave()
  • 방 전체에 메세지 broadcasting : socket.to("room1")
io.on("connection", (socket) => {

  // to one room
  socket.to("others").emit("an event", { some: "data" });

  // to multiple rooms
  socket.to("room1").to("room2").emit("hello");

  // or with an array
  socket.to(["room1", "room2"]).emit("hello");

  // a private message to another socket
  socket.to(/* another socket id */).emit("hey");

  // WARNING: `socket.to(socket.id).emit()` will NOT work
  // Please use `io.to(socket.id).emit()` instead.
});

 

위 기능들을 활용해서, 방에 참가했을 때 방 안의 모든 사람들에게 참여했음을 알리는 메세지를 전송해보자. 

 

먼저 HTML 을 수정한다. (div room 추가)

doctype html
html(lang="en")
    head
        meta(charset="UTF-8")
        meta(http-equiv="X-UA-Compatible", content="IE=edge")
        meta(name="viewport", content="width=device-width, initial-scale=1.0")
        title Noom
        link(rel="stylesheet", href="https://unpkg.com/mvp.css")
    body 
        header
            h1 Noom 
        main 
            div#welcome
                form
                    input(placeholder="room name", required, type = "text")
                    button Enter Room 
            div#room
                ul
                form
                    input(placeholder="message", required, type = "text")
                    button Send 
        script(src="/socket.io/socket.io.js")    
        script(src="/public/js/app.js")

 

이제 frontend 를 수정한다.

위에서 생성한 div#room은 처음에는 보이지 않아야 한다. 처음에는 div#welcome 만 보이고, 

roomname 을 입력한 뒤에 방에 들어가면 메세지를 입력할 수 있어야 하기 때문이다. 

room Element를 가져오고, (line 5)

showRoom 메소드를 생성하여 front에서 showRoom이 실행되면 즉 roomName 이 입력되면

그 입력된 값을 back->front 로 넘겨줄 때 이 메소드를 실행하여 welcome 은 숨기고 room 은 보여지게 된다. (line 9~12)

 

 

//app.js

const socket = io();

const welcome = document.getElementById("welcome");
const form = welcome.querySelector("form");
const room = document.getElementById("room");

room.hidden = true;

function showRoom(){
    welcome.hidden = true;
    room.hidden = false;
}

function handleRoomSubmit(event){
    event.preventDefault();
    const input = form.querySelector("input");
    socket.emit("enter_room", input.value, showRoom);
    input.value = "";
}
form.addEventListener("submit", handleRoomSubmit);

 

이제 참가한 room에 누가 참가했는지를 알려주자.

home.pug 에 h3 를 추가하자. 

    div#room
                h3
                ul
                form
                    input(placeholder="message", required, type = "text")
                    button Send

그 후 app.js 에 

roomName 변수를 생성하고 (line 8)

handleRoomSubmit 에 roomName 을 넣어주고 (line21)

showRoom에서 h3 변수를 생성한 뒤 html 의 h3 아이템을 받아와서 내용을 roomName으로 바꿔준다. (line 14-15)

const socket = io();

const welcome = document.getElementById("welcome");
const form = welcome.querySelector("form");
const room = document.getElementById("room");

room.hidden = true;
let roomName;

function showRoom(){
    welcome.hidden = true;
    room.hidden = false;
    const h3 = room.querySelector("h3");
    h3.innerText = `Room ${roomName}`;
}

function handleRoomSubmit(event){
    event.preventDefault();
    const input = form.querySelector("input");
    socket.emit("enter_room", input.value, showRoom);
    roomName = input.value;
    input.value = "";
}
form.addEventListener("submit", handleRoomSubmit);

실행 후 방 이름을 입력하면

해당 방에 들어왔다는 의미로 h3 헤드라인이 출력된다. 

다음 편에서는 방에 들어왔을 때 방의 모든 사람들에게 인사를 하는 기능을 추가해보자.

728x90
728x90

#2.2 SocketIO is Amazing

이제 room 에 대해 배워보자. 

Userr가 웹사이트로 가면, 방을 만들거나 다른 방에 참여할 수 있다. 여기서 room 의 개념이 사용되며

socketIO에는 room 기능이 있어서 방에 참가하고 나가는 것이 매우 쉽다. 

또한 room 기능으로 방에 메세지를 보내는 것도 지원해준다. 

 

room 기능 사용을 위해 먼저 frontend 에 form 을 만들어주자.  (line 13~16)

doctype html
html(lang="en")
    head
        meta(charset="UTF-8")
        meta(http-equiv="X-UA-Compatible", content="IE=edge")
        meta(name="viewport", content="width=device-width, initial-scale=1.0")
        title Noom
        link(rel="stylesheet", href="https://unpkg.com/mvp.css")
    body 
        header
            h1 Noom 
        main 
            div#welcome
                form
                    input(placeholder="room name", required, type = "text")
                    button Enter Room 
        script(src="/socket.io/socket.io.js")    
        script(src="/public/js/app.js")

socketIO를 사용하면 

1. event 명을 임의로 지정하여 emit 할 수 있다. (여기선 enter_room) => emit의 event 명과 on의 event 명이 동일해야한다. 

2. Object 를 emit 할 수 있다.

3. 인자를 여러개 보낼 수 있다. (socket.emit("enter_room", {~~}, 3,5,6,,,,,10 );  와 같이 보낼 수 있다. 추가로 인자 수만큼 데이터를 받는 backend 쪽에도 on 메소드에 인자를 추가로 넣어줘야한다. )

 

frontend 

 : line 9와 같이 enter_room 이벤트에 payload(Object) 를 backend 로 전송한다. 

 

 

backend 

 : front에서 보낸 메세지를 받는다. (Line 23)

 

브라우저 창에 myroom 이라고 치면 이 메세지가 backend 로 전달되어 터미널에 출력되는 것을 확인할 수 있다. 

이는 Javascript Object 형식임에 주목하자. 

front에서 back 으로 Object 를 전송하면 SocketIO가 Object를 처리한다.

 

다음은 Callback 에 대해 알아보자. Callback 은 서버에서 호출하는 function 이다. 

frontend의 emit() 메소드의 세번째 인자로 function을 넣어보자. 

 

front -> back 으로 emit 할 때 함수 정보도 함께 보내고,

back 에서는 이렇게 받은 인자들을 처리한다.

backend 에서는 done() 메소드를 호출하고

메소드가 실행되는 것은 frontend 에서 되는 것임을 유의하자. 

(만약 front 에서 보낸 function을 back 에서 실행한다면 이건 보안 이슈임)

 

//app.js 

const socket = io();

const welcome = document.getElementById("welcome")
const form = welcome.querySelector("form");

function handleRoomSubmit(event){
    event.preventDefault();
    const input = form.querySelector("input");
    socket.emit("enter_room", {payload: input.value}, () => {
        console.log("server is done!");
    });;
    input.value = "";
}
form.addEventListener("submit", handleRoomSubmit);

//server.js

import http from "http";
import SocketIO from "socket.io"
import WebSocket from "ws";
import express from "express";

const app = express();

//set the view
app.set("view engine", "pug");
app.set("views", __dirname + "/views");
app.use("/public", express.static(__dirname + "/public")); //user가 public 으로 이동하면 __dirname+/public 폴더를 보여주는 것. 
app.get("/", (req, res) => res.render("home"));//render 
app.get("/*", (req, res) => res.redirect("/"));

const handleListen = () => console.log('Listening on http://localhost:3000');
//app.listen(3000, handleListen);

//http의 서버
const httpServer = http.createServer(app); //app은 requestListener 의 경로. express application으로부터 서버 생성. 
const wsServer  = SocketIO(httpServer);

wsServer.on("connection", (socket) => {
    socket.on("enter_room", (msg, done) => {
        console.log(msg);
        setTimeout(() => {
            done();
        }, 10000);
    });
});


//websocket 서버
//const wss = new WebSocket.Server({ httpServer });


// function handleConnection(socket) {
//     console.log(socket)
// }

// const sockets = [];

// wss.on("connection", (socket) => {
//     sockets.push(socket);
//     socket["nickname"] = "Anon";    //처음엔 익명으로 설정 
//     console.log("Connected to Browser ✅");
//     socket.on("close", () => {
//         console.log("Disconnected from the Browser ❌");
//     });
//     socket.on("message", (msg) => { 
//         const message = JSON.parse(msg);        //string -> Object
//         //console.log(message, message.toString('utf-8'));
//         switch(message.type){
//             case "new_message" :
//                 //sockets.forEach((aSocket) => aSocket.send(message.payload.toString('utf-8'))); //채팅 메세지만 출력하기 위해 이것만 브라우저에 보냄 
//                 sockets.forEach((aSocket) => aSocket.send(`${socket.nickname}: ${message.payload}`)); //누가 보냈는지, 뭘 보냈는지 
//             case "nickname" :
//                 //console.log(message.payload.toString('utf-8'));     
//                 socket["nickname"] = message.payload;
//         }
//     });
    
// });

httpServer.listen(3000, handleListen);

frontend 에서 함수가 실행되어서 console 창에 함수 결과가 프린트된 것을 확인할 수 있다. 

주의) emit에 여러개의 인자를 넣고 on 에서 그걸 받는다고 했을 때, function을 다시 front 로 넘겨주기 위해서는 마지막 인자에 넣어서 보내줘야 한다. 

 

 

 

728x90
728x90

#2.0 SocketIO vs WebSockets

실시간 기능을 지원하는 framework는 SocketIO이다. 이는 나온지 오래되었으며 매우 안정적이다.

 

SocketIO기능 : 실시간, 양방향, event 기반 comm. (=> Websocet 과 같은 기능.)

그러나 SocketIO는 websocket 을 실행하는 것이 아님. 

websocket은 SocketIO 가 제공하는 기능 중 하나임. 

그저 SocketIO는 가끔 websocket을 이용해서 websocket 의 기능을 제공하는 framework임

 

SocketIO는 HTTP long polling 을 사용하여 wifi 연결이 잠시 끊겨도 재연결을 시도한다.

 

그럼 SocketIO 기능을 사용해보고 이를 websocket 과 비교해보자

 

#2.1 Installing SocketIO 

먼저 SocketIO를 설치한다.

npm i socket.io

 

그리고 SocketIO 를 import 하고 wsServer 변수를 선언해준다.

import http from "http";
import SocketIO from "socket.io"
import WebSocket from "ws";
import express from "express";

const app = express();

//set the view
app.set("view engine", "pug");
app.set("views", __dirname + "/views");
app.use("/public", express.static(__dirname + "/public")); //user가 public 으로 이동하면 __dirname+/public 폴더를 보여주는 것. 
app.get("/", (req, res) => res.render("home"));//render 
app.get("/*", (req, res) => res.redirect("/"));

const handleListen = () => console.log('Listening on http://localhost:3000');
//app.listen(3000, handleListen);

//http의 서버
const httpServer = http.createServer(app); //app은 requestListener 의 경로. express application으로부터 서버 생성. 
const wsServer  = SocketIO(httpServer);



//websocket 서버
//const wss = new WebSocket.Server({ httpServer });


// function handleConnection(socket) {
//     console.log(socket)
// }

// const sockets = [];

// wss.on("connection", (socket) => {
//     sockets.push(socket);
//     socket["nickname"] = "Anon";    //처음엔 익명으로 설정 
//     console.log("Connected to Browser ✅");
//     socket.on("close", () => {
//         console.log("Disconnected from the Browser ❌");
//     });
//     socket.on("message", (msg) => { 
//         const message = JSON.parse(msg);        //string -> Object
//         //console.log(message, message.toString('utf-8'));
//         switch(message.type){
//             case "new_message" :
//                 //sockets.forEach((aSocket) => aSocket.send(message.payload.toString('utf-8'))); //채팅 메세지만 출력하기 위해 이것만 브라우저에 보냄 
//                 sockets.forEach((aSocket) => aSocket.send(`${socket.nickname}: ${message.payload}`)); //누가 보냈는지, 뭘 보냈는지 
//             case "nickname" :
//                 //console.log(message.payload.toString('utf-8'));     
//                 socket["nickname"] = message.payload;
//         }
//     });
    
// });

httpServer.listen(3000, handleListen);

여기까지 설치를 했으면 SocketIO는 url 을 준다.

=> http://localhost:3000/socket.io/socket.io.js

url 로 들어가면 아래와 같은 내용이 나온다.

이제 frontend 에도 SocketIO 를 설치해주자. (websocket에서는 따로 설치 없이 브라우저가 주는 websocket API를 쓰면 됐었음)

이제 기존 코드를 삭제하고 다시 시작해보자. 

그 전에 아카이빙을 하자.


home.pug

doctype html
html(lang="en")
    head
        meta(charset="UTF-8")
        meta(http-equiv="X-UA-Compatible", content="IE=edge")
        meta(name="viewport", content="width=device-width, initial-scale=1.0")
        title Noom
        link(rel="stylesheet", href="https://unpkg.com/mvp.css")
    body 
        header
            h1 Noom 
        main 
            form#nick
                input(type="text", placeholder="choose a nickname", required) 
                button Save
            ul
            form#message
                input(type="text", placeholder="write a msg", required) 
                button Send
        script(src="/public/js/app.js")

app.js (frontend)

const messageList = document.querySelector("ul");
const nickForm = document.querySelector("#nick");
const messageForm = document.querySelector("#message");
const socket = new WebSocket(`ws://${window.location.host}`);

function makeMessage(type, payload){
    const msg = {type, payload};
    return JSON.stringify(msg); //string으로 바꿔서 리턴
}

socket.addEventListener("open", () => {
    console.log("Connected to Server ✅");
});

socket.addEventListener("message", (message)=>{
    //console.log("New message", message.data);
    const li = document.createElement("li");
    li.innerText = message.data;
    messageList.append(li);
});

socket.addEventListener("close", ()=>{
    console.log("Disconnected from Server ❌");
});

function handleSubmit(event){
    event.preventDefault();
    const input = messageForm.querySelector("input");
    socket.send(makeMessage("new_message", input.value));   //front -> back 전송
    const li = document.createElement("li");
    li.innerText = `You: ${input.value}`;
    messageList.append(li);
    input.value = "";           //초기화
    //console.log(input.value);
}

function handleNickSubmit(event){
    event.preventDefault();
    const input = nickForm.querySelector("input");
    //socket.send(input.value);   //front -> back 전송
    socket.send(makeMessage("nickname", input.value));   //front -> back 전송
    input.value = "";           //초기화
}
messageForm.addEventListener("submit", handleSubmit);
nickForm.addEventListener("submit", handleNickSubmit);


// setTimeout( () => {
//     socket.send("Hello from the browser!");
// }, 10000);//10sec

server.js (backend)

import http from "http";
import SocketIO from "socket.io"
import WebSocket from "ws";
import express from "express";

const app = express();

//set the view
app.set("view engine", "pug");
app.set("views", __dirname + "/views");
app.use("/public", express.static(__dirname + "/public")); //user가 public 으로 이동하면 __dirname+/public 폴더를 보여주는 것. 
app.get("/", (req, res) => res.render("home"));//render 
app.get("/*", (req, res) => res.redirect("/"));

const handleListen = () => console.log('Listening on http://localhost:3000');
//app.listen(3000, handleListen);

//http의 서버
const httpServer = http.createServer(app); //app은 requestListener 의 경로. express application으로부터 서버 생성. 
const wsServer  = SocketIO(httpServer);



//websocket 서버
//const wss = new WebSocket.Server({ httpServer });


// function handleConnection(socket) {
//     console.log(socket)
// }

// const sockets = [];

// wss.on("connection", (socket) => {
//     sockets.push(socket);
//     socket["nickname"] = "Anon";    //처음엔 익명으로 설정 
//     console.log("Connected to Browser ✅");
//     socket.on("close", () => {
//         console.log("Disconnected from the Browser ❌");
//     });
//     socket.on("message", (msg) => { 
//         const message = JSON.parse(msg);        //string -> Object
//         //console.log(message, message.toString('utf-8'));
//         switch(message.type){
//             case "new_message" :
//                 //sockets.forEach((aSocket) => aSocket.send(message.payload.toString('utf-8'))); //채팅 메세지만 출력하기 위해 이것만 브라우저에 보냄 
//                 sockets.forEach((aSocket) => aSocket.send(`${socket.nickname}: ${message.payload}`)); //누가 보냈는지, 뭘 보냈는지 
//             case "nickname" :
//                 //console.log(message.payload.toString('utf-8'));     
//                 socket["nickname"] = message.payload;
//         }
//     });
    
// });

httpServer.listen(3000, handleListen);

 

 


 

html 파일을 수정한다.  (line 13)

doctype html
html(lang="en")
    head
        meta(charset="UTF-8")
        meta(http-equiv="X-UA-Compatible", content="IE=edge")
        meta(name="viewport", content="width=device-width, initial-scale=1.0")
        title Noom
        link(rel="stylesheet", href="https://unpkg.com/mvp.css")
    body 
        header
            h1 Noom 
        main 
        script(src="/socket.io/socket.io.js")    
        script(src="/public/js/app.js")

 

io : 자동으로 backend socket.io와 연결해주는 function 이다. 자동으로 socket.io를 실행하고 있는 서버를 찾을 것이다. 

설치가 되면 브라우저에서 io 함수를 실행할 수 있고 

실행한 내용은 backend 에 찍힌다. 

 

 

728x90
728x90

#1.9 Conclusions

현재는 내가 보내는 메세지를 server 내 다른 socket 들 뿐만 아니라 나 자신에게도 전송하고 있다.

이를 개선해보자.

line 30~33 과 같이 addEventListener 내 코드를 handleSubmit 으로 복사한뒤, 

li.innerText = `You: ${input.value}`;

로 변경해준다. 
const messageList = document.querySelector("ul");
const nickForm = document.querySelector("#nick");
const messageForm = document.querySelector("#message");
const socket = new WebSocket(`ws://${window.location.host}`);

function makeMessage(type, payload){
    const msg = {type, payload};
    return JSON.stringify(msg); //string으로 바꿔서 리턴
}

socket.addEventListener("open", () => {
    console.log("Connected to Server ✅");
});

socket.addEventListener("message", (message)=>{
    //console.log("New message", message.data);
    const li = document.createElement("li");
    li.innerText = message.data;
    messageList.append(li);
});

socket.addEventListener("close", ()=>{
    console.log("Disconnected from Server ❌");
});

function handleSubmit(event){
    event.preventDefault();
    const input = messageForm.querySelector("input");
    socket.send(makeMessage("new_message", input.value));   //front -> back 전송
    const li = document.createElement("li");
    li.innerText = `You: ${input.value}`;
    messageList.append(li);
    input.value = "";           //초기화
    //console.log(input.value);
}

function handleNickSubmit(event){
    event.preventDefault();
    const input = nickForm.querySelector("input");
    //socket.send(input.value);   //front -> back 전송
    socket.send(makeMessage("nickname", input.value));   //front -> back 전송
    input.value = "";           //초기화
}
messageForm.addEventListener("submit", handleSubmit);
nickForm.addEventListener("submit", handleNickSubmit);


// setTimeout( () => {
//     socket.send("Hello from the browser!");
// }, 10000);//10sec

왼쪽 메세지 창에 Hello를 입력해보자.

위의 You : Hello 는 자바스크립트로 front 쪽에 쓴 것이고

아래의 Anon : Hellosms 서버가 front 로 보내준 내용이다. 

 

현재는 아래와 같이 backend 에서만  string을 Objecgt로 Parsing 해서 front 로 보내고 있다. 

이 기능을 front 에도 추가해보자. 

socket.on("message", (msg) => { 
        const message = JSON.parse(msg);        //string -> Object
        //console.log(message, message.toString('utf-8'));
        switch(message.type){
            case "new_message" :
                //sockets.forEach((aSocket) => aSocket.send(message.payload.toString('utf-8'))); //채팅 메세지만 출력하기 위해 이것만 브라우저에 보냄 
                sockets.forEach((aSocket) => aSocket.send(`${socket.nickname}: ${message.payload}`)); //누가 보냈는지, 뭘 보냈는지 
            case "nickname" :
                //console.log(message.payload.toString('utf-8'));     
                socket["nickname"] = message.payload;
        }
    });

 

그리고 front 에만 있는 아래의 stringify (Object -> String) 기능을 backend 에도 추가해주자.

function makeMessage(type, payload){
    const msg = {type, payload};
    return JSON.stringify(msg); //string으로 바꿔서 리턴
}

이러한 기능들을 framework 를 통해 쉽게 구현할 수 있으며 다음 섹션에서 배워보자. 

 

728x90
728x90

#1.8 Nicknames part Two

왜 Object 형식을 String 으로 바꿔서 전송해야할까?

바로 websocket이 브라우저에 있는 API 이기 때문이다. backend 에서는 다양한 프로그래밍 언어를 사용할 수 있기 때문에 API가 어떤 결정을 할 수는 없는 것이다. 

예로 우리는 javascript 로 서버를 작성했지만, javascript가 아닌 GO를 이용해 서버에 접속한다면 문제가 되기 때문이다. 

front에서 javascript Object를 GO server 로 보내면 안된다. 그래서 string 으로 바꿔서 서버로 전송하는 것이다. 

string 을 받은 서버는 각자 환경에 맞게 string 을 다른 type 으로 변경하면 되는 것이다. 

 

 

이제 back -> front  을 볼 차례이다.

backend 가 받은 string 은 아래와 같고, 이제 이 string 을 Object 형식으로 바꿔서 front 로 보내보자. 

 

server

 

backend 쪽에서 line 36~39 와 같이 수정해준다.

그럼 메세지를 입력했을 때, 입력한 메새지만 나오게 된다. 닉네임에 입력해도 아무것도 스크린에 뜨지 않는 걸로 보아 message type 이 구분되는 것임을 알 수 있다. 

message type 이 nickname 인 경우도 추가해주고, switch 구문으로 변경해서 가독성을 높였다.

(parsed -> message 로 변수명 변경)

이제 누가 메세지를 보낸건지 식별하기 위해 type이 nickname 인 메세지의 payload 를 socket 에 넣어보자. 

line 45와 같이 socket 에 새로운 아이템을 추가하자. socket 은 기본적으로 Object 이기 때문에 원하는 어떤 타입이든 넣을 수 있다.

그 후 line 31 과 같이 처음 접속한 사람을 위해 익명을 뜻하는 "Anon" 으로 nickname 을 설정해주고

line 42와 같이 채팅 메세지를 누가 보냈는지, 뭘 보냈는지에 대한 아이템을 객체 형태로 담아 front 로 전송한다. 

 

이제 서버를 재시작해보자. 

잘 나온다. 여기서 주목할 점은 socket 이 데이터를 저장할 수 있다는 것이다. (socket["nickname"] = ....)

728x90

+ Recent posts