요번 프로그램을 진행하면서 배우며 익힌 방법들에대해 개인적으로  정리하는 한주입니다

WEBRTC 와 SOCKET.IO 를 사용하여 화상채팅 구현해보기 ( node.js, javascript, pug )

  • 클라이언트 ( 프론트엔드 ) : app.js , home.pug
  • 서버 ( 백엔드 ) : server.js

완성된 화면

기본 css 를 사용하지않고 작게나마 꾸며진 css 를 적용한 

Map.css 

Header 에 link(rel="stylesheet",href="https://unpkg.com/mvp.css”) 추가 해주면 기본 css가 잡힘

home.pug ( HTML  화면을 보여주는 공간) 

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(placehorder="room name", required, type="text")
                    button Enter room
                    h4 Open Rooms :
                    ul

            div#call // 방이 만들어지면 사용자의 카메라 사용여부를 물어보고 화면이 나오고 채팅이 가능하게 만들어줌
                div#myStream
                    video#myFace(autoplay,playsinline,width="400",height="400")
                    button#mute Mute
                    button#camera Trun Camera OFF
                    select#cameras
                    video#peerFace(autoplay,playsinline, width="400",height="400")
                div#room
                    h3 
                    ul 
                    form#name
                        input(placeholder="nickname", required, type="text")
                        button Save
                    form#msg
                        input(placeholder="message", required, type="text")
                        button Send
        script(src="/socket.io/socket.io.js")
        script(src="/public/js/app.js")

 

서버

Javascript를 서버에서 사용하려면 구글의 V8 엔진을 기반으로 한 nodeJS 프레임워크를 사용해야 합니다

nodeJS를 사용한 REST 서버를 편리하게 구현하게 해주는 프레임워크로는 Koa, Hapi, express 등이 있고 이번 프로젝트에서는 express 사용하여 웹소켓에 대해 배워보았습니다

 

 

import 를 하여 package.json 의 모듈을 불러오는 

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

 

app이라는 변수에 express 함수의 변환 값을 저장하여  app이라는 변수로 REST End Point들을 생성하게 해주는

const app = express();

 

 REST API의 종류 (get, post, update, delete 등등)을 사용하여 End Point를 작성하는

 

app.set("view engine", "pug");
app.set("views", __dirname + "/public/views");
app.use("/public", express.static(__dirname + "/public"));
app.get("/", (reg, res) => res.render("home"));
app.get("/*", (req, res) => res.redirect("/"));

 

서버를 만들어 저장하고 그 서버위에 웹소켓 서버를 덮어 사용하는 

const httpServer = http.createServer(app);
const wsServer = SocketIO(httpServer);

 

 

공용된 방을 만들고 화상채팅과 실시간 채팅서버를 만들어주는 함수 

wsServer.on("connection", (socket) => {
  socket.on("join_room", (roomName) => {
    socket.join(roomName);
    socket.to(roomName).emit("welcome");
  });

  // join_room 이라는 이름으로 이름을 받아오고 해당 방으로 입장하며 클라이언트에 welcome 이라는 메세지를 보내준다
  
  socket.on("offer", (offer, roomName) => {
    socket.to(roomName).emit("offer", offer);
  });
  
  // offer는 roomName 에 해당하는 정보를 받아오는곳
  
  socket.on("answer", (answer, roomName) => {
    socket.to(roomName).emit("answer", answer);
  });
  
  // answer은 roomName 에 해당하는 정보의 결과를 보내주는것 
  
  socket.on("ice", (ice, roomName) => {
    socket.to(roomName).emit("ice", ice);
  });
  
  // ice는 이벤트는 로컬 ICE (en-US) 에이전트가 signaling 서버를 통해 원격 피어에게 메세지를 전달 할 필요가 있을때 마다 발생합니다
  
  socket["nickname"] = "Anon";
  
  // 입장시 기본값으로 Anon 이라는 이름으로 설정해줍니다
  
  socket.onAny((event) => {
    console.log(`Socket Event : ${event}`);
  });
  
  // 소켓 연결 되었을때 서버의 콘솔에 연결됬다는걸 알려줌
  
  socket.on("enter_room", (roomName, done) => {
    socket.join(roomName);
    done();
    socket.to(roomName).emit("welcome", socket.nickname, countRoom(roomName));
    wsServer.sockets.emit("room_change", publicRooms());
  });
  
  // enter_room 을 눌르면 해당하는 roomName의 방에 입장되고 
  // done() <- 성공 콜백함수
  // welcome 이라는 약속으로 클라이언트에게 사용자의 이름과 방의 정보를 넘겨준다
  
  socket.on("disconnecting", () => {
    socket.rooms.forEach((room) =>
      socket.to(room).emit("bye", socket.nickname, countRoom(room) - 1)
    );
  });
  
  //  연결을 끊는동안 클라이언트에게 bye 라는 이름으로 사용자의 이름과 룸의 갯수를 줄여준다
  
  socket.on("disconnect", () => {
    wsServer.sockets.emit("room_change", publicRooms());
  });
  
  // 연결이 끊기면 room_change 라는 타입으로 공용방을 보여준다
  
  socket.on("new_message", (msg, room, done) => {
    socket.to(room).emit("new_message", `${socket.nickname}: ${msg}`);
    done();
  });
  
  //  메세지가 오면 new_message 라는 타입으로 msg와 방의 정보와 성공여부를 받고
  //  방에다가 new_message 라는 타입으로 사용자의 이름 누구누구가 메세지를 보내왔다 넘겨준다
  
  socket.on("nickname", (nickname) => (socket["nickname"] = nickname));
  
});

 

 

 

 

Node.js를 이용하여 서버를 구축할 때는 http라는 모듈을 이용하여 listen으로 등록해주는 

 

// handleListen 함수를 만들어 콘솔에 서버 어디를 만들어 보여주고있다 라고 보여주는 함수
const handleListen = () => console.log(`Listening on http://localhost:3000`);
httpServer.listen(3000, handleListen);

 

 

클라이언트

 

서버와 소켓 연결을 하는

 

const socket = io();

html 의  문자열과 일치하는 정보들을 가져와서 변수에 담아 저장하는 곳

 

const myFace = document.getElementById("myFace");

// 카메라의 정보를 넣기위한 변수

const muteBtn = document.getElementById("mute");

// 마이크 음소거를 위한 변수 

const cameraBtn = document.getElementById("camera");

// 카메라를 키고 끄고 하기위한 변수

const cameraSelect = document.getElementById("cameras");

// 카메라를 어떤카메라를 선택할지를 쓰기위한 변수

const call = document.getElementById("call");

// 방생성을 하기위한 변수

const welcomes = document.getElementById("welcome");

// 채팅을 하기위한 변수

const form = welcomes.querySelector("form");

// 채팅을 하기위한 변수를 가져와 form 으로 변화하는 변수

WebRTC ( 동영상 및 오디오 )

방 생성 전 및 생성후 의 기본값들

call.hidden = true;
room.hidden = true;

// 방을 생성하기 전에는 hidden 을 사용해 가려준다

let myStream;
let muted = false;
let cameraOff = false;
let roomName;
let myPeerConnection;
let myDataChannel;

카메라의 정보를 가져와 사용하는 

async function getCameras() {
  try {
    const devices = await navigator.mediaDevices.enumerateDevices();
    const cameras = devices.filter((device) => device.kind === "videoinput");
    const currentCamera = myStream.getVideoTracks()[0];
    cameras.forEach((camera) => {
      const option = document.createElement("option");
      option.value = camera.deviceId;
      option.innerText = camera.label;
      if (currentCamera.label == camera.label) {
        option.selected = true;
      }
      cameraSelect.appendChild(option);
    });
  } catch (e) {
    console.log(e);
  }
}

getCameras 성공할경우

 navigator.mediaDevices.enumerateDevices() devices에 변수에 담는다 
 
  navigator.mediaDevices.enumerateDevices() = > 
  메서드는 사용(또는 접근)이 가능한 미디어 입력장치나 출력장치들의 리스트를 가져옵니다. 
  예를 들면 마이크, 카메라, 헤드셋 등의 미디어 입/출력 장치 리스트를 불러오는 것
  
  비디오 장치와 카메라 트렉등 카메라의 정보들을 불러와 선택하게해주는 함수

오디오의 정보를 가져와 사용하는 

async function getMedia(deviceId) {
  const initialConstrains = {
    audio: true,
    video: { facingMode: "user" },
  };
  const cameraConstraints = {
    audio: true,
    video: { deviceId: { exact: deviceId } },
  };
  try {
    myStream = await navigator.mediaDevices.getUserMedia(
      deviceId ? cameraConstraints : initialConstrains
    );
    myFace.srcObject = myStream;
    if (!deviceId) {
      await getCameras();
    }
  } catch (e) {
    console.log(e);
  }
}

getMedia 를 성공할경우

myStream 에
메서드는 사용자에게 미디어 입력 장치 사용 권한을 요청하며, 
사용자가 수락하면 요청한 미디어 종류의 트랙을 포함한 MediaStream을 반환합니다

실패할경우 콘솔에 에러가 나오게된다

마이크를 키고 끄고가 가능하게 해주는 함수

function hanleMuteClick() {
  myStream
    .getAudioTracks()
    .forEach((track) => (track.enabled = !track.enabled));
  if (!muted) {
    muteBtn.innerText = "Unmute";
    muted = true;
  } else {
    muteBtn.innerText = "Mute";
    muted = false;
  }
}

myStream 의 정보들을 가져와 키거나 끌수있게 만들어주는 함수

조건문을 걸어 muteBtn 이 true 면 화면에 Unmute 가 버튼으로 보이고 muted는 true로 상태로있으며
muteBtn이 true 면 화면에 보이는 버튼이 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;
  }
}

