안녕하세요. 이번 시간에는 socket.io로 실시간 업데이트를 구현해보겠습니다. socket.io는 웹소켓을 사용해서 클라이언트에 실시간으로 데이터를 전송합니다(웹소켓이 없을 시, xhr이나 flash를 사용합니다). 클라이언트에서는 이벤트 리스너로 대기하고 있으면 새로운 정보가 들어옴에 따라 보이는 정보를 업데이트할 수 있습니다. 게임이 끝났을 때의 레벨 정보를 다른 클라이언트에 실시간으로 전송해봅시다.
npm install --save socket.io
서버에서 쓸 socket.io입니다. 클라이언트 용은 따로 html 스크립트로 불러와야 합니다. socket.js를 만들어줍시다.
socket.js
module.exports = (io) => {
io.on('connection', (socket) => { // 웹소켓 연결 시
console.log('Socket initiated!');
socket.on('newScoreToServer', (data) => { // 클라이언트에서 newScoreToServer 이벤트 요청 시
console.log('Socket: newScore');
io.emit('newScoreToClient', data);
});
});
};
설명은 나중에 한 번에 하겠습니다. 이벤트 리스너 형식으로 되어있는 것이 포인트입니다. 아래와 같이 서버와 연결해줍시다. 서버와 연결하는 순간 socket.js의 이벤트 리스너들이 서버에 부착됩니다.
server.js
// ... 기존 코드
const passport = require('./passport.js');
const SocketIo = require('socket.io'); // 추가
const socketEvents = require('./socket.js'); // 추가
// ... 기존 코드
const server = app.listen(8080, () => { // const server 부분 추가
console.log('Express App on port 8080!');
});
const io = SocketIo(server); // socket.io와 서버 연결하는 부분
socketEvents(io); // 아까 만든 이벤트 연결
유의할 점은 마지막에 app.listen
을 변수에 담아서 socket.io 패키지와 한 번 더 연결해준다는 겁니다. 그 결과물인 io 변수를 이제 socket 모듈에 전달하는 거죠. 이렇게 전달된 io는 socket.js 모듈에서 사용됩니다. io.on('connection', 콜백)
은 서버와 클라이언트의 소켓이 연결되었을 때 실행됩니다. 아래 클라이언트 부분을 보시면 connect 메소드를 호출하는 데 그 부분이 서버와 연결을 시도하는 부분입니다. callback 안에 이제 진짜 socket을 가지고 할 일들을 넣어주면 됩니다.
코드 중에 socket.on('이벤트명', 콜백)
이 있는데요. 이 부분이 클라이언트에서 서버로 오는 요청을 처리할 이벤트 리스너입니다. 보면 newScoreToServer라는 요청이 왔을 때 io.emit('newScoreToClient', data)
를 실행합니다. io.emit
은 다시 클라이언트로 데이터를 보내는 겁니다. 정확히는 자신을 포함한 모든 클라이언트로 소켓 요청을 보내는 겁니다.
정리하자면, 한 클라이언트에서 소켓을 통해 서버로 데이터를 보냅니다. 서버는 그 데이터를 받아 모든 클라이언트에게 다시 보내는 거죠. 이렇게 보냄과 동시에 모든 클라이언트가 같은 데이터를 전송받을 수 있습니다. 즉 한 사람이 점수 기록을 경신한 정보가 플레이 중인 모든 사람들에게 전달됩니다.
이제 클라이언트를 볼까요? 그 전에 클라이언트를 좀 수정해봅시다. 전 시간에 사용한 파일들을 계속 사용합니다.
layout.pug
// ... 기존 코드
// turn.js 불러오는 스크립트 위에 다음 줄들 추가: 여기부터
script(src='/socket.io/socket.io.js')
script.
var socket = io.connect('http://localhost:8080');
var scores = [];
function updateScoreboard(scores) {
var scoreboard = document.getElementById('scoreboard');
scoreboard.innerHTML = '';
var frag = document.createDocumentFragment();
for (var i = 0; i < scores.length; i++) {
var div = document.createElement('div');
var name = document.createElement('b');
var lev = document.createElement('span');
name.innerHTML = scores[i].name;
lev.innerHTML = scores[i].lev;
div.appendChild(name);
div.appendChild(lev);
frag.appendChild(div);
}
scoreboard.appendChild(frag);
}
socket.on('newScoreToClient', function (data) {
scores.push(data);
updateScoreboard(scores);
});
// 여기까지 추가
script(src='turn.js')
main.pug
// #screen 태그 아래에 추가
#screen
// ... 기존 태그
#monster-stat
span#monster-name
span#monster-hp
span#monster-att
#scoreboard
turn.js
// gameOver 메소드를 다음과 같이 변경
gameOver: function () {
document.getElementById('screen').innerHTML = hero.name + '은 레벨' + hero.lev + '에서 죽었습니다. 새로 시작하려면 새로고침하세요';
socket.emit('newScoreToServer', { name: hero.name, lev: hero.lev }); // 추가
return false;
},
일단 layout.pug에 socket.io를 불러오고, io.connect
메소드로 서버와의 연결을 시도했습니다. 또한 서버에서 오는 이벤트를 받을 이벤트리스너를 연결해주었습니다. newScoreToClient 이벤트가 오면 #scoreboard 태그 안에 점수를 그립니다. 따라서 #scoreboard 태그를 main.pug에 만들어주었고요. turn.js의 gameOver 메소드에는 게임오버시 서버로 socket 요청을 할 메소드를 작성했습니다.
socket.emit('이벤트명', 데이터)
가 그것입니다. 해당 이벤트명으로 데이터를 서버로 보냅니다. 위의 예에서는 newScoreToServer의 이름으로 서버에 영웅의 name과 lev 정보를 보냈네요. 서버의 이벤트 리스너에 data 매개변수로 전송됩니다.
서버에 data 객체가 보내지면 서버는 이벤트 리스너 콜백에 따라 io.emit('newScoreToClient', data)
를 통해 다시 모든 클라이언트로 데이터를 보내게 됩니다. 이렇게 모든 클라이언트에 실시간으로 방금 죽은 영웅의 이름과 데이터 정보가 전송됩니다. 브라우저를 여러 개 켜놓고 실행해보세요. 다음과 같이 작동하는 것을 볼 수 있습니다.
크롬와 엣지를 동시에 실행시킨 후 게임을 진행했습니다(다른 브라우저여도 상관없습니다). 크롬에서 게임오버가 되자 엣지에 실시간으로 게임오버 정보가 전달됩니다.
더 다양한 정보를 전송해보세요! 이번 시간은 맛보기였고, 다음 시간에는 socket.io API에 대해 구체적으로 알아봅시다.
로컬 redis 서버를 cloud redis 서버로 분리하였으나 동일 증상이 발생되어, 크롬 f12 네트워크 탭을 보니 요청이 실패한 경우 provisional headers are shown 경고가 발생하는걸 확인했습니다. 보니까 button의 onclick 클릭 이벤트에 함수를 만들어서 요청한게 실수였던거 같습니다. 교재 방법 대로 \u003cform> 태그와 \u003cbutton type="submit"> 으로 해보겠습니다.