Showing

[Node, Vue] 클론코딩 Zoom(5) 채팅룸에서 닉네임 달고 메시지 주고 받기 본문

Node

[Node, Vue] 클론코딩 Zoom(5) 채팅룸에서 닉네임 달고 메시지 주고 받기

RabbitCode 2023. 6. 5. 05:34

* 노마드 코더의 Do it! 클론코딩 줌 책을 정리한 포스팅입니다.

 

1. disconnecting 이벤트 이해하기

채팅룸을 나갈 때 메시지를 표시하는 기능을 추가하도록 한다. 이때 사용할 socket.io의 이벤트는 disconnecting이다. disconnecting 이벤트는 브라우저 창을 닫거나 컴퓨터를 꺼서 채팅룸을 나가기 직전에 발생하는 이벤트이다. 이를 이용하면 채팅룸을 빠져나가기 직전에 메시지를 보낼 수 있다. 메시지 전송 방식은 채팅룸에 접속할 때와 거의 같다.

(1) 서버 코드 변경

wsServer.on('connection',(socket)=>{
    //console.log(socket);
    socket.on('enter_room',(roomName, done)=>{
        done();
        //console.log(roomName);
        //console.log(socket.id);
        //console.log(socket.rooms);
        socket.join(roomName);
        socket.to(roomName).emit("welcome");
        socket.on('disconnecting', ()=>{
            socket.rooms.forEach(room => {
                socket.to(room).emit("bye");                
            });
        });
        //console.log(socket.rooms);
        // setTimeout(()=>{
        //     done();
        // }, 5000);
    });
});
const handleListen = ()=>console.log("Listening on 3000 port");
httpServer.listen(3000, handleListen);

disconnecting 이벤트가 발생하면 사용자 소켓이 bye 이벤트를 발생시킨다. 접속 중이던 채팅룸에만 이벤트를 발생시키기 위해서 room 속성을 사용하였다. rooms는 접속 중인 채팅룸 목록을 뜻하는 셋(Set) 객체인데, 셋은 배열처럼 사용할 수 있는 객체이므로 forEach 메서드를 사용하면 셋이 포함하고 있는 개별 요소에 접근해 콜백함수를 호출할 수 있다. 

 

(2) 프런트 코드 변경

누군가 채팅룸을 나가면 그 사용자 소켓으로부터 bye 이벤트가 발생한다. bye 이벤트를 발생시킨 소켓은 이벤트가 발생하고 나서 바로 채팅룸을 떠나 버리기 때문에 이 이벤트를 처리하는건 남아있는 다른 사용자 소켓들의 몫이다. 

socket.on("bye", ()=>{
    addMessage("Someone left ㅠㅠ");
});

이렇듯 socket.io가 우리에게 제공하는 disconnecting 이벤트를 사용하면 채팅룸을 나가는 상황에서 무언가를 하도록 만들 수 있다. 꼭 메시지 출력이 아니라도 말이다.

 

2. 메시지 보내기

(1) 프런트 코드

아직은 메시지를 교환하거나 사용자 닉네임을 따로 표시하지도 못한다. 메시지 입력 폼에서 제출(submit)이벤트가 발생하면, 메시지를 받아서 보내주기만 하면 되니까 app.js에서 이벤트 핸들러 함수를 추가해보도록 한다.

app.js

function handleMessageSubmit(event){
    event.preventDefault();
    const input = room.querySelector("input");
    const value = input.value;
    socket.emit("new_message", value, roomName, ()=>{
        addMessage(`You : ${value}`);
    });
    input.value = "";
}

function showroom(){
    welcome.hidden = true;
    room.hidden = false;
    const h3 = room.querySelector("h3");
    h3.innerText = `Room ${roomName}`;
    const form = room.querySelector("form");
    form.addEventListener("submit", handleMessageSubmit);
}

새로 발생시킬 이벤트 이름은 new_message이다.메시지 폼으로부터 제출 이벤트가 발생하면, 폼에서 적은 메시지를 처리하기 위해 showRoom 함수 내부에 이벤트 핸들러 함수 등록을 추가했다. showRoom은 사용자가 채팅룸에 접속하면 호출하는 함수이기 때문이다. 이 함수 내부에서 이벤트 등록을 해주는게 순서상 가장 자연스럽다.

이벤트 핸들러 함수 handleMessageSubmit에서는 입력된 값을 읽어들인 다음 new_message라는 이름의 이벤트를 발생시키는 작업을 수행한다. 이벤트가 발생할 때 서버에는 입력된 값, 채팅룸 이름과 함께 콜백 함수도 같이 전달하는데, 콜백 함수는 사용자 화면에 메시지를 추가하는 함수 addMessage를 호출하는 기능을 한다.

(2) 서버 코드

wsServer.on('connection',(socket)=>{
    //console.log(socket);
    socket.on('enter_room',(roomName, done)=>{
        done();
        //console.log(roomName);
        //console.log(socket.id);
        //console.log(socket.rooms);
        socket.join(roomName);
        socket.to(roomName).emit("welcome");
        socket.on('disconnecting', ()=>{
            socket.rooms.forEach(room => {
                socket.to(room).emit("bye");                
            });
        });
        socket.on("new_message", (msg, roomName, done) => {
            socket.to(roomName).emit("new_message", msg);
            done();
        });
        //console.log(socket.rooms);
        // setTimeout(()=>{
        //     done();
        // }, 5000);

    });
});

socket.on("new_message", (msg, roomName, done) => {
            socket.to(roomName).emit("new_message", msg);
            done();
        });

