프로그래밍 언어/NODE JS

실시간 GIF 채팅방 만들기(2)

· 코딩마이데이

채팅 웹 메인 화면과 채팅방 등록 화면을 만들어보겠습니다. 채팅뿐만 아니라 채팅방도 실시간으로 추가되거나 제거됩니다.

화면의 레이아웃을 담당하는 layout.html 파일을 작성하고 views/error.html을 수정합니다.

 

views/layout.html

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>{{title}}</title>
    <link rel="stylesheet" href="/main.css" />
  </head>
  <body>
    {% block content %}
    {% endblock %}
    {% blockscript %}
    {% endblock %}
  </body>
</html>

 

views/error.html

{% extends 'layout.html' %}

{% block content %}
    <h1>{{message}}</h1>
    <h2>{{error.status}}</h2>
    <pre>{{error.stack}}</pre>
{% endblock %}

 

main.css를 추가하여 간단히 디자인합니다.

 

public/main.css

* {
  box-sizing: border-box;
}
.mine {
  text-align: right;
}
.system {
  text-align: center;
}
.mine img,
.other img {
  max-width: 300px;
  display: inline-block;
  border: 1px solid silver;
  border-radius: 5px;
  padding: 2px 5px;
}
.mine div:first-child,
.other div:first-child {
  font-size: 12px;
}
.mine div:last-child,
.other div:last-child {
  display: inline-block;
  border: 1px solid silver;
  border-radius: 5px;
  padding: 2px 5px;
  max-width: 300px;
}
#exit-btn {
  position: absolute;
  top: 20px;
  right: 20px;
}
#chat-list {
  height: 500px;
  overflow: auto;
  padding: 5px;
}
#chat-form {
  text-align: right;
}
label[for="gif"],
#chat,
#chat-form [type="submit"] {
  display: inline-block;
  height: 30px;
  vertical-align: top;
}
label[for="gif"] {
  cursor: pointer;
  padding: 5px;
}
#gif {
  display: none;
}
table,
table th,
table td {
  text-align: center;
  border: 1px solid silver;
  border-collapse: collapse;
}

 

이제 메인 화면을 담당하는 main.html 파일을 작성합니다.

 

views/main.html

{% extends 'layout.html' %} {% block content %}
<h1>GIF 채팅방</h1>
<fieldset>
  <legend>채팅방 목록</legend>
  <table>
    <thead>
      <tr>
        <th>방 제목</th>
        <th>종류</th>
        <th>허용 인원</th>
        <th>방장</th>
      </tr>
    </thead>
    <tbody>
      {% for room in rooms %}
      <tr data-id="{{room._id}}">
        <td>{{room.title}}</td>
        <td>{{'비밀방' if room.password else '공개방'}}</td>
        <td>{{room.max}}</td>
        <td style="color: {{room.owner}}">{{room.owner}}</td>
        <td>
          <button
            data-password="{{'true' if room.password else 'false'}}"
            data-id="{{room._id}}"
            class="join-btn"
          >
            입장
          </button>
        </td>
      </tr>
      {% endfor %}
    </tbody>
  </table>
  <div class="error-message">{{error}}</div>
  <a href="/room">채팅방 생성</a>
</fieldset>
<script src="/socket.io/socket.io.js"></script>
<script>
  const socket = io.connect("http://localhost:8005/room", {
    // 네임스페이스
    path: "/socket.io",
  });

  socket.on("newRoom", function (data) {
    // 새 방 이벤트 시 새 방 생성
    const tr = document.createElement("tr");
    let td = document.createElement("td");
    td.textContent = data.title;
    tr.appendChild(td);
    td = document.createElement("td");
    td.textContent = data.password ? "비밀방" : "공개방";
    tr.appendChild(td);
    td = document.createElement("td");
    td.textContent = data.max;
    tr.appendChild(td);
    td = document.createElement("td");
    td.style.color = data.owner;
    td.textContent = data.owner;
    tr.appendChild(td);
    td = document.createElement("td");
    const button = document.createElement("button");
    button.textContent = "입장";
    button.dataset.password = data.password ? "true" : "false";
    button.dataset.id = data._id;
    button.addEventListener("click", addBtnEvent);
    td.appendChild(button);
    tr.appendChild(td);
    tr.dataset.id = data._id;
    document.querySelector("table tbody").appendChild(tr); // 화면에 추가
  });

  socket.on("removeRoom", function (data) {
    // 방 제거 이벤트 시 id가 일치하는 방 제거
    document.querySelectorAll("tbody tr").forEach(function (tr) {
      if (tr.dataset.id === data) {
        tr.parentNode.removeChild(tr);
      }
    });
  });

  function addBtnEvent(e) {
    // 방 입장 클릭 시
    if (e.target.dataset.password === "true") {
      const password = prompt("비밀번호를 입력하세요");
      location.href = "/room/" + e.target.dataset.id + "?password=" + password;
    } else {
      location.href = "/room/" + e.target.dataset.id;
    }
  }

  document.querySelectorAll(".join-btn").forEach(function (btn) {
    btn.addEventListener("click", addBtnEvent);
  });
</script>
{% endblock %} {% block script %}
<script>
  window.onload = () => {
    if (new URL(location.href).searchParams.get("error")) {
      alert(new URL(location.href).searchParams.get("error"));
    }
  };
</script>
{% endblock %}

 

io.connect 메서드의 주소가 달라졌다는 점에 주목해주세요. 주소 뒤에 /room이 붙었습니다. 이것을 네임스페이스라고 부르며, 서버에서 /room 네임스페이스를 통해 보낸 데이터만 받을 수 있습니다. 네임스페이스를 여러 개 구분해 주고받을 데이터를 분류할 수 있습니다.

socket에는 미리 newRoom과 removeRoom 이벤트를 달아두었습니다. 서버에서 웹 소켓으로 해당 이벤트를 발생시키면 이벤트 리스너의 콜백 함수가 실행됩니다. 콜백 함수의 내용이 길지만 특별한 것은 없습니다. 각각 테이블에 새로운 방 목록을 추가하거나 제거하는 코드입니다. 입장 버튼을 누르면, 비밀방일 경우 비밀번호를 받고 공개방일 경우 바로 입장시킵니다.