본문 바로가기
Develop/Zoom만들기

Zoom만들기_ 6. 실시간 채팅 구현

by 보보트레인 2023. 8. 30.

1. 웹 요소 추가하기

 

브라우저 화면에 입력 필드와 목록을 만들고, 메시지를 주고받은 결과를 바로바로 표시할 수 있게 해보자.

※ 당연히 서버는 실행되어져 있어야함.

 

1-1) home.pug (뷰 엔진) 수정

 

코드의 ul부분은 메시지를 받을 때마다 자바스크립트를 이용해서 채울 예정 ( like 카카오톡 )

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 Zoom
        //MVP.css는 우리가 태그에 class나 id같은 특성을 추가하지 않아도 자동으로 스타일을 적용해주는 라이브러리다.
        link(rel="stylesheet", href="https://unpkg.com/mvp.css")
    body
        //h1 It works!
        header
            h1 Zoom
        main
            ul
            form
                input(type="text", placeholder="write a msg", required)
                button Send
        script(src="/public/js/app.js")


 

<결과화면>

1-2). form 이벤트 등록하기

 

form에 텍스트를 입력하고 그것을 전송하면 submit 이벤트가 발생하는데, 그때 어떤 일이 일어나길 바라는지 정의하는 것.

→  app.js에 다음의 코드 추가 + 5초 지연메시지는 지우기

 

const messageList = document.querySelector("ul");
const messageForm = document.querySelector("form");
function handleSubmit(event){
    event.preventDefault();
    const input = messageForm.querySelector("input");
    socket.send(input.value);
    input.value = "";
}

messageForm.addEventListener("submit", handleSubmit);

<전체코드>

const messageList = document.querySelector("ul");
const messageForm = document.querySelector("form");
const socket = new WebSocket(`ws://${window.location.host}`);

//연결되었을 때
socket.addEventListener("open", () => {
    console.log("Connected to Server");
})

//메시지가 전달되었을 때
socket.addEventListener("message", (message) => {
    console.log("Just got this:", message.data, "from the server");
})

//서버가 오프라인 되었을 때
socket.addEventListener("close", () => {
    console.log("Disconnected from Server");
})

function handleSubmit(event){
    event.preventDefault();
    const input = messageForm.querySelector("input");
    socket.send(input.value);
    input.value = "";
}

messageForm.addEventListener("submit", handleSubmit);

설명 : querySelector 메서드를 이용하여 목록(messageList)과 입력 폼(messageForm)을 선택.

handleSubmit이라는 함수를 만들어 입력 폼에서 submit 이벤트가 발생했을 때 동작하도록 설정.

input박스의 값을 받아서 input변수에 담고 soket으로 보냄 / input박스의 값 초기화.

 

<결과화면>

send 클릭 시

 

값이 초기화 되고

터미널에 메시지 값이 찍힌다.

why?) 우리가 server.js에서 메시지가 보내졌을 때, 콘솔(터미널)에 그 내용을 출력하도록 코드를 추가해뒀기 때문.

server.js

1-3) 메시지 돌려받기

 

서버에서 메시지를 받으면 브라우저에서는 확인할 길이 없음 ( 서버의 콘솔(터미널)을 통해서만 확인 가능한 상황 ㅠ)

따라서 서버에서 메시지를 받으면, 같은 메시지를 도로 브라우저로 전송해주는 기능 추가해보자.

 

server.js를 다음과 같이 수정하자.

wss.on("connection", (socket) => {
   // console.log(socket);

   // 브라우저가 연결됐을 때.
   console.log("Connected to Browser");
   //브라우저가 종료되었을 때를 의미하는 close
   socket.on("close", () => console.log("Disconnected from Browser"));

   //브라우저에서 서버로 메시지가 보내졌을때, 콘솔(터미널)에 그 내용을 출력
   socket.on("message", (message) => {
    //console.log(`${message}`)
    socket.send(`${message}`);
   });

   //브라우저로 메시지 전송 테스트
   //socket.send("hello!!");
})

console.log를 socket.send로 정정했음

 

<결과 화면>

그 결과  브라우저의 콘솔을 확인해보면

서버가 받은 메시지도 브라우저 콘솔창에서 확인할 수 있게 되었다!


2. 사용자 간 채팅하기

 

2-1) 서로 다른 탭에서 메시지 보내기 

 

사실 채팅앱은 2명의 다른 사용자가 메시지를 주고받아야 비로소 의미가 있는 것임을 우리는 알고있다.

