이 책을 기반으로 작성합니다.
예전에 히히 재밌겠다하고 사뒀던건데 제로베이스 부트캠프에 참여하면서
쫓기다보니 먼지가 쌓이던 줌 책 바닐라 크롬 만들기를 끝내고 펴봤습니다.
재밌더라구요..
저만 재밌으면 미안하니까 님들도 같이 재밌으시라고 씁니다.
https://github.com/XionWCFM/zoom-clone-coding
제 레포입니다.
그냥 따라치기만 하면 금방 하겠지만 모르는걸 하나하나 서칭해보면서 이런거구나..하고 넘어가다보니
완성까지 얼마나 걸릴진 몰?루
처음보는것들 투성이다보니 난감한 느낌을 공유하면서
제가 처음봤거나 사용해보는 것을 간단히 서술하겠습니다
pug
이건 진짜 살면서 아예 처음 본건데 서칭해보니까
예전엔 백엔드개발자가 프론트영역까지 다 만들때의 잔재...라는 이야기가 있더라구요
html을 간결하게 쓸수있는 탬플릿언어라고 합니다.
views 폴더 안에 있을때 제대로 돌아간다는 말도 구글링을 통해 봤네요..
express.js
이건 처음본건 아니긴한데
Node.js 환경에서 돌아가는 웹프레임워크라고하는데 이름만 들어본걸 여기서 사용하네요
nodemon
nodemon은 소스 코드를 수정할 때마다 코드의 변화를 감지해서
자동으로 서버프로그램을 재시작해주는 도구라고합니다.
babel
babel은 컴파일러의 일종으로 작성된 자바스크립트 코드를
다른 버전의 자바스크립트 코드로 변환해주는 역할을 수행해요
MVP.css
자동으로 css 스타일을 적용해주는 라이브러리의 일종입니다.
link만 달아주면 되는데
아무 css도 적용하지 않은 것보단 보기 좋게 나와서 좋은것같네요
link(rel="stylesheet", href="https://unpkg.com/mvp.css")
pug환경에서는 이 한줄만으로 css가 적용돼요! wow...
websocket
실시간 기능을 구현할 때 직면하는 큰 문제가 있습니다.
HTTP는 stateless라는 특징을 가지고 있습니다.
사용자와 서버가 요청과 응답을 주고받은 이후에 서버는 사용자를 기억하지 않는다는 거에요
사용자가 요청을 할때만 응답을 주는 방식으로는 실시간 채팅을 구현할 수 없겠죠..?
왜냐면 우리는 채팅을 할 때 우리가 말을 하는것도 중요하지만
상대방의 채팅을 바로바로 받아서 읽는것도 중요하잖아요
websocket은 한번 사용자와 서버가 연결한 상태가 되어서 웹소켓 연결이 성립되면
서버는 사용자가 누구인지도 알 수 있고 원한다면 서버가 사용자에게 메시지를 보낼수도 있습니다.
이런 양방향 연결을 websocket으로 구현할 수 있는거네용
웹소켓만들기
const wss = new Websocket("~~~~~")
웹소켓은 new 연산자를 이용해 생성합니다.
괄호안에는 ws or wss로 시작하는 url이 들어가야합니다.
wss가 보안적으로 ws보다 덜 취약하다고하여 wss를 사용하는것이 좋다고합니다.
🐸항상 wss://를 사용합시다.
wss://는 보안 이외에도 신뢰성(reliability) 측면에서 ws보다 좀 더 신뢰할만한 프로토콜입니다.ws://를 사용해 데이터를 전송하면 데이터가 암호화되어있지 않은 채로 전송되기 때문에 데이터가 그대로 노출됩니다. 그런데 아주 오래된 프락시 서버는 웹소켓이 무엇인지 몰라서 ‘이상한’ 헤더가 붙은 요청이 들어왔다고 판단하고 연결을 끊어버립니다.반면 wss://는 TSL(전송 계층 보안(Transport Layer Security))이라는 보안 계층을 통과해 전달되므로 송신자 측에서 데이터가 암호화되고, 복호화는 수신자 측에서 이뤄지게 됩니다. 따라서 데이터가 담긴 패킷이 암호화된 상태로 프락시 서버를 통과하므로 프락시 서버는 패킷 내부를 볼 수 없게 됩니다.
https://ko.javascript.info/websocket
웹소켓 이벤트의 종류
이벤트명 | 설명 |
close | 서버가 닫혔을 때 발생합니다. |
connection | 서버와 사용자 간 연결이 성립되었을 때 발생 |
error | 연결되어있는서버에 오류가 생겼을때 발생 |
headers | 서버의 응답 헤더가 소켓에 기록되기 전에 발생 |
listening | 연결되어있는 서버가 바인딩 되었을 때 발생 |
웹소켓 .send
사용자 -> 서버의 형태로 데이터를 보낼때 사용한 메서드입니다.
검색해보면서 알았는데 send 메서드는 텍스트(문자열 string), 이진데이터만 보낼 수 있다네요
반면 데이터를 받는 서버의 입장에서는 텍스트데이터는 문자열로만 오고
이진데이터는 blob과 arraybuffer 두 형식 중 하나를 골라 받을 수 있는데 blob이 디폴트입니다.
근데..blob은 뭐고 arraybuffer는 뭔데요
나중에 필요하게되면 더 찾아보는게 좋을 것 같네요..
지금 제 수준에선 그뭔씹밖에 나오지 않음
오늘의 코드리뷰
app.js에서는 프론트영역을
server.js에서는 서버영역을 담당하는 파일입니다.
먼저 app.js파일부터..
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)
}
socket.addEventListener("open" , () => {
console.log("Connected to Server");
})
socket.addEventListener("message", (message) => {
const li = document.createElement("li")
li.innerText = message.data
console.log(message)
messageList.append(li)
})
socket.addEventListener("close" , () => {
console.log("Disconnected from Server")
})
messageForm.addEventListener("submit" , handleSubmit)
nickForm.addEventListener("submit" , handleNickSubmit)
function handleSubmit(event) {
event.preventDefault()
const input = messageForm.querySelector("input")
socket.send(makeMessage("new_message" , input.value));
input.value = "";
}
function handleNickSubmit(event) {
event.preventDefault();
const input = nickForm.querySelector("input")
socket.send(makeMessage("nickname" , input.value))
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}`);
messageList = 채팅이 올라올 채팅창을 만들어줌
nickForm = 닉네임을 정할 수 있는 form 설정
messageForm = 채팅내용을 쓸 수 있는 form 설정
socket = 연결할 WebSocket 객체를 생성해주고 주소는 window.location.host로 지정
function makeMessage(type, payload) {
const msg = { type, payload};
return JSON.stringify(msg)
}
makeMessage 함수는 type, payload를 매개인자로 받아서
JSON 데이터로 만들어준다음
JSON을 문자열형태로 변환해서 서버로 보내줍니다.
자바스크립트로 만든 서버만 사용한다면 JSON을 그대로 보내도 상관없지만
어떤언어로든 웹소켓프로토콜에 기반한 서버를 만들 수 있으니
문자열로 변환해줍니다
socket.addEventListener("open" , () => {
console.log("Connected to Server");
})
웹소켓 서버와 연결 이벤트가 발생했을때
Connected to Server를 콘솔에 출력해주는 이벤트리스너
socket.addEventListener("message", (message) => {
const li = document.createElement("li")
li.innerText = message.data
console.log(message)
messageList.append(li)
})
소켓에 message 이벤트가 발생했을때
li태그를 생성하고 li의 내용으로 message.data를 넣어줍니다.
messageList의 마지막 자식요소로 li를 넣어줍니다.
!! 중요 message 이벤트는 send 메서드를 통해 일어납니다.
이 경우에는 server.js의 socket.send() 코드를 app.js가 eventListener를 통해 받는 것
socket.addEventListener("close" , () => {
console.log("Disconnected from Server")
})
messageForm.addEventListener("submit" , handleSubmit)
nickForm.addEventListener("submit" , handleNickSubmit)
웹소켓 연결이 끝나면 콘솔에 내용을 출력합니다.
messageForm의 submit 이벤트 발생시 handleSubmit을 호출합니다.
nickForm의 submit 이벤트 발생시 handleNickSubmit을 호출합니다.
function handleSubmit(event) {
event.preventDefault()
const input = messageForm.querySelector("input")
socket.send(makeMessage("new_message" , input.value));
input.value = "";
}
submit의 기본동작을 실행하지 않도록 preventDefault()해줍니다.
input에 MessageForm 안의 input 태그를 할당해주고
send 메서드를 이용해 서버에게 makeMessage()를 통해
input.value를 JSON.stringfy하여 전송해주고 type은 new_message라는 정보도 함께 담아서 전달합니다.
function handleNickSubmit(event) {
event.preventDefault();
const input = nickForm.querySelector("input")
socket.send(makeMessage("nickname" , input.value))
input.value = ""
}
handleSubmit과 비슷하게 동작하지만
type을 nickname으로 지정해주어 둘을 구분할 수 있게 했습니다.
입력내용을 읽은 다음 소켓을 통해 서버로 보내는 기능은 같지만
입력값의 목적이 다르기에 구분해줍니다
근데.. 이렇게 비슷한 일을 하는 함수면 어떻게 일원화할 수도 있을것같은데.. 일단 넘어가겠습니다
Server.js
import http from "http";
import WebSocket from 'ws';
import express from 'express';
const app = express();
app.set("view engine", "pug");
app.set("views", __dirname + "/views");
app.use("/public" , express.static(__dirname + "/public"));
app.get("/", (req,res) => res.render("home"));
app.get("/*", (req,res) => res.redirect("/"));
const handleListen = () => console.log("Listening on http://localhost:3000");
const server = http.createServer(app);
const wss = new WebSocket.Server({ server });
const sockets = []
wss.on("connection", (socket) => {
sockets.push(socket)
socket["nickname"] = "Anonymous"
socket.on('close' , () => console.log("Disconnected from Browser"))
socket.on("message" , (msg) => {
const message = JSON.parse(msg)
switch(message.type) {
case "new_message" :
sockets.forEach(aSocket => aSocket.send(`${socket.nickname} : ${message.payload}`))
break
case "nickname" :
socket["nickname"] = message.payload
break
}
})
})
server.listen(3000, handleListen);
전체 코드는 이렇습니다.
import http from "http";
import WebSocket from 'ws';
import express from 'express';
const app = express();
import - from 문법을 통해 express,http,websocket을 가져옴
app.set("view engine", "pug");
app.set("views", __dirname + "/views");
app.use("/public" , express.static(__dirname + "/public"));
app.get("/", (req,res) => res.render("home"));
app.get("/*", (req,res) => res.redirect("/"));
app.set() 메서드를 이용하면 설정하고자 하는 항목을 지정해
원하는 설정값을 입력할 수 있습니다.
express의 설정값을 지정해주는 건가..라고 이해했습니다.
dirname은 저번 바닐라크롬만들때 봤던 것이고
app.get() 메서드는 서버에 http 요청이 왔을때 지정된 콜백을 이용하여
라우팅 처리를 해주는 메서드입니다.
**여기에서 라우팅이란 ? -> 주소를 보고 어떤 페이지(뷰)를 제공할지 결정하는 작업
구글링을 통한 추가 정보
app.use와 app.get은 동작자체는 비슷한데 무슨 차이가 있는가?
app.use → 일반적으로 응용 프로그램에 미들웨어를 도입하는 데 사용되며 모든 유형의 HTTP 요청을 처리 할 수 있습니다.
app.get → GET HTTP 요청 만 처리합니다.
req,res가 궁금한데... 서칭이 살짝 어렵네요
res.render("home") res.render메서드로 home 탬플릿을 제공하는 것이며
app.get("/*", (req,res) => res.redirect("/"));는 사용자가 홈이 아닌 다른 주소로
get 요청을 보내더라도 홈으로 리다이렉션(우회)하는 코드
const handleListen = () => console.log("Listening on http://localhost:3000");
const server = http.createServer(app);
const wss = new WebSocket.Server({ server });
const sockets = []
haddleListen 변수는 맨밑줄의 Server.listen()에 사용됩니다.
server 는 express()를 이용해 서버 객체를 만드는 것같네요.. 너무 생소함
wss 는 웹소켓서버를 만들어주는데 http서버객체로 만든 server를 안에 넣어주네요
sockets는 데이터베이스역할을 위해 생성함
wss.on("connection", (socket) => {
sockets.push(socket)
socket["nickname"] = "Anonymous"
socket.on('close' , () => console.log("Disconnected from Browser"))
socket.on("message" , (msg) => {
const message = JSON.parse(msg)
switch(message.type) {
case "new_message" :
sockets.forEach(aSocket => aSocket.send(`${socket.nickname} : ${message.payload}`))
break
case "nickname" :
socket["nickname"] = message.payload
break
}
})
})
wss.on("connection", (socket) => {})
이벤트 처리를 위한 콜백 함수가 on 메서드 안에 들어온 것임.
바깥에 함수를 선언한 다음 on메서드에 인자를 전달하는 방식으로 구현해도 무방하지만
직관성을 위해 on 메서드에 익명함수를 만들어 포함되는 형태로 코드 구성
sockets.push(socket)
웹소켓 서버에 connection 이벤트 (새사용자가 접속하는것)이 발생할 때마다
sockets배열에 생성된 소켓을 push 해줌
socket["nickname"] = "Anonymous"
일단 socket의 nickname의 키에 모두 Anonymous를 할당해주고
나중에 자기 닉네임을 스스로 전달하는 경우 전달된 닉네임으로 바꿔주기위한 코드
socket.on('close' , () => console.log("Disconnected from Browser"))
소켓에 close 이벤트가 발생할 경우 console.log를 실행해줌
socket.on("message" , (msg) => {
const message = JSON.parse(msg)
우리는 app.js에서 JSON을 stringify한 문자열을 담아서 send 했습니다.
socket에 message 이벤트가 발생했을때 안에 담긴 내용물은 우리가 stringfy한 문자열일테니
이걸 다시 parse()해줍니다.
switch(message.type) {
case "new_message" :
sockets.forEach(aSocket => aSocket.send(`${socket.nickname} : ${message.payload}`))
break
case "nickname" :
socket["nickname"] = message.payload
break
}
message.type의 밸류가 new_message인 경우 sockets 배열에 forEach를 실행하고
각 소켓에 send를 해주는데 send하는 내용은 지금 전달받은 소켓의 닉네임과 메시지의 내용입니다.
case "nickname" :
socket의 닉네임에 내용을 할당해줍니다.
message, socket의 데이터가 어떻게 구성되어있는지
헷갈리기시작함... ㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠ
server.listen(3000, handleListen);
server.listen() 메서드는 지정된 포트 또는 경로에 수신기를 만듭니다.
server.listen(port, hostname, backlog, callback);
모르는게 너무 많다보니 에라 모르겠다하고 넘어가면서 봐도
쉽지않네요..
웹소켓과 message이벤트는 어떻게 구성되어 있는지 궁금
근데 웹소켓의 정보를 뜯어보다가 이상한걸 찾았습니다
readyState? 이거머임
readyState가 1이라는건 웹소켓 연결이 개방되어서 통신할 수 있다는 뜻이군요!
마지막으로 깃허브에 올려둔 readme..
이제 자야겠음
'zoom websocket' 카테고리의 다른 글
[zoom clone] 닉네임, 프라이빗룸 기능 구현 (1) | 2022.12.23 |
---|