JavaScript 만으로 채팅방 생성, 화상채팅, 개인 메시지를 구현합니다. Zoom Clone Challenge using NodeJS, WebRTC and Websockets.
- 23-07-25 : #0.0 ~ #1.9 / Initial Settings & Chat with WebSocket (+ Code Challenge(2 days)[2nd day])
- 초기 셋팅
- 패키지: { nodemon, babel, express, pug }
- .gitignore, nodemon.json, babel.config.json 생성 및 작성
- Back-End
- package.json에서 script 생성
- 서버 파일(server.js) 및 Express 서버 생성
- Front-End
- JavaScript 파일 및 Express에서 static 설정
- Express에서 Pug template 설정
- nodemon.json에서 Front-End 파일 수정 시 자동 재시작 기능 제거
- 간단한 CSS를 위해 'MVP.CSS' 적용
- 패키지: { nodemon, babel, express, pug }
- WebSocket
- 서버와 사용자 사이에 양방향성 실시간 연결 protocol
- 패키지: ws
- Node.js용 WebSocket을 실행(implementation)하는 패키지
- WebSocket의 기본적인 기능만 존재하므로, 부가적인 기능을 사용하려면 다른 framework 사용
- 설정법: 따로 WS서버를 구축하지 않고, HTTP를 사용하는 Express를 그대로 사용하면서 구축
- Node.js에 내장되어있는 HTTP package를 사용해 HTTP서버 생성
- 생성한 HTTP서버에 접근 후, HTTP서버 위에 WS서버 생성
- HTTP서버를 실행해, 두 서버 다 사용 가능 (같은 port를 공유하기 때문)
- 각각 따로 서버를 만들거나, WS서버만 만들어도 무방함
- WebSocket 연결(connection)
- WS서버를 사용하므로, 다른 설치가 필요없이 브라우저에서 지원됨
- 브라우저에서는 내장된 WebSocket API 존재
- (필수) Front-End에서 Back-End로 WS 연결을 만들어야 함
- 기본형: new WebSocket(host주소 [, 프로토콜]);
- ex: const frontSocket = new WebSocket(`ws://${window.location.host}`);
- 기본형: new WebSocket(host주소 [, 프로토콜]);
- (선택) Back-End에서 WS서버에 누가 연결했는지 알 수 있음
- 기본형: WS서버.on(이벤트명, 콜백함수)
- 콜백함수의 매개변수로 'socket'을 지원함
- 기본형: WS서버.on(이벤트명, 콜백함수)
- WS서버를 사용하므로, 다른 설치가 필요없이 브라우저에서 지원됨
- WebSocket Event
- Front-End: '.addEventListener()' 사용
- Back-End: '.on(이벤트명, 콜백함수)' 사용
- WebSocket Message 보내기/받기
- Front-End: 소켓.send(데이터) / 소켓.addEventListener("message", (message) => {... message.data ...});
- Back-End: 소켓.send(데이터) / 소켓.on("message", (message) => {... message ...});
- 실시간 채팅방
- 메시지: 기능은 브라우저마다 독립적으로 작동
- template에서 form 생성
- Front에서 Back으로 메시지를 전송
- Back에서 Front로 받은 메시지를 전송
- Front에서 메시지를 받은 후, element를 생성해 document 화면에 보여줌
- 온라인
- 서버에서 누가 연결되어있는지 알기위해 'fake DB'(Array) 사용
- 브라우저가 WS서버와 연결 시 socket을 DB list에 넣음
- Back-End가 메시지를 받을 시 연결되어있는 모든 브라우저에게 메시지를 전송
- '.forEach()'를 사용해 DB list에 있는 모든 socket에게 발송
- 여러 개의 메시지를 한 번에 전송
- WS는 한 번에 하나의 메시지(String 타입)만 발신/수신 가능
- JavaScript 객체 사용 불가능
- JSON을 이용해 문자열로 변환
- 메시지 발신
- JSON.stringify(): JS 객체 -> JSON 문자열
- JSON 문자열로 변환 후, 메시지 전송
- 메시지 수신
- JSON.parse(): JSON 문자열 -> JS 객체
- JS 객체로 변환 후, 조건문을 사용해 메시지의 용도를 구분
- 닉네임 설정: socket은 객체이기 때문에 프로퍼티를 추가해 사용
- WS는 한 번에 하나의 메시지(String 타입)만 발신/수신 가능
- 메시지: 기능은 브라우저마다 독립적으로 작동
- 초기 셋팅
- 23-07-26 : #2.0 ~ #2.3 / Socket.io(1) (+ Code Challenge(2 days)[1st day])
- Socket.io 패키지
- 실시간, 양방향 event 기반의 통신을 가능하게 하는 framework
- 연결이 끊어지면 자동으로 재연결을 시도
- WebSocket에 문제가 생겨도 다른 방법을 이용해 계속 작동
- 실시간 기능 등을 더 쉽게 만드는 편리한 코드를 제공
- 서버 설정법
- [Back-End] HTTP 서버 생성 후 io서버를 위에 쌓아올리는 방식
- [Front-End] client에도 Socket.io를 설치해야 함
- Socket.io는 WebSocket의 부가기능이 아니며, 브라우저의 WS와 호환되지 않기 때문
- Socket.io서버가 생성되면, '/socket.io/socket.io.js'라는 URL주소를 줌
- 해당 JavaScript 파일을 view의 최상단 script로 사용
- Back-End와 Front-End 연결(Connection)
- [Back-End] io서버의 'connection'이벤트를 사용해 연결
- [Front-End] 자동으로 io서버와 연결해주는 'io'함수를 사용해 socket 생성
- WebSocket처럼 host주소를 입력할 필요가 없음
- 연결 완료 시 지금 연결된 모든 socket을 자동적으로 추적함
- 메시지 이벤트(Message event)
- 송신: '소켓.emit(이벤트명, 보낼데이터 [, 콜백함수])'를 사용해 event를 생성해 전송
- 자기 자신을 제외한 다른 사용자들에게 메시지를 보내는 기능
- 이벤트명 : 원하는 이름 커스텀 사용 가능
- 보낼데이터 : 모든 데이터타입 가능 (JSON 변환 필요없음), 원하는 갯수만큼 가능
- 콜백함수 : 서버에서 제어할 수 있는 함수, 실행은 Front-End에서 됨
- 무조건 마지막 인수에서 사용해야 함
- 수신: '소켓.on(이벤트명, 콜백함수)'를 사용
- 이벤트명 : 메시지 수신 시 송신 이벤트명과 같은 이벤트명을 사용해야 함
- 콜백함수 : 메시지 수신 시 'message' 매개변수 사용
- 송신: '소켓.emit(이벤트명, 보낼데이터 [, 콜백함수])'를 사용해 event를 생성해 전송
- 실시간, 양방향 event 기반의 통신을 가능하게 하는 framework
- Socket.io 패키지
- 23-07-27 : #2.4 ~ #2.11 / Socket.io(2) (+ Code Challenge(2 days)[2nd day])
- room : 서로 소통할 수 있는 socket 그룹 (ex. 채팅방)
- 소켓.join(방이름) : 해당 room에 socket을 추가 (방 입장)
- 배열을 사용해 여러 room에 동시에 참가 가능
- 소켓.to(방이름) : 해당 room에 있는 전체에게 메시지를 보낼 수 있음
- 방이름 대신 socket.id를 사용하면, 귓속말 기능으로 사용 가능
- socket마다 자기자신의 private room에 존재하고있기 때문
- 체이닝으로 여러 방 또는 '.emit()' 메서드를 사용
- 방이름 대신 socket.id를 사용하면, 귓속말 기능으로 사용 가능
- 소켓.leave(방이름) : 해당 room 떠나기
- 소켓.id : 해당 socket의 id
- 소켓.rooms : 해당 socket이 참여중인 room의 리스트(Set타입)
- 사용자의 id는 사용자가 있는 방의 id와 같음
- 기본적으로 사용자와 서버 사이에 private 방이 있기 때문
- 소켓.onAny(콜백함수) : 모든 event에 사용하는 middleware 생성
- 콜백함수의 매개변수로 event와 다른 인수들 사용 가능
- 서버.sockets.emit() : 모두에게 메시지 전송
- 서버.socketJoin(방이름) : 서버의 모든인원을 해당 방에 입장시킴
- 서버.in(방이름1).socketJoin(방이름2) : '방1'의 모든인원을 '방2'에 입장시킴
- socket의 'disconnecting' 이벤트를 사용해 방에 나갈 시 이벤트를 사용 가능
- disconnecting : 서버와 연결 끊기 직전의 event
- disconnect : 서버와 연결 끊은 직후의 event
- 소켓.join(방이름) : 해당 room에 socket을 추가 (방 입장)
- 닉네임 설정 : socket은 객체이므로 새로운 프로퍼티를 생성 후 저장하여 사용
- adapter
- 다른 서버들 사이에 실시간 application을 동기화하는 것
- 현재 서버 memory에서 adapter를 사용 중이라, DB처럼 기억하지 않음
- 실제 app을 만들 시 DB를 사용해야 함 (DB를 사용해 adapter로 서버 간 통신)
- 모든 client가 동일한 서버에 연결되지는 않기 때문
- adapter는 application으로 통하는 창문
- 누가 연결되었는지, room이 얼마나 있는지 등을 알려줌
- '서버.sockets.adapter'를 통해 room들의 정보와 사용자들의 정보 등을 알 수 있음
- ex. wsServer.sockets.adapter
- .rooms : 방 목록
- .sids : 사용자(socket.id) 목록
- 방이름과 사용자명이 같으면 private room, 다르면 public room
- 모든 socket은 자신의 id와 동명의 방을 가지고 있기 때문
- 목록은 'Map' 데이터타입으로 되어있음
- 키-값 쌍의 집합으로 이루어진 데이터 타입
- 키의 중복 불가능
- ex. wsServer.sockets.adapter
- pubic room 추출 : '.forEach()' 반복문 사용
- [Map 데이터타입] 첫번째 매개변수는 value, 두번째 매개변수는 key
- '.rooms'에서 '.sids'를 뺸 나머지가 public rooms
- 채팅방의 인원 수
- '서버.sockets.adapter.rooms'를 통해 각 방 마다 누가있는지 확인 가능
- 각 value는 Set 데이터타입으로 이루어져있음
- Set은 '.size' 프로퍼티를 이용해 갯수를 알아낼 수 있음
- 다른 서버들 사이에 실시간 application을 동기화하는 것
- Admin UI
- Socket.io의 Back-End를 위한 UI : 모든 socket, room, client 등 확인 가능
- 설치법 : npm i '@socket.io/admin-ui'
- 설정법
- 서버파일에 'import { instrument } from "@socket.io/admin-ui";' 추가하기
- io서버 생성무에서 옵션 추가하기
- 'admin.socket.io'에 접속하기: Server URL은 host주소
- room : 서로 소통할 수 있는 socket 그룹 (ex. 채팅방)
- 23-07-29 : #3.0 ~ #3.6 / Video call(1) (+ Code Challenge(3 days)[2nd day])
- video 권한
- template에서 video element 생성하기
- video의 'playsinline' 속성 : 모바일에서 인라인으로 재생하게하는 프로퍼티
- JavaScript에서 권한 및 stream 생성하기
- stream : video와 audio가 결합된 것
- 'navigator.mediaDevices.getUserMedia(옵션)' : 비동기로 stream 추출
- stream은 동적 source이므로, video.srcObject 프로퍼티에 값을 할당
- template에서 video element 생성하기
- 카메라 제어 버튼
- stream은 'track'을 제공함
- track : video, audio, 자막 등
- '스트림.getAudioTracks()' 또는 '스트림.getVideoTracks()'를 사용
- '.enabled' : 활성화/비활성화
- '.muted' : (읽기 전용) 음소거 여부
- ex. myStream.getAudioTracks().forEach((track) => (track.enabled = !track.enabled));
- stream은 'track'을 제공함
- 디바이스 목록 만들기
- 'navigator.mediaDevices.enumerateDevices()' : 모든 장치 정보(배열)를 가져옴
- promise 형태이기 때문에 비동기로 사용
- 각 값의 'kind'속성에서 video인지 audio인지 구분 가능
- 가져온 장치 정보 배열에서 원하는 장치 목록을 구성
- select element를 사용해서 리스트를 열거
- option element의 value는 '.deviceId'로 설정 (Unique ID)
- 사용할 장치를 선택한 후, stream을 다시 시작해야 함
- stream의 옵션으로 'deviceId'를 사용해 생성
- 'navigator.mediaDevices.enumerateDevices()' : 모든 장치 정보(배열)를 가져옴
- WebRTC (Web Real-Time Communication)
- 실시간 소통을 가능하게 해주는 기술
- 'peer-to-peer(P2P)' 지원 : 서버를 거치지 않고, 직접적으로 통신
- 서버가 아예 필요없지는 않고, 'signaling'을 하기 위해 서버가 필요함
- 사용자가 어디에 있는지(ip, port, 방화벽 등) 알아내어 서로 연결하기 위함
- signaling이 끝나면, P2P 연결이 됨
- video, audio, text 등을 통신할 수 있음
- RTC 연결
- Socket.io로 room을 만들면, 서로 서버를 통한 연결이 됨
- 서버와 연결된 양쪽 사용자를 서로 연결시키는 연결통로를 만들어야 함
- 각 사용자 마다 설정이 이루어지고, 서버(Socket.io)를 사용해 연결
- [Front-End] 각 사용자에게 peer연결 설정하기
- 'new RTCPeerConnection()' 객체를 생성해 peer연결 설정
- [Front-End] peer연결 안에 연결할 데이터 집어넣기
- video와 audio를 연결을 통해 전달하기 위해, P2P 연결 안에 데이터를 집어넣어야 함
- stream에서 각각의 track(video, audio)을 추출한 후, peer연결에 넣음
- '스트림.getTracks()' : stream의 현재 track들을 배열로 변환
- 'peer연결.addTrack(트랙, 스트림)' : peer연결에 해당 스트림의 트랙 데이터를 넣음
- [Front-End] 연결 'offer' 생성/설정/전송하기
- 먼저 들어온 사용자가 'offer'를 생성하기
- 'peer연결.createOffer()'
- 뒤에 들어온 사용자로부터 socket메시지를 받는 이벤트를 사용해 구현
- 'offer' 설정하기
- 방금 만든 offer로 연결을 구성해야 함
- 'peer연결.setLocalDescription(OFFER)' 메서드를 사용해 offer를 구성
- 연결 인터페이스와 관련이 있는 로컬 설명을 변경함
- (ex.) myPeerConnection.setLocalDescription(offer);
- 'offer' 전송하기
- Socket.io를 사용해 전송
- 제공하는 자 ➡ 서버 ➡ 제공받는 자
- 먼저 들어온 사용자가 'offer'를 생성하기
- [Front-End] 'offer' 수령 및 'answer' 생성/설정/전송하기
- 받은 'offer'를 설정하기
- 'peer연결.setRemoteDescription(데이터)' : 멀리 떨어진 peer의 description을 세팅
- 'answer' 생성 : 'peer연결.createAnswer()'
- 'answer' 설정 : 'peer연결.setLocalDescription(ANSWER)'
- 'answer' 전송
- 받은 'offer'를 설정하기
- [Front-End] 'answer' 수령하기
- 'peer연결.setRemoteDescription(데이터)'
- Socket.io로 room을 만들면, 서로 서버를 통한 연결이 됨
- video 권한
- 23-07-30 : #3.7 ~ #3.12 / Video call(2) (+ Code Challenge(3 days)[3rd day])
To-Do list
- 마이크 목록 만들기
- 음소거, 화면on/off 버튼 수정
- 음소거 후 카메라 변경 시 작동이 반대로 됨