Socket.IO로 웹소켓(websocket) 구현하기: 실시간 채팅 애플리케이션 만들기

이 글에서는 Socket.IO를 사용하여 웹소켓 기반의 실시간 채팅 애플리케이션을 구현하는 방법에 대해 알아봅니다. 본 내용은 초기 환경 설정부터 서버와 클라이언트 간의 실시간 통신 구현까지, 단계별로 진행됩니다.

1. 의존성 설정

첫 단계로, WEBSOCKET 폴더에서 시작하여 npm init을 통해 package.json 파일을 생성합니다. 이때, “name” 속성은 고유해야 하므로, socket.ioexpress와 같은 일반적인 이름은 사용할 수 없습니다. 이후, 필요한 의존성을 설치하기 위해 npm install express@4 명령을 실행합니다.

JavaScript
{
  "name": "socket-chat-example",
  "version": "0.0.1",
  "description": "my first socket.io app",
  "type": "commonjs",
  "dependencies": {}
}
  • package.json을 위와 같이 고친다.
    • “name” 속성은 고유해야 합니다. 따라서 name에 socket.io 또는 express 와 같은 값을 사용할 수 없습니다.
  • 이제 필요한 것들로 의존성 속성을 쉽게 채우기 위해 npm install을 사용하겠습니다:
    • npm install express@4

2. html 띄우기

Node.js와 Express 프레임워크를 이용해 간단한 웹 서버를 구축합니다.

  • index.js 를 생성하고 다음 코드를 작성합니다.
JavaScript
const express = require('express');
const { createServer } = require('node:http');

const app = express();
const server = createServer(app);

app.get('/', (req, res) => {
  res.send('<h1>Hello world</h1>');
});

