WebSocket과 Socket.IO
Source: Dev.to
이 게시물에는 깜박이는 GIF가 포함되어 있습니다.
HTTP 요청만으로도 많이 해냈지만, 이제 한계에 부딪히고 있습니다. 서버가 자정에 업데이트되었음을 클라이언트에게 어떻게 알려서 최신 데이터를 가져오게 할 수 있을까요? 한 사용자가 게시물을 올렸을 때 다른 사용자에게 어떻게 알릴 수 있을까요? 요컨대, 클라이언트가 요청을 시작하지 않아도 정보를 어떻게 전달할 수 있을까요?
웹소켓
가능한 해결책 중 하나는 WebSockets를 사용하는 것입니다. WebSocket은 클라이언트와 서버 사이에 지속적인 연결을 설정하여, 클라이언트의 다음 요청을 기다리지 않고도 원하는 시점에 데이터를 클라이언트에게 보낼 수 있게 해줍니다. WebSocket은 자체 프로토콜을 가지고 있지만(연결은 HTTP 요청으로 시작됩니다) 언어에 구애받지 않습니다.
원한다면 WebSocket 클라이언트와 해당 서버를 직접 구현하거나 Deno와 함께 구현할 수도 있고, 이미 어려운 작업을 해놓은 라이브러리를 사용할 수도 있습니다. 이전 프로젝트에서 Socket.IO를 사용해 본 경험이 있기 때문에 이번에도 그것을 사용하겠습니다. 이전에 작업하면서 즐거웠고, WebSocket이 실패할 경우에 대비한 폴백(fallback) 기능도 제공한다는 장점이 있습니다.
컬러소켓
즉각적인 시각 피드백을 위해, 하나의 클라이언트가 모든 클라이언트에 표시되는 색상을 바꿀 수 있는 작은 데모를 만들겠습니다. /color 엔드포인트에 연결된 각 클라이언트는 하나의 기본 색상을 제어하는 슬라이더와, 다른 모든 /color 클라이언트의 색상을 반전시키는 버튼을 가지고 있습니다.
- 클라이언트가 연결될 때 서버가 순서대로 색상을 할당하므로, 세 가지 색상을 모두 얻을 때까지 몇 번 새로 고침하면 됩니다.
- 중복된 색상도 동기화됩니다.
/admin사용자는 기본 색상을 켜거나 끌 수 있습니다.
앱이 실제로 동작하는 모습은 다음과 같습니다:
클라이언트들이 서버에 지속적으로 요청을 보내는 것은 아닙니다. 어떻게 업데이트를 알게 될까요?
Source:
연결 설정
각 클라이언트가 io() 호출을 실행하면 새로운 소켓이 생성되고, 서버와의 연결이 열립니다.
// color.html
const socket = io('/color'); // 인자는 나중에 다시 설명합니다
스크립트는 이후 새 소켓에 대해 서버로부터 받을 것으로 기대하는 다양한 이벤트에 대한 핸들러를 할당합니다:
// color.html
socket.on('assign-color', (color, colorSettings, activeSettings) => {
document.getElementById('color-name').innerText = color;
controllingColor = color;
currentBackground = colorSettings;
active = activeSettings;
colorSlider.disabled = !active[controllingColor];
document.getElementById('active').innerText = active[controllingColor] ? 'active' : 'inactive';
colorSlider.value = colorSettings[controllingColor];
updateBackground();
});
socket.on('set-color', (color, value) => {
currentBackground[color] = value;
if (controllingColor === color) {
colorSlider.value = value;
}
updateBackground();
});
socket.on('invert', () => {
inverted = !inverted;
document.getElementById('inverted').innerText = inverted ? '' : 'not ';
updateBackground();
});
socket.on('toggle-active', (color) => {
active[color] = !active[color];
if (controllingColor === color) {
colorSlider.disabled = !active[color];
}
document.getElementById('active').innerText = active[controllingColor] ? 'active' : 'inactive';
updateBackground();
});
한편, 서버는 새로운 연결을 감지하고 클라이언트에 색상을 할당한 뒤 해당 색상과 현재 애플리케이션 상태를 클라이언트에 전송하며, 소켓을 통해 수신되는 이벤트에 대한 자체 핸들러를 설정합니다:
// index.js
colorNamespace.on('connection', (socket) => {
const color = colors[colorCount % 3]; // 리스트에서 다음 색을 선택하고 순환
colorCount++;
socket.emit('assign-color', color, colorSettings, activeSettings); // 클라이언트를 애플리케이션 상태와 동기화
socket.data.color = color; // 소켓의 data 키에 정보를 저장할 수 있습니다
socket.on('set-color', (color, value) => {
colorSettings[color] = value;
colorNamespace.emit('set-color', color, value);
});
socket.on('invert', () => {
socket.broadcast.emit('invert');
});
});
/admin 페이지도 유사한 방식으로 설정됩니다.
클라이언트에 정보 전송
한 페이지에서 사용자의 상호작용이 다른 모든 페이지에 어떻게 영향을 미치는지 살펴보겠습니다.
파란 페이지에서 사용자가 슬라이더를 움직이면, 슬라이더는 change 이벤트를 발생시키고, 이 이벤트는 슬라이더의 이벤트 리스너에 의해 포착됩니다:
// color.html
colorSlider.addEventListener('change', (event) => {
socket.emit('set-color', controllingColor, event.target.value);
});
그 리스너는 색상과 새로운 값을 포함한 set-color 이벤트를 새로 발생시킵니다. 서버는 클라이언트의 set-color를 수신한 뒤, 자체적으로 모든 클라이언트에 해당 데이터를 전송하기 위해 이벤트를 다시 발생시킵니다. 각 클라이언트는 메시지를 받아 파란 값을 그에 맞게 업데이트합니다.
Source: …
다른 소켓에 브로드캐스트하기
“Invert others” 버튼을 클릭하면 다른 /color 사용자들에게는 영향을 주지만, 실제로 버튼을 클릭한 사용자에게는 영향을 주지 않습니다. 여기서 핵심은 broadcast 플래그입니다:
socket.on('invert', () => {
socket.broadcast.emit('invert'); // 보낸 사람을 제외한 모든 사람에게 전송
});
socket.broadcast.emit을 사용하면 원본 클라이언트가 자신의 브로드캐스트를 받지 않으며, 다른 모든 연결된 소켓은 받게 됩니다. 이 패턴은 변경 사항을 발신자에게 다시 되돌려 보내지 않고 다른 클라이언트들을 업데이트하고 싶을 때 모든 이벤트에 적용할 수 있습니다.
Broadcasting Events – “보낸 사람을 제외한 모든 연결된 클라이언트에게”
Socket.IO documentation – Broadcasting events
서버가 invert 메시지를 받아 다시 전송할 때:
// server.js
socket.on('invert', () => {
socket.broadcast.emit('invert'); // 브로드캐스트
});
이 플래그는 서버가 해당 소켓 외의 모든 소켓에 이벤트를 전송한다는 의미입니다.
이 간단한 예제에서는 멋진 트릭에 불과하지만, 실제로는 원래 만든 사용자가 이미 해당 정보를 가지고 있기 때문에 그 사용자에게 다시 전송하는 것을 방지하는 데 유용합니다.
Source: …
네임스페이스
다른 세 페이지와 달리 admin 탭의 색상이 바뀌지 않는 것을 눈치채셨을 수도 있습니다. 간단히 하기 위해 admin 페이지에 대한 핸들러를 설정하지 않았습니다. 설령 설정했더라도 admin 소켓이 해당 이벤트를 전혀 받지 않기 때문에 아무 일도 일어나지 않을 겁니다. 이는 admin 탭이 다른 네임스페이스에 있기 때문입니다.
// color.html
const socket = io('/color');
// admin.html
const socket = io('/admin');
// index.js (server)
const colorNamespace = io.of('/color');
const adminNamespace = io.of('/admin');
…
오직 /color 네임스페이스에 있는 소켓만 다음을 받습니다:
colorNamespace.emit('set-color', color, value);
(명확성을 위해 두 네임스페이스의 이름을 페이지가 위치한 두 엔드포인트와 동일하게 지정했지만, 반드시 그럴 필요는 없습니다. 클라이언트가 서버와 일치하기만 하면 네임스페이스는 임의의 이름을 가질 수 있으며 기능에는 변화가 없습니다.)
네임스페이스는 소켓의 일부 집합을 편리하게 대상으로 삼을 수 있게 해 주지만, 서로 통신할 수도 있습니다:
// admin.html
const toggleFunction = (color) => {
socket.emit('toggle-active', color);
};
// index.js (server)
// admin 페이지의 버튼을 클릭하면 color 페이지에 변화가 발생합니다
adminNamespace.on('connection', (socket) => {
socket.on('toggle-active', (color) => {
activeSettings[color] = !activeSettings[color];
colorNamespace.emit('toggle-active', color);
});
});
// color.html (client)
socket.on('toggle-active', (color) => {
active[color] = !active[color];
if (controllingColor === color) {
colorSlider.disabled = !active[color];
}
document.getElementById('active').innerText = active[controllingColor] ? 'active' : 'inactive';
updateBackground();
});
위 예시들에서는 모두 클라이언트 중 하나에서 발생한 상호작용에 의해 이벤트가 발생했습니다. 이벤트가 서버로 전송되고, 서버가 적절한 클라이언트들에게 두 번째 메시지를 전송했습니다. 이는 가능한 경우 중 아주 작은 샘플에 불과합니다. 예를 들어, 서버가 WebSocket을 사용해 정기적으로 모든 클라이언트를 업데이트하거나, API에서 정보를 가져와 클라이언트에 푸시할 수도 있습니다. 이 데모는 제가 배우고 있는 내용을 보여주는 예시이며, 앞으로 프로젝트에 적용해 나가고자 합니다.