따라서 브라우저 탭을 2개 열고 같은 주소에 접속하고 , 각각 test1/test2메시지를 전송해보자.

 

이처럼 각각 독립적으로 서버가 구동되어짐을 확인할 수 있다.

 

여기서 핵심은 '독립적'으로 각각의 서버가 돌아가면 안되고

'상호적'으로 메시지를 주고받아야 한다. 

→ 서로 다른 사용자 간의 메시지를 교환할 수 있게 만들려면, 우선 누가 연결되었는지를 알아야함.

 

결론 :  임시방편으로 가짜 데이터베이스를 만들어 이를 활용해볼 예정

→ server.js을 다음과 같이 수정

const sockets = [];
wss.on("connection", (socket) => {
   socket.push(socket);

   // 브라우저가 연결됐을 때.
   console.log("Connected to Browser");
   //브라우저가 종료되었을 때를 의미하는 close
   socket.on("close", () => console.log("Disconnected from Browser"));

   //브라우저에서 서버로 메시지가 보내졌을때, 콘솔(터미널)에 그 내용을 출력
   socket.on("message", (message) => {
    //console.log(`${message}`)
    socket.send(`${message}`);
   });

   //브라우저로 메시지 전송 테스트
   //socket.send("hello!!");
})

설명 : socket이라는 배열을 만들고,

웹소켓 서버에 connection이라는 이벤트가 발생할 때 마다, 이 배열에 생성된 소켓이 push를 통해 추가될 예정.

→ 사용자별로 socket을 sokets라는 배열에 담아 관리한다는 뜻. ( 그럼 각각의 socket별로 데이터 주고받기 가능 )

 

다음의 코드 추가

//브라우저에서 서버로 메시지가 보내졌을때, 콘솔(터미널)에 그 내용을 출력
   socket.on("message", (message) => {
        sockets.forEach(aSocket => aSocket.send(`${message}`));
   });

forEach는 반복문. 따라서 message이벤트가 발생할 때 마다 sockets에 들어있는 모든 socket에 접근하여 메시지 보냄

결론 : 한 사용자가 보낸 메시지를 서버에 접속한 다른 모든 사용자가 받을 수 있다는 뜻.

 

<결과 화면>

웹브라우저 1로 test1을 전송해도 웹브라우저 2에서 test1을 확인할 수 있음

두 번째 웹브라우저에 test1이 잘 출력됨을 확인할 수 있다.

 

물론 첫 번째 웹브라우저에서도 test1이 잘 출력됨.

2-2) app.js 수정하기

 

언제까지나 콘솔창으로 메시지를 확인할 수는 없다!  → 메시지를 화면에 보여주도록 프론트 수정 필요!

 

메시지가 전송될 때 마다, li요소를 만들어주고 그 안에 메시지를 적은 다음, li를 ul 안으로 넣어줄 예정.

다음과 같이 app.js수정

//서버로부터 메시지를 받았을 때 때
socket.addEventListener("message", (message) => {
    const li = document.createElement("li");
    li.innerText = message.data;
    messageList.append(li);
})

설명 : 이미 사용한 message 이벤트 처리함수를 수정하여 li요소 생성 후, 거기에 message를 담음.

메시지를 담은 li를 messageList에 append로 붙히는데 이 messageList는 우리가 이전에 ul을 탐색하는 변수로 설정해뒀음

const messageList = document.querySelector("ul");

따라서 결국 ul에 li가 담기게 되는 것임.

 

<결과 화면>

ㅎㅇ를 텍스트박스에 적어 send 누르면 li형식으로 ㅎㅇ가 화면에 잘 출력됨을 확인할 수 있음. 

 

※ 다만 브라우저 1의 채팅내용은 브라우저 2의 화면에도 출력됨....

우리는 브라우저 - 서버 - 모든 브라우저로 데이터를 뿌리는 flow를 따르고 있음. 


3. 닉네임 추가하기

 

각각의 사용자별로 구별수단(key값)이 필요. → 닉네임 부여해보자

 

3-1) home.pug의 코드에 닉네임 폼을 추가하자. 

<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 Zoom
        //MVP.css는 우리가 태그에 class나 id같은 특성을 추가하지 않아도 자동으로 스타일을 적용해주는 라이브러리다.
        link(rel="stylesheet", href="https://unpkg.com/mvp.css")
    body
        //h1 It works!
        header
            h1 Zoom
        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")


