이 글에서는 Socket.IO를 사용하여 웹소켓 기반의 실시간 채팅 애플리케이션을 구현하는 방법에 대해 알아봅니다. 본 내용은 초기 환경 설정부터 서버와 클라이언트 간의 실시간 통신 구현까지, 단계별로 진행됩니다.
1. 의존성 설정
첫 단계로, WEBSOCKET 폴더에서 시작하여 npm init을 통해 package.json 파일을 생성합니다. 이때, “name” 속성은 고유해야 하므로, socket.io나 express와 같은 일반적인 이름은 사용할 수 없습니다. 이후, 필요한 의존성을 설치하기 위해 npm install express@4 명령을 실행합니다.
{
"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 를 생성하고 다음 코드를 작성합니다.
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” 메시지를 표시합니다.
3. Serving HTML
3-1. index.js 편집
사용자 인터페이스를 위해 HTML 파일을 제공합니다. index.js 파일에 다음 코드를 추가하여 index.html 파일을 클라이언트에게 제공하도록 설정합니다.
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 생성
<!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>
- 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 편집
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');
});
- Socket.IO 모듈 추가: const { Server } = require(‘socket.io’); 이 코드는
socket.io
라이브러리의Server
클래스를 불러옵니다.Socket.IO
는 실시간, 양방향 통신을 위한 JavaScript 라이브러리로, 웹소켓과 같은 실시간 통신 기술을 추상화하여 제공합니다. - Socket.IO 서버 인스턴스 생성: const io = new Server(server); 이 코드는 HTTP 서버 위에
Socket.IO
서버를 초기화합니다. 여기서server
는 Node.js의createServer
함수를 통해 생성된 HTTP 서버 인스턴스입니다. 이렇게 함으로써, 웹 서버와Socket.IO
서버가 같은 포트에서 실행될 수 있게 됩니다. - 클라이언트 연결 이벤트 핸들링:
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> (끝 본문 태그) 앞에 다음 스니펫을 추가합니다:
</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를 다시 실행하여) 웹 페이지를 새로 고치면 콘솔에 “사용자가 연결되었습니다.”라는 메시지가 표시됩니다.
여러 탭을 열어보면 여러 메시지가 표시됩니다.
5. 실시간 이벤트 처리
Socket.IO의 기본 개념은 원하는 데이터와 함께 원하는 모든 이벤트를 주고받을 수 있다는 것입니다. JSON으로 인코딩할 수 있는 모든 객체를 사용할 수 있으며 바이너리 데이터도 지원됩니다.
클라이언트에서 서버로 메시지를 전송하고, 서버에서는 이 메시지를 받아 다른 클라이언트에게 전파하는 기능을 구현합니다. index.html에서 폼 제출 이벤트를 처리하여 서버로 메시지를 전송하도록 설정합니다. 서버 측에서는 index.js를 수정하여 클라이언트로부터 메시지를 받고, 이를 다른 클라이언트에게 전파하도록 합니다.
5-1. index.html 편집
클라이언트 측에서는 메시지를 전송하고 수신하는 기능을 구현합니다. index.html 파일에 다음과 같은 JavaScript 코드를 추가하여 메시지 전송 기능을 완성합니다.
<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>
- 폼과 입력 필드 요소 선택: document.getElementById 메서드를 사용하여 HTML 문서 내의 폼(
form
)과 텍스트 입력 필드(input
) 요소를 선택합니다. - 폼 제출 이벤트 리스너 추가: form.addEventListener(‘submit’, (e) => {…}); 코드는 폼이 제출될 때마다 실행될 콜백 함수를 등록합니다. 사용자가 메시지를 입력하고 ‘Send’ 버튼을 클릭하거나 엔터 키를 누르면 이 이벤트가 발생합니다.
- 폼 제출 방지 및 메시지 전송: 이벤트 리스너 내부에서는 기본 폼 제출 동작을 방지하기 위해 e.preventDefault();를 호출합니다. 그런 다음, 입력 필드에 값이 있는지 확인합니다. 값이 있으면, socket.emit(‘chat message’, input.value); 코드를 통해 ‘chat message’ 이벤트와 함께 입력된 메시지 값을 서버로 전송합니다. 메시지 전송 후, 입력 필드는 다시 빈 문자열로 초기화되어 새 메시지 입력을 위해 준비됩니다.
5-2. index.js 수정
io.on('connection', (socket) => {
socket.on('chat message', (msg) => {
io.emit('chat message', msg);
});
});
이 코드는 클라이언트로부터 ‘chat message’ 이벤트를 수신할 때마다, 그 메시지를 모든 클라이언트에게 방송합니다.
6. 방송하기
이제 서버는 클라이언트로부터 메시지를 받아 다른 모든 클라이언트에게 방송할 수 있습니다. 이를 통해 실시간으로 상호 작용하는 채팅 애플리케이션을 구현할 수 있습니다. 서버와 클라이언트 사이의 실시간 통신을 가능하게 하는 Socket.IO의 강력한 기능 덕분에, 개발자는 복잡한 네트워크 프로토콜 없이도 쉽게 실시간 웹 애플리케이션을 만들 수 있습니다.
- 모든 연결된 소켓에 이벤트 방송하기: io.emit() 메서드를 사용하면, 서버와 연결된 모든 클라이언트에게 이벤트를 전송할 수 있습니다. 예를 들어, io.emit(‘hello’, ‘world’); 코드는 모든 클라이언트에게 ‘hello’ 이벤트를 ‘world’ 데이터와 함께 전송합니다.
- 특정 소켓을 제외하고 이벤트 방송하기: 어떤 경우에는 이벤트를 발생시킨 특정 소켓을 제외한 모든 클라이언트에게 메시지를 보내고 싶을 수 있습니다. 이럴 때는 socket.broadcast.emit() 메서드를 사용합니다. 예를 들어, socket.broadcast.emit(‘hi’); 코드는 이벤트를 발생시킨 소켓을 제외한 모든 클라이언트에게 ‘hi’ 이벤트를 전송합니다.
6-1. index.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>
- 이벤트 리스닝: socket.on(‘chat message’, (msg) => {…}); 코드는 서버로부터 ‘chat message’ 이벤트를 수신 대기합니다. 즉, 서버가 이 이벤트를 클라이언트로 전송할 때마다 콜백 함수가 호출되어, 전송된 메시지(msg)를 처리합니다.
- 메시지를 담을 새로운 요소 생성: const item = document.createElement(‘li’); 코드는 새로운 요소를 생성합니다. 이 요소는 수신된 채팅 메시지를 담기 위한 용도로 사용됩니다.
- 수신된 메시지로 요소의 내용 설정: item.textContent = msg; 코드는 생성된 요소의 텍스트 내용으로 서버로부터 수신된 메시지(msg)를 설정합니다. 이로써, 메시지가 사용자에게 보여질 수 있게 됩니다.
- 메시지 목록에 새 메시지 추가: messages.appendChild(item); 코드는 앞서 생성하고 메시지로 채운 요소를 메시지를 표시하는 부분의 목록(messages)에 추가합니다. 여기서 messages는 HTML 문서 내에 미리 정의된 <ul> 요소에 해당하며, 사용자에게 채팅 메시지를 시각적으로 표시하는 역할을 합니다. 이 과정을 통해, 채팅 애플리케이션에서 실시간으로 메시지가 업데이트되고 사용자 인터페이스에 반영됩니다.