server.listen(3000, () => {
  console.log('server running at http://localhost:3000');
});
  • const express = require(‘express’);: Node.js의 require 함수를 사용하여 Express 모듈을 불러옵니다. 이를 통해 Express 애플리케이션을 생성하고 관리할 수 있습니다.
  • const { createServer } = require(‘node:http’);: Node.js의 내장 http 모듈에서 createServer 함수를 구조 분해 할당 방식으로 불러옵니다. 이 함수는 HTTP 서버를 생성하는 데 사용됩니다.
  • const app = express();: Express 함수를 호출하여 애플리케이션 인스턴스를 생성합니다. 이 인스턴스(app)를 사용하여 라우팅 설정, 미들웨어 추가 등의 작업을 수행할 수 있습니다.
  • const server = createServer(app);: Express 애플리케이션 인스턴스를 createServer 함수의 인자로 전달하여 HTTP 서버를 생성합니다. 이렇게 생성된 서버(server)는 HTTP 요청을 받아 처리할 준비가 됩니다.
  • app.get(‘/’, (req, res) => { res.send(‘<h1>Hello world</h1>’); });: Express 애플리케이션의 라우팅 메서드 중 하나인 get을 사용합니다. 첫 번째 인자로는 경로(‘/’)를, 두 번째 인자로는 해당 경로로 요청이 왔을 때 실행될 콜백 함수를 받습니다. 이 함수는 요청 객체(req)와 응답 객체(res)를 인자로 받으며, 여기서는 응답 객체의 send 메서드를 사용하여 클라이언트에게 HTML 형식의 문자열(<h1>Hello world</h1>)을 전송합니다.
  • server.listen(3000, () => { console.log(‘server running at http://localhost:3000’); });: 생성된 서버가 3000번 포트에서 클라이언트의 요청을 기다리도록 설정합니다. 서버가 성공적으로 시작되면 콘솔에 ‘server running at http://localhost:3000’이라는 메시지를 출력합니다.

  • 이 코드는 3000번 포트에서 실행되는 기본 서버를 설정합니다. 클라이언트가 루트 URL(‘/’)에 접속하면 “Hello world” 메시지를 표시합니다.
터미널에서 node index.js 실행 결과
socket-html 띄우기-결과
port:3000 접속 결과

3. Serving HTML

3-1. index.js 편집

사용자 인터페이스를 위해 HTML 파일을 제공합니다. index.js 파일에 다음 코드를 추가하여 index.html 파일을 클라이언트에게 제공하도록 설정합니다.

JavaScript
const express = require('express');
const { createServer } = require('node:http');
const { join } = require('node:path');

const app = express();
const server = createServer(app);

app.get('/', (req, res) => {
  res.sendFile(join(__dirname, 'index.html'));
});

server.listen(3000, () => {
  console.log('server running at http://localhost:3000');
});
  • const { join } = require(‘node:path’);: Node.js의 내장 path 모듈에서 join 함수를 불러옵니다. path 모듈은 파일 및 디렉토리 경로를 작업하기 위한 유틸리티를 제공합니다. join 함수는 여러 인자로 받은 경로 조각들을 하나의 경로로 결합하고, OS별로 다를 수 있는 경로 구분자 문제를 자동으로 처리합니다.
  • res.sendFile(join(__dirname, ‘index.html’));: Express의 res.sendFile 메소드를 사용하여 클라이언트에게 파일을 전송합니다. 여기서 join(__dirname, ‘index.html’)은 현재 실행 중인 스크립트가 있는 디렉토리(__dirname)와 ‘index.html’ 파일명을 결합하여, 그 결과 생성된 절대 경로를 sendFile 메소드에 전달합니다. 이렇게 함으로써, 서버는 ‘index.html’ 파일을 찾아 그 내용을 클라이언트에게 전송할 수 있습니다. 이는 단순히 문자열을 보내는 대신, 실제 HTML 파일의 내용을 클라이언트에게 전달하고자 할 때 유용합니다.

3-2. index.html 생성

HTML
<!DOCTYPE html>
<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <title>Socket.IO chat</title>
    <style>
      body { margin: 0; padding-bottom: 3rem; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; }

      #form { background: rgba(0, 0, 0, 0.15); padding: 0.25rem; position: fixed; bottom: 0; left: 0; right: 0; display: flex; height: 3rem; box-sizing: border-box; backdrop-filter: blur(10px); }
      #input { border: none; padding: 0 1rem; flex-grow: 1; border-radius: 2rem; margin: 0.25rem; }
      #input:focus { outline: none; }
      #form > button { background: #333; border: none; padding: 0 1rem; margin: 0.25rem; border-radius: 3px; outline: none; color: #fff; }

      #messages { list-style-type: none; margin: 0; padding: 0; }
      #messages > li { padding: 0.5rem 1rem; }
      #messages > li:nth-child(odd) { background: #efefef; }
    </style>
  </head>
  <body>
    <ul id="messages"></ul>
    <form id="form" action="">
      <input id="input" autocomplete="off" /><button>Send</button>
    </form>
  </body>
</html>

socket-Serving HTML-결과
html 적용 결과
  • html 적용 시킨 결과 타이틀이 Socket.IO chat 인것을 볼 수 있다.

4. Socket.IO 통합

Socket.IO를 이용하여 서버와 클라이언트 간의 실시간 통신 기능을 추가합니다. 먼저, npm install socket.io 명령을 실행하여 Socket.IO를 설치합니다. 그 후, index.js 파일을 수정하여 Socket.IO 서버를 구성합니다.

4-1. socket.io 의존성 추가

Socket.IO는 두 부분으로 구성됩니다:

  • Node.JS HTTP 서버와 통합(또는 마운트)되는 서버(socket.io 패키지)
  • 브라우저 측에서 로드되는 클라이언트 라이브러리(socket.io-client 패키지)

개발 과정에서 socket.io가 자동으로 클라이언트를 제공하므로 지금은 하나의 모듈만 설치하면 됩니다

  • npm install socket.io 를 통해 socket.io 를 받는다.

그러면 모듈이 설치되고 package.json에 종속성이 추가됩니다. 이제 index.js를 편집하여 추가해 보겠습니다:

4-2. index.js 편집

JavaScript
const express = require('express');
const { createServer } = require('node:http');
const { join } = require('node:path');
const { Server } = require('socket.io');

const app = express();
const server = createServer(app);
const io = new Server(server);

app.get('/', (req, res) => {
  res.sendFile(join(__dirname, 'index.html'));
});

io.on('connection', (socket) => {
  console.log('a user connected');
  socket.on('disconnect', () => {
    console.log('user disconnected');
  });
});

server.listen(3000, () => {
  console.log('server running at http://localhost:3000');
});
  1. Socket.IO 모듈 추가: const { Server } = require(‘socket.io’); 이 코드는 socket.io 라이브러리의 Server 클래스를 불러옵니다. Socket.IO는 실시간, 양방향 통신을 위한 JavaScript 라이브러리로, 웹소켓과 같은 실시간 통신 기술을 추상화하여 제공합니다.
  2. Socket.IO 서버 인스턴스 생성: const io = new Server(server); 이 코드는 HTTP 서버 위에 Socket.IO 서버를 초기화합니다. 여기서 server는 Node.js의 createServer 함수를 통해 생성된 HTTP 서버 인스턴스입니다. 이렇게 함으로써, 웹 서버와 Socket.IO 서버가 같은 포트에서 실행될 수 있게 됩니다.
  3. 클라이언트 연결 이벤트 핸들링:
    io.on(‘connection’, (socket) => { console.log(‘a user connected’); });
    이 부분의 코드는 클라이언트가 서버에 연결될 때마다 실행됩니다. io.on('connection', callback) 메서드는 클라이언트가 연결되었을 때의 이벤트를 리스닝하고, 해당 이벤트가 발생할 때마다 콜백 함수를 실행합니다. 콜백 함수의 매개변수 socket은 연결된 개별 클라이언트를 나타내며, 이를 통해 서버는 클라이언트별로 데이터를 주고받거나 이벤트를 처리할 수 있습니다. 여기서는 단순히 콘솔에 ‘a user connected’라는 메시지를 출력하여, 사용자 연결을 확인합니다.

이 변경을 통해 기존의 단순한 웹 서버에서 실시간 웹 통신 기능을 가진 어플리케이션으로 업그레이드되었습니다. Socket.IO를 도입함으로써, 서버와 클라이언트 간에 실시간으로 데이터를 주고받을 수 있는 기반을 마련하였으며, 이는 채팅 애플리케이션과 같은 실시간 인터랙티브 애플리케이션을 구현하는 데 있어 필수적인 기능입니다.

4-3. index.html 편집

이제 index.html에서 </body> (끝 본문 태그) 앞에 다음 스니펫을 추가합니다:

HTML
</body>
<script src="/socket.io/socket.io.js"></script>
<script>
  const socket = io();
</script>
  • <script src=”/socket.io/socket.io.js”></script>: 서버로부터 Socket.IO 클라이언트 라이브러리를 로드합니다.
  • <script>const socket = io();</script>: Socket.IO 클라이언트 라이브러리를 사용하여 웹 페이지가 로드될 때 서버와의 웹소켓 연결을 초기화합니다. io() 함수는 서버로의 소켓 연결을 생성하고, 이 연결을 통해 서버와 실시간으로 데이터를 주고받을 수 있게 합니다.

  • 이것으로 io 글로벌(및 엔드포인트 GET /socket.io/socket.io.js)을 노출하는 socket.io-client를 로드하고 연결하기만 하면 됩니다.
  • 클라이언트 측 JS 파일의 로컬 버전을 사용하려면 node_modules/socket.io/client-dist/socket.io.js에서 찾을 수 있습니다.
  • 기본적으로 페이지를 제공하는 호스트에 연결을 시도하기 때문에 io()를 호출할 때 URL을 지정하지 않은 것을 알 수 있습니다.

이제 프로세스를 다시 시작하고(Control+C를 누른 다음 노드 index.js를 다시 실행하여) 웹 페이지를 새로 고치면 콘솔에 “사용자가 연결되었습니다.”라는 메시지가 표시됩니다.

socket-Socket.IO 통합-결과
websocket.io 연결

여러 탭을 열어보면 여러 메시지가 표시됩니다.

5. 실시간 이벤트 처리

Socket.IO의 기본 개념은 원하는 데이터와 함께 원하는 모든 이벤트를 주고받을 수 있다는 것입니다. JSON으로 인코딩할 수 있는 모든 객체를 사용할 수 있으며 바이너리 데이터도 지원됩니다.

클라이언트에서 서버로 메시지를 전송하고, 서버에서는 이 메시지를 받아 다른 클라이언트에게 전파하는 기능을 구현합니다. index.html에서 폼 제출 이벤트를 처리하여 서버로 메시지를 전송하도록 설정합니다. 서버 측에서는 index.js를 수정하여 클라이언트로부터 메시지를 받고, 이를 다른 클라이언트에게 전파하도록 합니다.

5-1. index.html 편집

클라이언트 측에서는 메시지를 전송하고 수신하는 기능을 구현합니다. index.html 파일에 다음과 같은 JavaScript 코드를 추가하여 메시지 전송 기능을 완성합니다.

HTML
<script src="/socket.io/socket.io.js"></script>
<script>
  const socket = io();

  const form = document.getElementById('form');
  const input = document.getElementById('input');

  form.addEventListener('submit', (e) => {
    e.preventDefault();
    if (input.value) {
      socket.emit('chat message', input.value);
      input.value = '';
    }
  });
</script>
  1. 폼과 입력 필드 요소 선택: document.getElementById 메서드를 사용하여 HTML 문서 내의 폼(form)과 텍스트 입력 필드(input) 요소를 선택합니다.
  2. 폼 제출 이벤트 리스너 추가: form.addEventListener(‘submit’, (e) => {…}); 코드는 폼이 제출될 때마다 실행될 콜백 함수를 등록합니다. 사용자가 메시지를 입력하고 ‘Send’ 버튼을 클릭하거나 엔터 키를 누르면 이 이벤트가 발생합니다.
  3. 폼 제출 방지 및 메시지 전송: 이벤트 리스너 내부에서는 기본 폼 제출 동작을 방지하기 위해 e.preventDefault();를 호출합니다. 그런 다음, 입력 필드에 값이 있는지 확인합니다. 값이 있으면, socket.emit(‘chat message’, input.value); 코드를 통해 ‘chat message’ 이벤트와 함께 입력된 메시지 값을 서버로 전송합니다. 메시지 전송 후, 입력 필드는 다시 빈 문자열로 초기화되어 새 메시지 입력을 위해 준비됩니다.

5-2. index.js 수정

JavaScript
io.on('connection', (socket) => {
  socket.on('chat message', (msg) => {
    io.emit('chat message', msg);
  });
});

이 코드는 클라이언트로부터 ‘chat message’ 이벤트를 수신할 때마다, 그 메시지를 모든 클라이언트에게 방송합니다.

web 에서 console로 데이터 보내기
console에서 web에서 온 데이터 확인

6. 방송하기

이제 서버는 클라이언트로부터 메시지를 받아 다른 모든 클라이언트에게 방송할 수 있습니다. 이를 통해 실시간으로 상호 작용하는 채팅 애플리케이션을 구현할 수 있습니다. 서버와 클라이언트 사이의 실시간 통신을 가능하게 하는 Socket.IO의 강력한 기능 덕분에, 개발자는 복잡한 네트워크 프로토콜 없이도 쉽게 실시간 웹 애플리케이션을 만들 수 있습니다.

  • 모든 연결된 소켓에 이벤트 방송하기: io.emit() 메서드를 사용하면, 서버와 연결된 모든 클라이언트에게 이벤트를 전송할 수 있습니다. 예를 들어, io.emit(‘hello’, ‘world’); 코드는 모든 클라이언트에게 ‘hello’ 이벤트를 ‘world’ 데이터와 함께 전송합니다.
  • 특정 소켓을 제외하고 이벤트 방송하기: 어떤 경우에는 이벤트를 발생시킨 특정 소켓을 제외한 모든 클라이언트에게 메시지를 보내고 싶을 수 있습니다. 이럴 때는 socket.broadcast.emit() 메서드를 사용합니다. 예를 들어, socket.broadcast.emit(‘hi’); 코드는 이벤트를 발생시킨 소켓을 제외한 모든 클라이언트에게 ‘hi’ 이벤트를 전송합니다.

6-1. index.html 편집하기

HTML
<script src="/socket.io/socket.io.js"></script>
<script>
  const socket = io();

  const form = document.getElementById('form');
  const input = document.getElementById('input');
  const messages = document.getElementById('messages');

  form.addEventListener('submit', (e) => {
    e.preventDefault();
    if (input.value) {
      socket.emit('chat message', input.value);
      input.value = '';
    }
  });

  socket.on('chat message', (msg) => {
    const item = document.createElement('li');
    item.textContent = msg;
    messages.appendChild(item);
    window.scrollTo(0, document.body.scrollHeight);
  });
</script>
  1. 이벤트 리스닝: socket.on(‘chat message’, (msg) => {…}); 코드는 서버로부터 ‘chat message’ 이벤트를 수신 대기합니다. 즉, 서버가 이 이벤트를 클라이언트로 전송할 때마다 콜백 함수가 호출되어, 전송된 메시지(msg)를 처리합니다.
  2. 메시지를 담을 새로운 요소 생성: const item = document.createElement(‘li’); 코드는 새로운 요소를 생성합니다. 이 요소는 수신된 채팅 메시지를 담기 위한 용도로 사용됩니다.
  3. 수신된 메시지로 요소의 내용 설정: item.textContent = msg; 코드는 생성된 요소의 텍스트 내용으로 서버로부터 수신된 메시지(msg)를 설정합니다. 이로써, 메시지가 사용자에게 보여질 수 있게 됩니다.
  4. 메시지 목록에 새 메시지 추가: messages.appendChild(item); 코드는 앞서 생성하고 메시지로 채운 요소를 메시지를 표시하는 부분의 목록(messages)에 추가합니다. 여기서 messages는 HTML 문서 내에 미리 정의된 <ul> 요소에 해당하며, 사용자에게 채팅 메시지를 시각적으로 표시하는 역할을 합니다. 이 과정을 통해, 채팅 애플리케이션에서 실시간으로 메시지가 업데이트되고 사용자 인터페이스에 반영됩니다.

채팅 메시지를 받아서 화면에 표시한 결과

댓글 달기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다

Scroll to Top