myStream 의 정보들을 가져와 키거나 끌수있게 만들어주는 함수

cameraOff가 false 면 화면에 버튼이 Turn Camera Off 가 보이고
cameraOff가 true 면 화면에 버튼이 Turn Camera On 이 보이게된다

카메라를 장치를 변경하게 해주는  함수

 

async function handleCameraChange() {
  await getMedia(cameraSelect.value);
  if (myPeerConnection) {
    const videoTrack = myStream.getVideoTracks()[0];
    const videoSender = myPeerConnection
      .getSenders()
      .finde((sender) => sender.track.kind === "video");
    videoSender.replaceTrack(videoTrack);
  }
}


videoSender 와 videoTrack 장치를 찾아 변경가능하게 해주는 함수입니다

실시간 채팅 ( 메세지 보내기, 메세지 받기 , 방참가 )

 

채팅 메세지 받기

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

메세지를 받아 html 안의 ul 에 추가해주는 함수입니다

채팅 메세지 보내기

function handleMessageSubmit(event) {
  event.preventDefault();
  const input = room.querySelector("#msg input");
  const value = input.value;
  socket.emit("new_message", input.value, roomName, () => {
    addMessage(`You : ${value}`);
  });
  input.value = "";
}

Input 박스안에 사용자가 메세지를 입력하면 서버로 보내지게되고 작성을 누를시에 빈공간으로 남겨주는 함수

