깃허브 링크
프로젝트 생성
npm init -y
nodemon, babel 설치
npm i -D nodemon
touch babel.config.json
touch nodemon.json (nodemon 명령어 파일)
npm i -D @babel/core @babel/cli @babel/node @babel/preset-env
npm run dev 프로젝트 실행
// package.json "scripts": { "dev": "nodemon" } dev 명령어 입력 => nodemon 실행 => nodemon.json파일 참조 => exec 명령어 실행 => npm run babel-node src/server.js 실행
Express/PUG 설치
npm i express
npm i pug
[프로토콜] HTTP vs WebSocket
HTTP
stateless
- 서버는 응답과 동시에 클라이언트와 연결을 끊는다.
서버는 오직 요청이 올 때만, 응답한다.
WebSocket
stateful
- 서버와 클라이언트가 서로 요청과 응답을 한다.
서버는 클라이언트의 정보를 갖고 있는다.
한번 연결되면 지속적인 실시간 대화가 가능하다.
WS vs SocketIO
npm i ws
nodeJS에서 사용되는 WebSocket 프로토콜을 이용한 라이브러리이다.
npm i socket.io
webSocket을 사용하지만, 이것만 사용하지 않는다.
webSocket API를 지원하지 않는 오래된 브라우저에서도 실시간, 양방향 통신이 가능하도록 도와준다.
private room
이 있다.소켓ID
가 있어 구별하기 쉽다.참고 블로그 : https://inpa.tistory.com/category/Node.js/Socket.IO
👨🏻💻 클라이언트 (front/js/index.js)
// socketIO 사용하는 서버와 연결 const connectionServer = io(); // 닉네임 변수 const nameForm = document.querySelector("#nameForm"); const nameInput = nameForm.querySelector("input"); const useNameBtn = nameForm.querySelector("#useNameBtn"); const editNameBtn = nameForm.querySelector("#editNameBtn"); // 대기실 변수 const waitingRoom = document.querySelector("#waitingRoom"); const enterForm = waitingRoom.querySelector("form"); const enterInput = enterForm.querySelector("input"); // 채팅방 변수 const chatRoom = document.querySelector("#chatRoom"); const chatForm = chatRoom.querySelector("form"); const chatInput = chatForm.querySelector("input"); const chatDiv = document.querySelector("#chatDiv"); // ul태그 감싼 div const exitRoomBtn = document.querySelector("#exitRoomBtn"); // 🚀 닉네임 폼 submit 이벤트 nameForm.addEventListener("submit", (event) => { event.preventDefault(); connectionServer.emit("setName", nameInput.value, () => { nameInput.disabled = true; useNameBtn.style.display = "none"; editNameBtn.style.display = "inline-block"; }); }); // 🚀 닉네임 폼 click 이벤트 editNameBtn.addEventListener("click", (event) => { event.preventDefault(); nameInput.disabled = false; useNameBtn.style.display = "inline-block"; editNameBtn.style.display = "none"; }); // 🚀 대기실 폼 submit 이벤트: 대기실 -> 채팅방 입장 함수 enterForm.addEventListener("submit", (event) => { event.preventDefault(); connectionServer.emit("enterRoom", enterInput.value, (roomName, count) => { chatRoom.style.display = "inline-block"; waitingRoom.style.display = "none"; setRoomTitle(roomName, count); }); }); // 🚀 채팅방 폼 usbmit 이벤트: 채팅방 채팅 함수 chatForm.addEventListener("submit", (event) => { event.preventDefault(); connectionServer.emit( "sendChat", chatInput.value, enterInput.value, (msg) => { addMsg(msg, true); } ); chatInput.value = ""; chatInput.focus(); }); // 🚀 채팅방 나가기 버튼 이벤트 exitRoomBtn.addEventListener("click", () => location.reload()); /* 🚀 socketIO 함수 - welcomeMsg : 입장 알림 - byeMsg : 퇴장 알림 - sendChat : 채팅 입력 - editNameMsg : 닉네임 변경 알림 - changeRoom : 공개방 조회 및 출력 */ connectionServer.on("welcomeMsg", (name, count) => { addMsg(`${name}님이 입장하셨습니다.`, false); setRoomTitle(name, count); }); connectionServer.on("byeMsg", (name, count) => { addMsg(`${name}님이 퇴장하셨습니다.`, false); setRoomTitle(name, count); }); connectionServer.on("sendChat", (msg, name) => { addMsg(`[${name}]: ${msg}`, false); }); connectionServer.on("editNameMsg", (prevName, editName) => { addMsg(`${prevName} -> ${editName} 이름변경`, false); }); connectionServer.on("changeRoom", (rooms) => { const publicRoomUl = document.querySelector("#publicRoomList"); publicRoomUl.innerHTML = ""; if (rooms.length === 0) { publicRoomUl.innerHTML = ""; return; } rooms.forEach((room) => { const publicRoomLi = document.createElement("li"); publicRoomLi.innerText = room; publicRoomUl.appendChild(publicRoomLi); }); }); /* 🚀 공통함수 - addMsg() : 채팅 추가 함수 - addMyMsg() : 나의 채팅 추가 함수 - setRoomTitle() : 채팅방 이름 설정 함수 */ function addMsg(msg, isMyMsg) { const chatList = chatRoom.querySelector("ul"); const chat = document.createElement("li"); if (isMyMsg) { chat.id = "myMsg"; } chat.innerText = msg; chatList.appendChild(chat); chatDiv.scrollTop = chatDiv.scrollHeight; // 채팅 스크롤 항상 아래로 } function setRoomTitle(roomName, count) { document.querySelector( "#roomTitle" ).innerText = `채팅방: ${roomName} (${count})`; }
👨🏻💻 서버 (server.js)
// express http 서버 const httpServer = http.createServer(app); // express http 서버 기반으로 생성한 socketIO 서버 const ioServer = new Server(httpServer, { cors: { origin: ["https://admin.socket.io"], credentials: true, }, }); // socketIO admin 페이지 설정 instrument(ioServer, { auth: false, }); /* 🚀 getPublicRooms(): 공개방만 구하는 함수 sids: 서버와 연결된 socket ID 리스트 rooms: 생성된 채팅방 리스트 (개인방 포함) publicRooms : 유저가 직접 생성한 채팅방 리스트 (공개방만) 🚀 getCount() : 해당 공개방의 인원수 구하는 함수 */ function getPublicRooms() { const { sids, rooms } = ioServer.sockets.adapter; const publicRooms = []; rooms.forEach((_, key) => { if (sids.get(key) === undefined) { publicRooms.push(key); } }); return publicRooms; } function getCount(roomName) { return ioServer.sockets.adapter.rooms.get(roomName)?.size; } /* 📦 socketIO Server - connection : 서버와 브라우저가 연결되었을 때 - disconnecting : 채팅방과 연결 끊기기 직전 이벤트 - disconnect : 채팅방과 연결이 끊어진 이후 이벤트 */ ioServer.on("connection", (frontSocket) => { frontSocket.onAny((event) => console.log(`🚀 [Event] ${event}`)); frontSocket.name = "익명"; // 초기 닉네임 설정 // 🚀 changeRoom() ioServer.sockets.emit("changeRoom", getPublicRooms()); // 🚀 setName() frontSocket.on("setName", (editName, done) => { const prevName = frontSocket.name; frontSocket.name = editName; // 🚀 editNameMsg() frontSocket.rooms.forEach((eachRoom) => { frontSocket.to(eachRoom).emit("editNameMsg", prevName, editName); }); done(); }); // 🚀 enterRoom() frontSocket.on("enterRoom", (roomName, done) => { frontSocket.join(roomName); // 해당 채팅방 입장 done(roomName, getCount(roomName)); // 입장 이후 브라우저에게 채팅방 이름을 포함한 제어권 전달 // 🚀 welcomeMsg() frontSocket .to(roomName) .emit("welcomeMsg", frontSocket.name, getCount(roomName)); // 본인 이외에 같은 채팅방 유저에게 id전달 // 🚀 changeRoom() ioServer.sockets.emit("changeRoom", getPublicRooms()); }); // 🚀 sendChat() frontSocket.on("sendChat", (msg, roomName, done) => { frontSocket.to(roomName).emit("sendChat", msg, frontSocket.name); done(msg); }); frontSocket.on("disconnecting", () => { // 🚀 byeMsg() frontSocket.rooms.forEach((eachRoom) => { frontSocket .to(eachRoom) .emit("byeMsg", frontSocket.name, getCount(eachRoom) - 1); }); }); frontSocket.on("disconnect", () => { // 🚀 changeRoom() ioServer.sockets.emit("changeRoom", getPublicRooms()); }); });
👨🏻💻 PUG (index.html)
body header h1 WA_ZOOM 😎 h2#roomTitle div#waitingRoom form#enterForm(action="", method="get") h3 와줌 채팅방 리스트 #publicRoomList ul input(type="text", name="enterInput", placeholder="채팅방 이름 입력", maxLength="10" required) button 채팅방 입장하기 div#nameDiv form#nameForm(action="", method="get") h3 닉네임 설정 input(type="text", name="nameInput", placeholder="사용할 닉네임 입력", maxLength="7", required) button#useNameBtn 사용하기 button#editNameBtn 수정하기 div#chatRoom #chatDiv ul form#chatForm(action="", method="get") h3 input(type="text", name="msgInput", placeholder="메시지 입력", required, maxLength="20") #chatBtnDiv button 메시지 전송 button#exitRoomBtn 나가기 script(src="/socket.io/socket.io.js") <!-- 브라우저에 socketIO 설치 --> script(src="/static/js/index.js")