이번 챕터에서는 socket.io를 이용해서 특정 채팅방 안에서 사용자가 왔다갔다 할 때마다 알림이 뜨고, 닉네임을 설정하고 메시지를 전달할 수 있도록 만들어보도록 하겠습니다!

 

※ 본 내용은 줌 클론코딩의 2.4~2.7 까지의 수업 내용을 담고 있습니다!

 

목차


    0. 방 만들기

    앞에서 방을 만들기 위한 다양한 준비를 했었는데요,

    socket.io를 이용하면 정말 쉽고 간단하게 방을 만들 수 있습니다!!

     

    0.0. server.js

     

    위 코드를 보시면, join 메서드 단 하나만으로 쉽게 방을 만든 모습을 보실 수 있습니다.

    ("welcome" 뭐시기 하는 부분은 아래에서 더 자세히 확인하실 수 있습니다!)

     

    그 외에도 이번 단계에서 새롭게 사용한 socket.io 코드들을 차근차근 살펴보면 아래와 같습니다.

     

    • join : 방에 들어가는 기능을 하는 메서드
    • onAny : 소켓 내에서 발생한 이벤트를 캐치하는 역할을 하는 메서드
    • to : 특정 위치에 이벤트를 전달하기 위해 사용하는 메서드
    • rooms : 현재 소켓이 들어가있는 방을 표시하는 메서드
    • id : 현재 소켓의 id를 나타내는 메서드

     

    이처럼 다양한 역할들을 하는 메서드들을 아주 쉽게 사용할 수 있습니다.

    더 자세한 내용을 참고하고 싶으시다면 아래 socket.io 홈페이지를 참고하시면 좋습니다!

     

     

    Server API

    ServerExposed by require("socket.io"). Related documentation pages: installation initialization details of the server instance new Server(httpServer[, options]) httpServer (http.Server) th

    socket.io

     

    0.1. app.js

    서버에서 enter_room 이벤트를 받겠다고 했으니, 클라이언트에서는 enter_room 이벤트를 전송해 줘야겠죠?

    handleRoomSubmit를 조금 바꿔봅시다.

     

    원래 있었던 backendDone 함수를 showRoom으로 바꾸고, 안에 있는 backendDone도 바꿔줍니다.

    추가로 굳이 payload를 객체 형태로 전달할 필요가 없어서 input.value를 따로 밖으로 빼버렸습니다.

    (roomName = input.value는 지금 단계에서는 신경쓰실 필요가 없습니다!)

     

     

    여기까지 완성하고 나면, 방에 접속이 가능해집니다!

    console.log를 이용해서 어떤 사용자가 어떤 방에 접속했는 지도 출력할 수 있죠!

     

    payload를 빼기 전이네요..?! 앞이 ID고 뒤가 방이름입니다!

     

    아, 추가로 방에 들어가기 전에는 닉네임이나 메시지를 보낼 수 있는 form 박스가 보이지 않도록 app.js에서 querySelector로 가져온 room에 hidden 속성을 부여해봅시다!

     

     


    1. 알림 보내기

    1.0. server.js

    이제 사용자가 접속했을 때와 접속을 끊었을 때 알림을 보내는 기능을 추가해보겠습니다.

     

    아까 봤던 사진이죠? 방에 접속하면 toemit을 이용해서 roomName이라는 방에 있는 사람들에게 welcome 메시지를 보낼 수 있게 만들었습니다.

    또한 disconnecting이라는 상태를 통해서 클라이언트가 서버와의 연결이 끊어지는 경우 끊어지기 직전에 해당 클라이언트가 소속해있던 모든 방에 bye라는 메시지를 남길 수 있도록 만들었습니다. 

     

     

    disconnecting 이벤트에 대해 조금 더 알고 싶으시다면 아래 링크를 참조하세요!

     

     

    Server API

    ServerExposed by require("socket.io"). Related documentation pages: installation initialization details of the server instance new Server(httpServer[, options]) httpServer (http.Server) th

    socket.io

     

    1.1. app.js

    이제 app.js에서는 사용자가 들어오고 나갔을 때 받는 welcome, bye 이벤트에 대해서 설정을 해 줘야겠죠?

    각각 메시지를 화면에 전달할 수 있도록 했습니다.

    addMessage 함수를 따로 분리해서 재사용하기 쉽게 만든 부분이 인상적입니다!

     

     

     

     

    여기까지 완성하면, 서로 다른 두 브라우저에서 각각 접속했을 때 아래와 같은 알림이 정상적으로 출력되는 것을 확인하실 수 있습니다!

     

    들어왔을 때!!

     

    나갔을 때!!!

     


    2. 메시지 전송하기

    이제 닉네임을 설정하고 메시지를 보냈을 때 누가 보냈는지 확인할 수 있도록 만들어보겠습니다.

     

    2.0. home.pug

    먼저 html을 수정해줍니다.

    name과 msg를 id로 가지는 form태그를 2개 만들어줍니다.

    name에서는 닉네임을 설정할 수 있고, msg에서는 메세지를 보낼 수 있겠죠?

     

     

    2.1. app.js

    그 다음 app.js를 수정해줍니다.

    메세지와 닉네임을 보낼 수 있는 핸들러를 만들어줍니다.

    querySelector를 통해 id가 room인 요소 아래에 있는 것들 중에서 name이나 msg라는 id를 가진 친구의 input을 찾아서 input으로 등록하게 되는 구조입니다.

     

    handleMessageSubmit에서 value를 굳이 따로 만든 이유는 만약 value를 만들지 않았다면 생기는 문제점에 대해서 알아보면 쉽게 이해할 수 있습니다. (value값을 따로 저장해 두지 않았을 때는 addMessage 안에 들어있는 value가 원래는 input.value였습니다!)

    만약 input.value를 따로 저장해두지 않으면, socket.emit 함수가 동작하고 메시지를 보내기 전에 input.value를 초기화 하는 작업이 비동기로 먼저 작동하여서 addMessage가 돌아갈 때는 이미 우리가 보내고 싶었던 input.value가 초기화가 되어버립니다.

    값을 따로 저장하는 것은 이런 상황을 방지하기 위해서입니다.

     

     

    서버로부터 메시지를 전송받았을 때의 이벤트도 처리합니다.

    다른 사람이 보내면 서버는 그 메시지를 모두에게 뿌려주겠죠?

    그 때 작동하는 부분이라고 생각하시면 됩니다!

    addMessage에는 별도의 매개변수를 넣지 않아도 알아서 msg가 잘 들어가게 됩니다!

     

     

    2.2. server.js

    마지막으로 서버에서 처리하는 부분을 만들어보겠습니다.

    새로운 메시지와 어느 방인지, 처리가 다 끝나면 어떤 함수를 실행할 지를 받아서 작업합니다.

    닉네임과 메세지가 뜨도록 설정해두었습니다.

    done 함수는 백엔드가 아니라 프론트엔드로 돌아가서 작동한다는 것을 꼭 기억해두시면 좋습니다!!

     

    아래 닉네임은, 그냥 닉네임 설정입니다.

     

     

    기본 닉네임을 만들어두기 위해서 맨 위쪽에 익명이라는 이름을 디폴트로 설정해줄 수도 있습니다.

     

     

    여기까지 완성하시면 아래와 같이 메시지가 잘 전달되는 것을 확인하실 수 있습니다!!

     

     

    추가로 업그레이드 할 수 있는 부분은 방에 접속하기 전에 닉네임을 설정하는 기능도 만들어볼 수도 있을 것이라고 생각합니다.

    크게 어렵지 않아서 니코쌤도 설명해주지 않으셨는데, 한번 도전해보시는 것은 어떨까요!!

     


    3. 소스코드

    아래는 순서대로 home.pug, app.js, server.js 소스코드입니다!

     

    //- home.pug
    
    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(placeholder="room name", required, type="text")
                        button Enter Room 
                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")
    // app.js
    
    const socket = io(); // io function은 알아서 socket.io를 실행하고 있는 서버를 찾을 것이다!
    
    // 방을 만들것!! (socket IO에는 이미 방기능이 있다!)
    
    const welcome = document.getElementById("welcome");
    const form = welcome.querySelector("form");
    const room = document.getElementById("room");
    
    room.hidden = true; // 처음에는 방안에서 할 수 있는 것들 안보이게!
    
    let roomName;
    
    function addMessage(message){
        const ul = room.querySelector("ul");
        const li = document.createElement("li");
        li.innerText = message;
        ul.appendChild(li);
    }
    
    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}`);
        }); // 백엔드로 new_message 이벤트를 날림, (input.value이랑 방이름도 같이 보냄!), 마지막 요소는 백엔드에서 시작시킬 수 있는 함수!
        input.value = "";
    }
    
    function handleNicknameSubmit(event){
        event.preventDefault();
        const input = room.querySelector("#name input");
        socket.emit("nickname", input.value);
    }
    
    
    function showRoom() { // 방에 들어가면 방 내용이 보이게
        welcome.hidden = true;
        room.hidden = false; 
        const h3 = room.querySelector("h3");
        h3.innerText = `Room ${roomName}` // 저장된 방 이름을 pug의 요소에 전달해서 띄움! 
        const msgForm = room.querySelector("#msg");
        const nameForm = room.querySelector("#name");
        msgForm.addEventListener("submit", handleMessageSubmit);
        nameForm.addEventListener("submit", handleNicknameSubmit);
    }
    
    function handleRoomSubmit(event){
        event.preventDefault();
        const input = form.querySelector("input");
        // argument 보내기 가능 (socketIO는 Object 전달가능)
        // 첫 번째는 이벤트명(아무거나 상관없음), 두 번째는 front-end에서 전송하는 object(보내고 싶은 payload), 세 번째는 서버에서 호출하는 function
        socket.emit( // emit의 마지막 요소가 function이면 가능
            "enter_room",
            input.value,
            showRoom // 백엔드에서 끝났다는 사실을 알리기 위해 function을 넣고 싶다면 맨 마지막에 넣자!
        ); // 1. socketIO를 이용하면 모든 것이 메세지일 필요가 없다! / 2. client는 어떠한 이벤트든 모두 emit 가능 / 아무거나 전송할 수 있다(text가 아니어도 되고 여러개 전송 가능!)
        roomName = input.value; // roomName에 입력한 방 이름 저장
        input.value = "";
    }
    
    // 서버는 back-end에서 function을 호출하지만 function은 front-end에서 실행됨!!
    
    form.addEventListener("submit", handleRoomSubmit);
    
    socket.on("welcome", (user) => {
        addMessage(`${user} arrived!`);
    })
    
    socket.on("bye", (left) => {
        addMessage(`${left} left ㅠㅠ`);
    })
    
    socket.on("new_message", addMessage); // addMessage만 써도 알아서 msg를 매개변수로 넣는다!
    // server.js
    
    import http from "http"; // 이미 기본 설치되어있음
    import WebSocket from "ws"; // 기본설치!
    import express from "express"; // npm i express 설치
    import SocketIO from "socket.io"; 
    
    const app = express(); // app이라는 변수에 가져와서 사용
    
    app.set("view engine", "pug"); // 뷰 엔진을 pug로 하겠다
    app.set("views", __dirname + "/views"); // 디렉토리 설정
    app.use("/public", express.static(__dirname + "/public")); // public 폴더를 유저에게 공개 (유저가 볼 수 있는 폴더 지정)
    app.get("/", (req, res) => res.render("home")); // 홈페이지로 이동할 때 사용될 템플릿을 렌더
    app.get("/*", (req, res) => res.redirect("/")) // 홈페이지 내 어느 페이지에 접근해도 홈으로 연결되도록 리다이렉트 (다른 url 사용 안할거라)
    
    const handleListen = () => console.log(`Listening on http://localhost:3000`)
    // app.listen(3000, handleListen); // 3000번 포트와 연결
    
    const httpServer = http.createServer(app); // app은 requestlistener 경로 - express application으로부터 서버 생성
    const wsServer = SocketIO(httpServer); // localhost:3000/socket.io/socket.io.js로 연결 가능 (socketIO는 websocket의 부가기능이 아니다!!)
    
    // websocket에 비해 개선점 : 1. 어떤 이벤트든지 전달 가능 2. JS Object를 보낼 수 있음
    wsServer.on("connection", socket => {
        socket["nickname"] = "Anonymous";
        socket.onAny((event) => { // 미들웨어같은 존재! 어느 이벤트에서든지 console.log를 할 수 있다!
            console.log(`Socket Event:${event}`)
        })
        socket.on("enter_room", (roomName, done) => {
            // console.log(socket.rooms); // 현재 들어가있는 방을 표시 (기본적으로 User와 Server 사이에 private room이 있다!)
            socket.join(roomName);
            // console.log(socket.rooms);  // 앞은 id, 뒤는 현재 들어가있는 방
            done();
            socket.to(roomName).emit("welcome", socket.nickname) // welcome 이벤트를 roomName에 있는 모든 사람들에게 emit한 것
        });
        socket.on("disconnecting", () => { // 클라이언트가 서버와 연결이 끊어지기 전에 마지막 굿바이 메시지를 보낼 수 있다!
            socket.rooms.forEach(room => socket.to(room).emit("bye", socket.nickname)); // 방안에 있는 모두에게 보내기 위해 forEach 사용!
        })
        socket.on("new_message", (msg, room, done) => { // 메세지랑 done 함수를 받을 것
            socket.to(room).emit("new_message", `${socket.nickname}: ${msg}`); // new_message 이벤트를 emit한다! 방금 받은 메시지가 payload가 된다!
            done(); // done은 프론트엔드에서 코드를 실행할 것!! (백엔드에서 작업 다 끝나고!!)
        });
        socket.on("nickname", nickname => socket["nickname"] = nickname);
    });
    
    
    // 웹소켓 사용한 부분 주석처리!
    // const wss = new WebSocket.Server({ server }); // http 서버 위에 webSocket서버 생성, 위의 http로 만든 server는 필수 X - 이렇게 하면 http / ws 서버 모두 같은 3000번 포트를 이용해서 돌릴 수 있다!
    
    // const sockets = []; // 누군가 우리 서버에 연결하면 그 connection을 여기에 넣을 것이다!!
    
    
    // // on method에서는 event가 발동되는 것을 기다린다
    // // event가 connection / 뒤에 오는 함수는 event가 일어나면 작동
    // // 그리고 on method는 backend에 연결된 사람의 정보를 제공 - 그게 socket에서 옴
    // // 익명함수로 바꾸기
    // wss.on("connection", socket => { // 여기의 socket이라는 매개변수는 새로운 브라우저를 뜻함!! (wss는 전체 서버, socket은 하나의 연결이라고 생각!!)
    //     sockets.push(socket); // 파이어폭스가 연결되면 sockets 배열에 firefox를 넣어줌! (다른 브라우저도 마찬가지!)
    //     socket["nickname"] = "Anonymous"; // 익명 소켓인 경우 처리 - 맨 처음 닉네임은 Anonymous
    //     console.log("Connected to Browser ✅");
    //     socket.on("close", () => console.log("Disconnected to Server ❌")); // 서버를 끄면 동작
    //     socket.on("message", msg => {
    //         const message = JSON.parse(msg);
    //         // new_message일 때 모든 브라우저에 payload를 전송!
    //         // if / else if로 해도 잘 돌아간다!
    //         // 받아온 String 형태의 메시지(바로 출력하면 Buffer로 뜨지만..!)를 parse로 파싱한 후 구분해서 출력
    //         switch(message.type){
    //             case "new_message":
    //                 sockets.forEach((aSocket) => aSocket.send(`${socket.nickname}: ${message.payload}`));
    //             case "nickname":
    //                 socket["nickname"] = message.payload; // socket은 기본적으로 객체라 새로운 아이템 추가 가능! : 닉네임을 socket 프로퍼티에 저장중!
    //         }
    //         // const utf8message = message.toString("utf8"); // 버퍼 형태로 전달되기 때문에 toString 메서드를 이용해서 utf8로 변환 필요!
    //         // sockets.forEach(aSocket => aSocket.send(utf8message)); // 연결된 모든 소켓에 메시지를 전달!!
    //         // socket.send(utf8message);
    //     }); // 프론트엔드로부터 메시지가 오면 콘솔에 출력
    // }) // socket을 callback으로 받는다! webSocket은 서버와 브라우저 사이의 연결!!!
    
    
    httpServer.listen(3000, handleListen); // 서버는 ws, http 프로토콜 모두 이해할 수 있게 된다!

     


     

    반응형
    • 네이버 블로그 공유하기
    • 네이버 밴드에 공유하기
    • 페이스북 공유하기
    • 카카오스토리 공유하기