3-2) app.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}`);

//연결되었을 때
socket.addEventListener("open", () => {
    console.log("Connected to Server");
})

//서버로부터 메시지를 받았을 때 때
socket.addEventListener("message", (message) => {
    const li = document.createElement("li");
    li.innerText = message.data;
    messageList.append(li);
})

//서버가 오프라인 되었을 때
socket.addEventListener("close", () => {
    console.log("Disconnected from Server");
})
//메시지 이벤트 처리
function handleSubmit(event){
    event.preventDefault();
    const input = messageForm.querySelector("input");
    socket.send(input.value);
    input.value = "";
}
//닉네임 이벤트처리
function handleNickSubmit(event){
    event.preventDefault();
    const input = nickForm.querySelector("input");
    socket.send(input.value);
    input.value = "";
}

//서버로 메시지 보내기
messageForm.addEventListener("submit", handleSubmit);

설명 : 기존 메시지 폼에서 메시지가 입력되면 submit 이벤트가 발생해 handleSubmit을 호출하는 것처럼,

닉네임 폼에서 닉네임이 입력되면 submit 이벤트가 발생해 handleNickSubmit을 호출하도록 했음.

 

<결과화면>


4. 닉네임과 메시지 구별하기

닉네임과 메시지는 엄연히 다른 정보라 구별을 반드시 해줘야함!

→ 입력값을 뚜렷하게 구분하기 위해서 JSON데이터를 만들어서 거기에 입력값을 포함시키고 추가 정보를 더 넣어줄 예정

 

4-1) app.js 코드 수정 ( JSON처리 )

//닉네임 이벤트처리
function handleNickSubmit(event){
    event.preventDefault();
    const input = nickForm.querySelector("input");
    socket.send({
        type: "nickname",
        payload: input.value
    });
    input.value = "";
}

설명 : JSON에 키를 2개 추가해서 전송했음

이 데이터가 어떤 유형인지 나타내는 'type'과 실제 값을 의미하는 'payload' 2가지로 구성됨.

 

4-2) JSON 문자열로 변환하기

 

JSON은 객체형태로 전송되니 이를 문자열로 변환해야 우리가 원하는 데이터작업이 가능함.

  • JSON을 문자열로 변환할 때는 JSON.stringify메서드를 사용
  • 문자열을 JSON으로  변환할 때는 JSON.parse 메서드를 사용

4-2-1) app.js에서 JSON을 문자열로 변환하기

<app.js 변경 코드>

//JSON을 문자열로 변환하기
function makeMessage(type, payload){
    const msg = { type, payload};
    return JSON.stringify(msg);
}

//메시지 이벤트 처리
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 = "";
}

설명 : makeMessage 함수는 입력값 유형(type)과 입력값(payload)을 인자로 전달받아서 그것들로 이루어진 JSON을 만든 뒤 문자열 형태로 변환하는 역할을 함.

<전체 코드>

const messageList = document.querySelector("ul");
const nickForm = document.querySelector("#nick");
const messageForm = document.querySelector("#message");
const socket = new WebSocket(`ws://${window.location.host}`);

//JSON을 문자열로 변환하기
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;
    messageList.append(li);
})

//서버가 오프라인 되었을 때
socket.addEventListener("close", () => {
    console.log("Disconnected from Server");
})
//메시지 이벤트 처리
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);

 

<테스트 및 결과 화면>

이렇게 닉네임과 메시지를 각각 save/send하면
다음과 같이 타입이 구분되어 JSON형식으로 전송된다.

4-2-2) app.js에서 문자열을 JSON으로 변환하기

 

server.js 코드를 다음과 같이 수정

//브라우저에서 서버로 메시지가 보내졌을때, 콘솔(터미널)에 그 내용을 출력
   socket.on("message", (msg) => {
        const message = JSON.parse(msg);
        console.log(message.type, message.payload);
        sockets.forEach(aSocket => aSocket.send(`${message}`));
   });

설명 : 데이터를 JSON으로 잘 전달받아 JSON.parse를 이용해 콘솔(터미널)에 잘 출력하고,

다시 전체 socket에 뿌려주고 있음 ( 문자열 → JSON )

 

<결과화면>

이렇게 각각의 값을 입력 시
콘솔에 잘 입력됨.

다만 json으로 변환한 상태에서 다시 브라우저로 그 값을 쏴주는데, 객체상태로 넘어가므로

json형태로 넘어옴... 다시 문자열 변환 과정 필요..

 서버 사이드에서 브라우저 사이드로 재전송시에는 문자열로 변환하는 과정이 반드시 필요할것 이해하기!

 

반응형