이번에는 두 브라우저가 동시에 연결되어서 메시지를 주고받는 것과, 닉네임을 설정하는 것을 구현해보도록 하겠습니다.

 

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

 

목차


    0. 채팅 완성하기

    서버와 클라이언트간에 서로 연결이 되었으니, 이제 서로 메시지를 주고 받는 것을 구현해보도록 하겠습니다.

    먼저 home.pug에서 메시지를 담을 수 있는 input box와 send button을 만들어줍니다.

     

     

    이제 send 버튼을 눌렀을 때 발생하는 이벤트를 처리해주기 위해서 app.js를 바꿔줍니다.

    home.pug(보여지는 페이지)에서 app.js(클라이언트)를 통해 server.js(서버)로 데이터가 전달되는 방식이죠!

    순서대로 바꿔주면 됩니다!

     

    home.pug에서 설정한 ul 내의 form들의 모임을 각각 messageList, messageForm으로 받아옵니다.

     

     

    Send 버튼을 눌렀을 때 발생하는 이벤트인 submit를 처리하기 위한 이벤트리스너도 만들어야겠죠?

    handleSubmit 함수를 만들어서 이벤트를 처리해줍니다.

     

     

    마지막으로, server.js를 수정합니다.

    37줄처럼 만들면 보낸 사람에게 다시 돌려주지만, 이것은 우리가 원하는 기능이 아닙니다. (자기가 보낸 메시지를 자기만 받아봐야 뭐합니까...)

     

    우리가 원하는 기능은 내가 보낸 채팅이 모든 사람에게 보여질 수 있는 것이기 때문에 sockets 배열을 만들어서 연결된 모든 소켓을 배열에 넣고 배열 내의 모든 브라우저에 메시지를 뿌릴 수 있도록 만들어야 합니다.

     

    sockets가 없을 때와 있을 때의 차이

     

     

    왼쪽 : 파이어폭스 / 오른쪽 : 크롬

     


    1. 닉네임 만들기

    이제 콘솔창이 아닌 화면에 채팅을 띄우고, 닉네임을 설정해서 구분할 수 있도록 해 보겠습니다.

     

    html에서 닉네임을 설정할 수 있는 form을 하나 만들어줍니다.

    #이 붙어있는데, 이건 아래에서 설명하겠습니다.

     

    //- 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
                //- 닉네임 설정
                form#nick
                    input(type="text", placeholder="choose a nickname", required)
                    button Save
                //- 메시지리스트 : 앞으로 리스트를 채워줄 것
                ul 
                form#message
                    input(type="text", placeholder="write a msg", required)
                    button Send
            script(src="/public/js/app.js")

     

    앞에서 handler 함수를 하나 만들어서 입력값을 가져왔었죠?

    이번에도 같은 방식으로 가져옵니다.

     

    그런데 입력창이 2개죠?

    어떤 입력창에서 작성한 요소를 가져오는 지를 파악하기 위해서 #으로 구분해 준 것입니다. (이것을 id값 할당이라고 부릅니다.)

     

    이렇게 닉네임으로 쿼리셀렉터를 쓸 수 있습니다.

     

    또 하나의 문제가 있는데, 서버로 보내는 메시지는 반드시 String 형태여야 합니다. (우리가 만든 프로그램이 반드시 JS으로 구현된 브라우저에서만 사용될 것은 아니니까요!)

    따라서 서버는 어떤 메시지가 닉네임이고 어떤 메시지가 채팅인지 구분할 수가 없습니다.

     

    그렇다면 어떻게 서버가 구분할 수 있도록 만들 수 있을까요?

    바로 JSON형태를 사용하는 것입니다.

    JSON 형태를 이용하면 그 메시지의 키값을 통해 닉네임인지 채팅인지 확인할 수 있겠죠?

     

    JSON 형태의 데이터

     

    대신 서버는 String 형태의 데이터만 받을 수 있기 때문에 따로 JSON을 String으로 변환해주는 함수를 만들어야 합니다.

    이건 JSON.stringify()를 이용하면 쉽게 만들 수 있습니다.

     

     

    그리고 우리가 보낸 메시지를 클라이언트 화면에 출력해 주어야 합니다.

    html파일에 li라는 리스트 요소를 만들고, 그 요소 안에 데이터를 넣어서 출력하는 방식으로 구현하였습니다. 

     

     

    // app.js
    
    const messageList = document.querySelector("ul");
    const nickForm = document.querySelector("#nick");
    const messageForm = document.querySelector("#message");
    
    // alert("hi"); // frontend에서 backend랑 연결해 달라고 해야 앞에서 만든 wss.on의 connection이 작동한다!
    
    const socket = new WebSocket(`ws://${window.location.host}`); // 이제 서버로 접속 가능! - 여기 socket을 이용해서 frontend에서 backend로 메세지 전송 가능!
    // 여기 socket은 서버로의 연결
    
    
    // makeMessage : JSON을 String 형태로 바꿔주는 함수
    // 사용하는 클라이언트가 GO일 수도 있고, JAVA일 수도 있기 때문에 Javascript Object 형태로 보내면 안되고, String 형태로 보내서 모든 언어를 대비할 수 있어야 한다!
    // 우리가 사용하는 API인 websocket은 브라우저의 API이기 때문이다!!
    // 백엔드에서는 다양한 프로그래밍 언어를 사용할 수 있기 때문에 API는 어떠한 판단도 하면 안된다!
    function makeMessage(type, payload){
        const msg = {type, payload};
        return JSON.stringify(msg); 
    }
    
    
    socket.addEventListener("open", ()=>{ // open되면 동작
        console.log("Connected to Server ✅");
    })
    
    socket.addEventListener("message", message => {
        // console.log("New message: ", message.data);
        const li = document.createElement("li"); // 새로운 메시지를 받으면 새로운 li 생성
        li.innerText = message.data; // message.data를 li 안에 넣어주기
        messageList.append(li);
    });
    
    socket.addEventListener("close", () => {
        console.log("Disconnected to Server ❌");
    });
    
    // setTimeout(() => {
    //     socket.send("hello from the browser!"); // backend로 메시지 보내기!
    // }, 10000); // 10초 후에 작동
    
    
    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 = "";
    }
    
    messageForm.addEventListener("submit", handleSubmit);
    nickForm.addEventListener("submit", handleNickSubmit);
    
    // { // 일반 메시지와 닉네임을 구분하기 위해 JSON 형식으로 전송!
    // // 우리는 text만 전송이 가능하므로 이걸 보내야하는데, 보낸 후에 parse를 이용해서 다시 JSON형태로 복구시키면 된다!
    // type:"nickname",
    // payload:input.value,
    // }

     

    마지막으로 서버를 보겠습니다.

    서버에서는 받아온 String 형태의 메시지를 다시 JSON 형태로 바꾸고, 키값에 따라 다르게 동작하도록 만들어야 합니다.

    이 과정을 switch를 이용해서 구현하였습니다. (if / elseif 형태로도 동일하게 만들 수 있습니다!)

     

    특징으로는 받아온 String파일을 바로 사용하는게 아니라 parse한 후에 출력하기 때문에 이전처럼 버퍼를 다시 String으로 바꿔서 사용할 필요가 없습니다. (Windows 환경 기준입니다!!)

     

    또한 socket이 객체 형태이기 때문에 별도의 키값을 부여해서 데이터를 저장할 수도 있다는 점을 활용했습니다. (마지막 문장을 보시면, 브라우저로부터 받아온 socket에 nickname을 저장해서 사용합니다!)

     

     

    // server.js
    
    import http from "http"; // 이미 기본 설치되어있음
    import WebSocket from "ws"; // 기본설치!
    import express from "express"; // npm i express 설치
    
    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 server = http.createServer(app); // app은 requestlistener 경로 - express application으로부터 서버 생성
    
    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은 서버와 브라우저 사이의 연결!!!
    
    
    server.listen(3000, handleListen); // 서버는 ws, http 프로토콜 모두 이해할 수 있게 된다!

     

    이제 서버를 실행하고 두 브라우저에서 확인해보면, 정상적으로 닉네임이 설정되고 메시지가 출력되는 모습을 확인하실 수 있습니다!!

     

     


    지금까지는 모든 내용을 전부 구현했는데, 앞으로의 강의에서는 조금 더 편리하게 만들기 위해서 라이브러리를 이용해서 이미 만들어져 있는 기능들을 쉽게 사용할 수 있는 방법에 대해 설명해 주신다고 합니다!

    앞으로의 강의가 더 기대되네요 :)

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