메시지 입력을 마친 사용자가 new_message 이벤트를 발생시키면 서버에서는 메시지, 채팅룸 이름, 콜백 함수를 받아서 이를 처리한다. 각각 msg, roomName, done 이라는 이름으로 받았다. 여기에는 다시 to 메서드를 사용해서 같은 채팅룸에 있는 소켓을 대상으로 new_message 이벤트를 발생시키고, 그런 다음 콜백 함수를 호출한다.

사용자에서도 new_message 이벤트를 발생시키고, 서버에서 같은 이름의 이벤트를 발생시키고 있다. 이는 사용자가 보낸 메시지를 서버가 받아서 같은 채팅룸에 있는 모든 사용자에게 보내 주기 위해 이벤트를 탁구공처럼 주고 받는 상황인데, 편의상 같은 이름을 사용했지만 다른 이름으로 작성하여도 상관없다. 예를 들어 사용자가 메시지를 보낼 때는 new_message, 서버에서 같은 채팅룸의 모든 사용자에게 다시 메시지를보낼 때는 send_message로 이벤트명을 정한다고 해도 동작할 때 전혀 문제가 되지 않는다. 

 

(3) 다시 프런트 코드

정말 중요한 작업이 남았다. 현재 채팅룸에 접속한 사용자가 메시지를 보내면, 서버가 그걸 받아서 채팅룸에 있는 모든 사용자에게 보내도록 해두었지만 정작 그 메시지가 화면에 표시되고 있지는 않다. 이제 사용자에게 전달된 메시지를 화면에 표시할 차례이다.

socket.on("new_message", (msg)=>{
    addMessage(msg);
});

addMessage 함수가 다시 사용되었다. 이제 한 사용자가 메시지를 보내면, 같은 채팅룸에 있는 모든 사용자의 화면에 메시지가 표시될 것이다. addMessage 사례에서 알 수 있듯이, 자주 사용하는 기능을 함수로 만들어 놓으면 코드를 작성할 때 수고를 덜 수 있어 생산성이 향상된다.

 

3. 닉네임 추가하기

(1) 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")

(2) app.js 수정

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}`);
    });
    input.value = "";
  }

  function handleNicknameSubmit(event) {
    event.preventDefault();
    const input = room.querySelector("#name input");
    const value = input.value;
    socket.emit("nickname", value);
    input.value = "";
  }
  
  function showroom(){
    welcome.hidden = true;
    room.hidden = false;
    const h3 = room.querySelector("h3");
    h3.innerText = `Room ${roomName}`;
    //const form = room.querySelector("form"); // room 요소의 하위에 있는 form 요소 선택
    //form.addEventListener("submit", handleMessageSubmit);
    const msgForm = room.querySelector("#msg"); // room 요소의 하위에 있는 form 요소 선택
    msgForm.addEventListener("submit", handleMessageSubmit);
    const nameForm = room.querySelector("#name"); // room 요소의 하위에 있는 form 요소 선택
    nameForm.addEventListener("submit", handleNicknameSubmit);
}

showRoom은 사용자가 채팅룸에 접속하면 동작하는 함수이므로, 메시지 폼과 닉네임 폼에 대한 이벤트 핸들러 함수를 모두 등록한다.

(3) server.js 수정

닉네임을 입력할 때 발생하는 nickname 이벤트를 서버에서 처리할 수 있게 해주어야 한다. 이벤트 핸들러 함수를 추가하고, 전달받은 value를 토대로 기능을 수행해본다.

wsServer.on('connection',(socket)=>{
    socket["nickname"] = "Anonymous익명의친구";
    //console.log(socket);
    socket.on('enter_room',(roomName, done)=>{
        done();
        //console.log(roomName);
        //console.log(socket.id);
        //console.log(socket.rooms);
        socket.join(roomName);
        //socket.to(roomName).emit("welcome");
        socket.to(roomName).emit("welcome", socket.nickname);
        socket.on('disconnecting', ()=>{
            socket.rooms.forEach(room => {
                socket.to(room).emit("bye", socket.nickname);                
            });
        });
        socket.on("new_message", (msg, roomName, done) => {
            socket.to(roomName).emit("new_message", `${socket.nickname}: ${msg}`);
            done();
        });
        socket.on("nickname", (nickName)=>socket["nickname"] = nickName);
        //console.log(socket.rooms);
        // setTimeout(()=>{
        //     done();
        // }, 5000);

    });
});

닉네임을 입력받기 전에는 모든 사용자가 Anon이라는 이름을 가지도록 했고, 그 상태에서 닉네임이 입력되어 nickname 이벤트가 발생하면 소켓의 nickname 속성에 값이 대입된다.

nickname 입력 처리가 추가됨에 따라 기존에 정의되어 있던 enter_room, dissonnecting, new_message 이벤트 핸들러에서 각각 코드를 추가한다. 입장, 퇴장, 메시지 생ㅇ성마다 누가 입력한 메시지인지 표시되도록 메시지에 닉네임을 함께 전달하도록 한 것이다.

(4) app.js 수정

위에서 닉네임이나 메시지를 입력해 이벤트가 발생하면 서버에서는 이를 처리하면서 그에 대응하는 이벤트를 발생시키도록 했다. 이제 서버에서 이벤트가 발생하면 다시 프런트엔드 쪽에서 이벤트를 받아 처리해야 한다. 그래야 사용자가 최종적으로 이벤트 처리 결과를 확인할 수 있기 때문이다.

socket.on("welcome", (userNickname)=>{
    addMessage(`${userNickname} joined!`);
});

socket.on("bye", (userNickname)=>{
    addMessage(`${userNickname} left ㅠㅠ`);
});

채팅룸 입장과 퇴장을 뜻하는 welcome과 bye 이벤트를 처리할 때 닉네임을 함께 표시하도록 각 이벤트 핸들러를 조금씩 수정한다.