오늘은 WebRTC를 사용하기 위해 활용했던 PeerJS에 대해 포스팅을 해보고자 한다.

PeerJS란?

위의 사진은 PeerJS 공식 홈페이지에 나와있는 설명이다.

간단하게 요약해보면 ID만 가지고 있으면 WebRTC를 활용하여 손쉽게 P2P data나 media stream connection을 만들 수 있다고 말하는 것 같다. (영어에 약합니다…)

좀 더 간단하게 설명하면 WebRTC를 쉽게 사용할 수 있게 해주는 라이브러리이다!

짚고 갈 것들

  • WebRTC를 사용하기 위해서는 https 통신이 필요하다. 만약 서버에 배포를 하고자 한다면 https 환경을 구축해야 한다.
  • 나는 heroku를 통해 배포를 할 예정이다.

환경 구축하기

express generator를 통해 express 서버를 만들 것이다. 내가 본 영상에서 ejs를 활용하고 있기에 마찬가지로 ejs를 사용하기로 했다. 다음 명령어를 실행시키자. (express generator가 안 깔려 있다면 깔아주세요!)

express --ejs

다음과 같은 구조가 만들어졌다면 성공

그 후 필요한 모듈을 install 해주자.

npm install --save socket.io peer uuid

socket.io는 새로운 사용자와 기존 사용자 간에 동기화를 위해 사용할 것이고 peer는 PeerJS 서버를 사용하기 위해 설치했다. uuid는 이름 그대로 식별자를 생성하기 위하여 설치했다.

기본적인 준비는 모두 끝났다!

서버쪽 코드 작성하기

socket을 연결하고 라우팅을 해주자.

// bin/www

var app = require('../app');
var debug = require('debug')('peerjs-example:server');
var http = require('http');

var port = normalizePort(process.env.PORT || '3000');
app.set('port', port);

var server = http.createServer(app);
const io = require('socket.io')(server);

io.on('connection', (socket) => {
  socket.on('join-room', (roomId, userId) => {
    socket.join(roomId);
    socket.to(roomId).broadcast.emit('user-connected', userId);
    
    socket.on('disconnect', () => {
      socket.to(roomId).broadcast.emit('user-disconnected', userId);
    });
  });
});

...

const io = require('socket.io')(server); 이 부분을 통해 socket 서버를 열고 소켓이 연결되면 그에 맞는 이벤트들이 추가되게끔 작성하였다. 이벤트 명이 직관적이기 때문에 어떤 상황에서 발생하는 이벤트들인지 금방 알 수 있을 것이라 생각한다.
socket.join이 생소할 수 있는데 socket.io에서는 namespace보다 작은 단위인 room 기능을 제공한다. 말 그대로 같은 room에 있는 socket들 끼리만 통신할 수 있게 해주는 기능이다. uuid를 통해 room을 나누고 room 단위로 통신을 할 예정이다.

// routes/index.js

var express = require('express');
var router = express.Router();
const { v4: uuidV4 } = require('uuid');

router.get('/', (req, res) => {
  res.redirect(`/${uuidV4()}`);
});

router.get('/:room', (req, res) => {
  res.render('index', { roomId: req.params.room });
});

module.exports = router;

클라이언트쪽 코드 작성하기

서버쪽은 비교적 어렵지 않았을 것이라 생각한다. 클라이언트 쪽은 조금 어려울 수 있다. (내 기준엔 그랬다…) 일단은 코드를 수정하고 실행을 시켜보자.

// views/index.ejs

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <script>
      const ROOM_ID = '<%= roomId %>';
    </script>
    <script
      defer
      src="https://unpkg.com/peerjs@1.2.0/dist/peerjs.min.js"
    ></script>
    <script src="/socket.io/socket.io.js" defer></script>
    <script src="javascripts/script.js" defer></script>
    <title>Document</title>
    <style>
      #video-grid {
        display: grid;
        grid-template-columns: repeat(auto-fill, 300px);
        grid-auto-rows: 300px;
      }

      video {
        width: 100%;
        height: 100%;
        object-fit: cover;
      }
    </style>
  </head>
  <body>
    <div id="video-grid"></div>
  </body>
</html>
// public/javascripts/script.js

const socket = io('/', {transports: ['polling']});
const videoGrid = document.getElementById('video-grid');
const myPeer = new Peer();
const myVideo = document.createElement('video');
myVideo.muted = true;
const peers = {};

navigator.mediaDevices
  .getUserMedia({
    video: true,
    audio: true,
  })
  .then((stream) => {
    addVideoStream(myVideo, stream);

    myPeer.on('call', (call) => {
      call.answer(stream);
      const video = document.createElement('video');
      call.on('stream', (userVideoStream) => {
        addVideoStream(video, userVideoStream);
      });
    });

    socket.on('user-connected', (userId) => {
      connectToNewUser(userId, stream);
    });
});

socket.on('user-disconnected', (userId) => {
  if (peers[userId]) peers[userId].close();
});

myPeer.on('open', (id) => {
  console.log("voice chat on!");
  socket.emit('join-room', ROOM_ID, id);
});

function connectToNewUser(userId, stream) {
  const call = myPeer.call(userId, stream);
  const video = document.createElement('video');
  call.on('stream', (userVideoStream) => {
    addVideoStream(video, userVideoStream);
  });
  call.on('close', () => {
    video.remove();
  });

  peers[userId] = call;
}

function addVideoStream(video, stream) {
  video.srcObject = stream;
  video.addEventListener('loadedmetadata', () => {
    video.play();
  });
  videoGrid.append(video);
}

여기까지 작성하고 npm start를 해보면 다음과 같은 결과를 얻을 수 있을 것이다.

흐름을 그림으로 표현하면 다음과 같다.

유튜브 강의에서 설명을 다 해줬을 것 같은데 사실 나는 영어에 약해서 공식 문서를 직접 찾아가며 코드를 이해했다. (틀린 부분이 있을 수도 있다.) 흐름도를 봐도 이해가 잘 안 간다면 GitHub 코드를 참고해도 좋을 것 같다. 이 글에는 주석을 포함시키지 않았지만 GitHub에 주석을 포함한 코드를 올려놓았다.

참고 및 링크

참고

링크