Node.js 교과서 개정 2판 책의 내용을 참고하였음.

개념


노드 생태계에서는 웹 소켓이란 말을 들으면 Socket.IO를 먼저 떠올리는 경우가 많다. 하지만 Socket.IO는 웹 소켓을 활용한 라이브러리일 뿐이면 웹 소켓 그 자체는 아니다.

웹 소켓은 HTML5에 새로 추가된 스펙으로 실시간 양방향 데이터 전송을 위한 기술이며, HTTP와 다르게 WS라는 프로토콜을 사용한다. 따라서 브라우저와 서버가 WS 프로토콜을 지원하면 사용할 수 있다. 최신 브라우저는 대부분 웹 소켓을 지원하고, 노드에서는 ws나 Socket.IO 같은 패키지를 통해 웹 소켓을 사용할 수 있다.

<aside> 💡 웹 소켓이 나오기 이전에는 폴링(polling)이라 불리는 방식을 이용해 HTTP 기술을 사용하여 실시간 데이터 전송을 구현했다. 폴링 방식은 HTTP가 클라이언트에서 서버로 향하는 단방향 통신을 주기적으로 서버에 새로운 업데이트가 있는지 확인하는 요청을 보낸 후, 있다면 새로운 내용을 가져오는 단순 무식한 방법이었다.

</aside>

HTML5가 나오면서 웹 브라우저와 웹 서버가 지속적으로 연결된 라인을 통해 실시간으로 데이터를 주고받을 수 있는 웹 소켓이 등장했다. 웹 소켓 연결이 이루어지고 나면 계속 연결된 상태로 있으므로 따로 업데이트가 있는지 요청을 보낼 필요가 없다. 업데이트할 내용이 생겼다면 서버에서 바로 클라이언트에 알릴 수 있게되었다. 폴링 방식에 비해 성능도 매우 개선되었다.

단방향 통신 - 서버센트 이벤트(Server Sent Events) SSE

EventSource라는 객체를 사용한다. 처음에 한 번만 연결하면 서버가 클라이언트에 지속적으로 데이터를 보낸다. 웹 소켓과 다른 점은 클라이언트에서 서버로는 데이터를 보낼 수 없다는 것이다. 서버에서 클라이언트로 데이터를 보내는 단방향 통신이다. 이러한 기능은 양방향인 웹소켓으로 구현을 할 수 있긴 하다. 하지만 서버에서 일방적으로 데이터를 내려주기만 해도 되는 주식 차트 업데이트나 SNS에서 새로운 게시물 가져오기 같이 굳이 양방향 통신이 필요 없는 경우도 있기 때문에 고려해볼 수 있다.

Socket.IO

웹 소켓을 편리하게 사용할 수 있도록 도와주는 라이브러리이다. 웹 소켓을 지원하지 않는 IE9와 같은 브라우저에서는 알아서 웹 소켓 대신 폴링 방식을 사용하여 실시간 데이터 전송을 가능하게 한다. 클라이언트 측에서 웹 소켓 연결이 끊겼다면 자동으로 재연결을 시도하고, 채팅방을 쉽게 구현할 수 있도록 메서드를 준비해두었다.

예제 소스


ws 패키지는 간단하게 웹 소켓을 사용하고자 할 때 좋다. 좀 더 복잡한 서비스를 구현하고자 한다면 Socket.IO를 사용하는 것이 편하다.

app.js

const express = require('express');
const path = require('path');
const morgan = require('morgan');
const cookieParser = require('cookie-parser');
const session = require('express-session');
const nunjucks = require('nunjucks');
const dotenv = require('dotenv');

dotenv.config();
const webSocket = require('./socket');
const indexRouter = require('./routes');

const app = express();
app.set('port', process.env.port || 8081);
app.set('vie engine', 'html');
nunjucks.configure('views', {
  express: app,
  watch: true
});

app.use(morgan('dev'));
app.use(express.static(path.join(__dirname, 'public')));
app.use(express.json());
app.use(express.urlencoded({ extended: false}));
app.use(cookieParser(process.env.cookie_secret));
app.use(session({
  resave: false,
  saveUninitialized: false,
  secret: process.env.cookie_secret,
  cookie: {
    httpOnly: true,
    secure: false
  }
}));

app.use('/', indexRouter);

app.use((req, res, next) => {
  const error = new Error(`${req.method} ${req.url} 라우터가 없습니다.`);
  error.status = 404;
  next(error);
});

app.use((err, req, res, next) => {
  res.locals.messate = err.message;
  res.locals.error = process.env.NODE_ENV !== 'production' ? err : {};
  res.status(err.status || 500);
  res.render('error');
})

const server = app.listen(app.get('port'), () => {
  console.log(app.get('port'), '번 포트에서 대기 중');
})

webSocket(server);

socket.js

const webSocket = require('ws');

module.exports = (server) => {
  const wss = new WebSocket.Server({ server });

  wss.on('connection', (ws, req) => { //웹소켓 연결시
    const ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress;
    console.log('새로운 클라이언트 접속', ip);
    
    ws.on('message', (message) => { //클라이언트로부터 메시지 수신 시
      console.log('message : ', message);
    })
    ws.on('error', (error) => { //에러 시
      console.log('error : ', error);
    })
    ws.on('close', () => { //연결 종료 시
      console.log('클라이언트 접속 해제', ip);
      clearInterval(ws.interval);
    })

    ws.interval = setInterval(() => { // 3초마다 클라이언트로 메시지 전송
      if (ws.readyState === ws.OPEN) {
        ws.send('서버에서 클라이언트로 메시지를 보냅니다.');
      }
    }, 3000);
  });
}