본문 바로가기

Node.js

Node.js - ws로 웹 소켓 사용해보기 (Web Socket Using ws Module)

먼저 gif-chat이라는 새로운 프로젝트를 다음과 같이 만들어주고 패키지들을 설치해주자.

 

{
  "name": "gif-chat",
  "version": "0.0.1",
  "description": "GIF CHATTING",
  "main": "app.js",
  "scripts": {
    "start": "nodemon app"
  },
  "author": "Effort",
  "license": "ISC",
  "dependencies": {
    "cookie-parser": "^1.4.5",
    "dotenv": "^8.2.0",
    "express": "^4.17.1",
    "express-session": "^1.17.1",
    "morgan": "^1.10.0",
    "nunjucks": "^3.2.2"
  },
  "devDependencies": {
    "nodemon": "^2.0.4"
  }
}

 

그 후, .env 파일에 COOKIE_SECRET에 쿠키 비밀 키를 넣어준다. 그리고 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 indexRouter = require('./routes');

const app = express();
app.set('port', process.env.PORT || 8005);
app.set('view 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.message = err.message;
    res.locals.error = process.env.NODE_ENV !== 'production' ? err : {};
    res.status(err.status || 500);
    res.render('error');
});

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

 

그리고 인덱스 라우터 (routes/index.js) 를 다음과 같이 만들어주자.

 

const express = require('express');

const router = express.Router();

router.get('/', (req, res) => {
    res.render('index');
});

module.exports = router;

 

이제 ws 모듈을 설치하여 웹 소켓을 구현해보자.

 

> npm i ws

 

그 후 app.js에 다음과 같이 연결해준다.

 

...

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

...

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

webSocket(server);

 

서버 변수를 따로 빼준 후 webSocket 함수에 넣어준다.

 

이제 웹 소켓 로직이 들어 있는 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);
        });
        ws.on('error', (err) => { // 에러 발생 시
            console.error(err);
        });
        ws.on('close', () => { // 연결 종료 시
            console.log('클라이언트 접속 해제', ip);
            clearInterval(ws.interval);
        });

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

 

 

 

ws 모듈을 불러온 후 서버를 웹 소켓 서버와 연결했다. 익스프레서 (HTTP) 와 웹 소켓 (WS) 은 같은 포트를 공유할 수 있으므로 별도의 작업이 필요없다.

 

연결 후에는 웹 소켓 서버 (wss) 에 이벤트 리스너를 붙인다. 웹 소켓은 이벤트 기반으로 작동한다고 생각하면 된다. 실시간으로 데이터를 전달받기 때문에 항상 대기하고 있어야 한다. connection 이벤트는 클라이언트가 서버와 웹 소켓 연결을 맺을 때 발생한다. req.headers['x-forwarded-for'] || req.connection.remoteAddress 는 클라이언트의 IP를 알아내는 유명한 방법 중 하나이므로 알아두는 것이 좋다. 익스프레스에서는 IP를 확인할 때, proxy-addr 패키지를 사용하므로 이 패키지를 사용해도 좋다. 로컬 호스트로 접속한 경우, 크롬에서는 IP가 ::1 으로 뜬다. 다른 브라우저에선 ::1 외의 다른 IP가 뜰 수 있다.

 

익스프레스 서버와 연결한 후, 웹 소켓 객체 (ws) 에 이벤트 리스너 세 개, message, error, close를 연결했다. message는 클라이언트로부터 메시지가 왔을 때, error는 웹 소켓 연결 중 문제가 생겼을 때, 그리고 close는 클라이언트와 연결이 끊겼을 때 발생한다.

 

setInterval은 3초마다 연결된 모든 클라이언트에 메시지를 보낸다. 먼저 readyState가 OPEN 상태인지 확인한다. 웹 소켓에는 네 가지 상태가 있는데, 바로 CONNECTING (연결 중), OPEN (열림), CLOSING (닫는 중), CLOSED (닫힘) 이다. OPEN일 때만 에러 없이 메시지를 보낼 수 있다. 확인 후 ws.send 메서드로 하나의 클라이언트에 메시지를 보낸다. 또한, close 이벤트에서 setInterval을 clearInterval로 정리하는 것을 기억해둬야 한다. setInterval을 끝내지 않는다면 메모리 누수가 발생할 것이다.

 

웹 소켓은 단순히 서버에서 설정한다고 동작하는 것이 아니다. 클라이언트에도 웹 소켓을 적용해줘야 한다. views 폴더를 만들어 index.html 파일을 작성하여 script 태그에 웹 소켓 코드를 넣자. views 폴더 안에 error.html도 같이 작성해주자.

 

아래는 index.html 파일이다. WebSocket 객체를 만들어 서버 주소로 연결해주자.

 

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>GIF 채팅방</title>
</head>
<body>
    <div>F12키를 눌러 console 탭과 network 탭을 확인해보세요.</div>
    <script>
        const webSocket = new WebSocket("ws://localhost:8005");
        webSocket.onopen = function () {
            console.log('서버와 웹 소켓 연결됨');
        };
        webSocket.onmessage = function (event) {
            console.log(event.data);
            webSocket.send('클라이언트에서 서버로 답장을 보냅니다.');
        }
    </script>
</body>
</html>

 

WebSocket 생성자에 연결할 서버 주소를 넣고 webSocket 객체를 생성한다. 서버 주소의 프로토콜이 ws인 것에 주의하자. 클라이언트 역시 이벤트 기반으로 동작한다. 서버와 연결이 맺어지는 경우에는 onopen 이벤트 리스너가 호출되고, 서버로부터 메시지가 오는 경우엔 onmessage 이벤트 리스너가 호출된다. 서버에서 메시지가 오면 서버로 답장을 보낸다.

 

서버를 실행하는 순간, 서버는 클라이언트에 3초마다 메시지를 보내고, 클라이언트도 서버로부터 메시지가 오는 순간 바로 답장을 보낸다. 브라우저와 노드 콘솔에서 결과를 확인해보자.

 

http://localhost:8005에 접속하여 콘솔 탭을 켜보자. 접속하는 순간부터 노드의 콘솔과 브라우저의 콘솔에 3초마다 메시지가 찍힐 것이다. 브라우저의 콘솔은 메시지가 왔기 때문에 event의 data를 출력하고, 노드의 콘솔은 다시 메시지를 전달받았기 때문에 메시지가 출력된다.

 

노드 콘솔

새로운 클라이언트 접속 ::1
GET /favicon.ico 404 7.630 ms - 1335
클라이언트에서 서버로 답장을 보냅니다.
클라이언트에서 서버로 답장을 보냅니다.
클라이언트에서 서버로 답장을 보냅니다.
클라이언트에서 서버로 답장을 보냅니다.
클라이언트에서 서버로 답장을 보냅니다.

 

브라우저 콘솔

 

 

Network 탭에서 웹 소켓 항목을 클릭한 후 Messages 탭에서 메시지가 잘 오가는 것을 볼 수 있다.

 

 

 

출처

Node.js 교과서 개정 2판 - 길벗, 조현영