채팅 메세지 보내는 이름 보내주기

function handleNicknameSubmit(event) {
  event.preventDefault();
  const input = room.querySelector("#name input");
  const value = input.value;
  socket.emit("nickname", input.value);
  input.value = "";
}

사용자가 이름을 만들어서 그 이름으로 사용자가 입력하는 입력값을 같이 서버로 보내주는 함수

채팅 방 만드는 함수

function showRoom() {
  welcome.hidden = true;
  room.hidden = false;
  const h3 = room.querySelector("h3");
  h3.innerText = `Room ${roomName}`;
  const msgForm = room.querySelector("#msg");
  const nameForm = room.querySelector("#name");
  msgForm.addEventListener("submit", handleMessageSubmit);
  nameForm.addEventListener("submit", handleNicknameSubmit);
}

채팅방을 만들어주면 보여주던 welcome 을 숨김처리 해준다
방의 숨김처리 되어있던 룸을 숨김처리를 풀어준다
제목을 활용하여 방의 이름을 설정해주고 메세지와 이름을 서버로 보내준다

 

채팅 방만드는 함수

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);

muteBtn.addEventListener("click", hanleMuteClick);
cameraBtn.addEventListener("click", handleCameraClick);
cameraSelect.addEventListener("input", handleCameraChange);

'프로그램 시작후 각 주차 정리' 카테고리의 다른 글

공부 13주차 - part 1 ( ApexCharts , Recoil )  (0) 2022.06.04
공부 12주차 - part 2 ( React-Query )  (0) 2022.05.29
공부 10주차  (0) 2022.05.15
공부 9주차  (0) 2022.05.07
프로그램 8주차  (0) 2022.05.01

+ Recent posts