웹 소켓(Web Socket) 이용하기(with. ws 모듈, Socket.IO)

백지연·2022년 3월 11일
2

NodeJS

목록 보기
23/26
post-thumbnail

이번 포스팅에서는 웹에서의 실시간 통신인 소켓을 이용해보자! 대부분의 설명은 주석으로, 추가적인 코멘트가 필요한 경우 블로그에 적었다.

책 Node.js 교과서(개정 2판) 책의 12장의 내용을 참고했다.
+모든 코드는 github주소에 있다.


1. 웹 소켓(Web Socket)이란?

폴링(polling)

  • 단방향
  • 클라이언트 -> 서버
  • 클라이언트에서 주기적으로 서버에 업데이트 있는지 확인하는 요청을 보냄

서버센트 이벤트(SSE; Sever Sent Event)

  • 단방향
  • 서버 -> 클라이언트
  • 한 번 연결하면 서버가 클라이언트에 지속적으로 데이터를 보냄
  • 클라이언트에서 서버로 데이터를 보낼 수 없음!

그렇다면, 웹 소켓은?

웹 소켓(Web Socket)

  • 양방향
  • 서버 <-> 클라이언트
  • 한 번 웹 소켓이 연결하면 계속 연결된 상태로 있어서 따로 업데이트가 있는지 요청을 보낼 필요가 없음!
  • node의 모듈 및 라이브러리: ws, Socket.IO

2. ws 모듈로 웹 소켓 사용하기

ws는 간단한 웹 소켓 사용에서 좋다.

Github주소: https://github.com/delay-100/study-node/tree/main/ch12/gif-chat-ws

설치할 모듈: ws

npm i ws

+그 외는 package.jsondependenciesdevDependecies참고

ws 주요 코드

Git [gif-chat-ws/package.json]

{
  "name": "gif-chat",
  "version": "1.0.0",
  "main": "app.js",
  "scripts": {
    "start": "nodemon app"
  },
  "author": "delay100",
  "license": "ISC",
  "dependencies": {
    "cookie-parser": "^1.4.6",
    "dotenv": "^16.0.0",
    "express": "^4.17.3",
    "express-session": "^1.17.2",
    "morgan": "^1.10.0",
    "nunjucks": "^3.2.3",
    "ws": "^8.5.0"
  },
  "devDependencies": {
    "nodemon": "^2.0.3"
  },
  "description": ""
}

Git [gif-chat-ws/app.js] 中 socket 주요 코드

const express = require('express');
const session = require('express-session');
const webSocket = require('./socket'); // 웹 소켓
const app = express();

//express-session, 인수: session에 대한 설정
app.use(session({
    resave: false, // resave : 요청이 올 때 세션에 수정 사항이 생기지 않더라도 세션을 다시 저장할지 설정
    saveUninitialized: false,  // saveUninitialized : 세션에 저장할 내역이 없더라도 처음부터 세션을 생성할지 설정
    secret: process.env.COOKIE_SECRET,
    cookie: {
        httpOnly: true, // httpOnly: 클라이언트에서 쿠키를 확인하지 못하게 함
        secure: false, // secure: false는 https가 아닌 환경에서도 사용 가능 - 배포할 때는 true로 
    },
}));

...

// 웹 소켓을 express에 연결
const server = app.listen(app.get('port'), () => {
    console.log(app.get('port'), '번 포트에서 대기 중');
});

webSocket(server);

Git [gif-chat-ws/socket.js] 中 ws 주요 코드

// 웹 소켓 로직이 들어있음
// ws 웹 소켓 - 양방향 통신이므로 client에도 작성해줘야 함 views/index.html
const WebSocket = require('ws');

module.exports = (server) => { // server: app.js에서 넘겨준 서버
    const wss = new WebSocket.Server({ server });  // express 서버를 웹 소켓 서버와 연결함
                                                   // express(HTTP)와 웹 소켓(WS)은 같은 포트를 공유할 수 있으므로 별도의 작업 필요X
    
    wss.on('connection', (ws, req) => { // 연결 후 웹 소켓 서버(wss)에 이벤트 리스너를 붙힘 - connection 이벤트
                                        // 웹 소켓은 이벤트 기반으로 작동되므로 항상 대기해야 함
        const ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress; // req.headers['x-forwarded-for'] || req.connection.remoteAddress: 클라이언트의 IP를 알아내는 유명한 방법 중 하나
                                                                                   // express에서는 IP 확인 시 proxy-addr 패키지를 사용하므로 이 패키지(proxy-addr) 사용해도 괜찮음
                                                                                   // localhost 접속 시 크롬에서 IP가 ::1로 뜸, 다른 브라우저는 ::1외에 다른 IP가 뜰 수 있음
        console.log('새로운 클라이언트 접속', ip);

        // 이벤트 리스너(message, error, close) 세 개 연결
        ws.on('message', (message) => { // 클라이언트로부터 메시지 수신 시(메시지 왔을 때 발생), 클라이언트의 onmessage 실행 시 실행됨
            console.log(message.toString());
        });
        ws.on('error', (error) => { // 에러 시(웹 소켓 연결 중 문제가 발생한 경우)
            console.error(error);
        });
        ws.on('close', () => { // 연결 종료 시(클라이언트와 연결 끊겼을 때 발생)
            console.log('클라이언트 접속 해제', ip);
            clearInterval(ws.interval); // setInterval을 clearInterval로 정리 - 안 적어주면 메모리 누수 발생
        });

        ws.interval = setInterval(() => { // 3초마다 연결된 모든 클라이언트로 메시지 전송
            if(ws.readyState == ws.OPEN) { // OPEN(열림) - OPEN일때 만 에러 없이 메시지 전송 가능
                                           // + CONNECTION(연결 중), CLOSING(닫는 중), CLOSED(닫힘)
                ws.send('서버에서 클라이언트로 메시지를 보냅니다.'); 
            }
        }, 3000);
    });
};

Git [gif-chat-ws/views/index.html]

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>GIF 채팅방</title>
    </head>
    <body>
        <div>F12를 눌러 console 탭과 network 탭을 확인하세요.</div>
        <script>
            const webSocket = new WebSocket("ws://localhost:8005"); // 연결할 서버 주소를 넣고 webSocket 객체 생성, 서버의 주소 프로토콜이 ws임
            // 서버처럼 이벤트 리스너로 동작
            webSocket.onopen = function() { // onopen 이벤트 리스너 호출: 서버와 연결이 맺어지는 경우
                console.log('서버와 웹소켓 연결 성공!');
            };
            webSocket.onmessage = function (event) { // onmessage 이벤트 리스너 호출: 서버로부터 메시지가 오는 경우, 서버에서 메세지가 오면 서버로 답장을 보냄
                console.log(event.data);
                webSocket.send('클라이언트에서 서버로 답장을 보냅니다.');
            };
        </script>
    </body>
</html>

ws 실행결과

  • 서버 실행 화면

  • Console 탭 화면

  • Network 탭

  • 웹 소켓 내용 확인(네트워크 - 메시지 탭)


3. Socket.IO로 웹 소켓 사용하기

구현하려는 서비스가 복잡해지면 Socket.IO의 사용이 좋다.

  • 핵심: iosocket 객체
  • 사용자가 직접 이벤트 명을 줄 수 있음

    서버 측
    socket.on('이벤트 명', (data) => { 전달할 정보 });

    클라이언트 측
    socket.emit('이벤트 명', '전달할 정보');

  • 먼저 폴링 방식으로 자동으로 연결되고 다음부터, 웹 소켓을 사용(웹 소켓을 지원하지 않는 브라우저 대비)

    처음부터 웹 소켓만 사용하고 싶다면?(아래 예제 코드)
    io.connect 시, transports: ['websocket'],를 추가

Github주소: https://github.com/delay-100/study-node/tree/main/ch12/gif-chat-socketIO

설치할 모듈: socket.io@2

npm i socket.io@2

+그 외는 package.jsondependenciesdevDependecies참고

Socket.IO 주요 코드

Git [gif-chat-ws/package.json]

{
  "name": "gif-chat",
  "version": "1.0.0",
  "main": "app.js",
  "scripts": {
    "start": "nodemon app"
  },
  "author": "delay100",
  "license": "ISC",
  "dependencies": {
    "cookie-parser": "^1.4.6",
    "dotenv": "^16.0.0",
    "express": "^4.17.3",
    "express-session": "^1.17.2",
    "morgan": "^1.10.0",
    "nunjucks": "^3.2.3",
    "socket.io": "^2.4.1",
  },
  "devDependencies": {
    "nodemon": "^2.0.3"
  },
  "description": ""
}

Git [gif-chat-ws/app.js] 中 socket 주요 코드

const express = require('express');
const session = require('express-session');
const webSocket = require('./socket'); // 웹 

//express-session, 인수: session에 대한 설정
app.use(session({
    resave: false, // resave : 요청이 올 때 세션에 수정 사항이 생기지 않더라도 세션을 다시 저장할지 설정
    saveUninitialized: false,  // saveUninitialized : 세션에 저장할 내역이 없더라도 처음부터 세션을 생성할지 설정
    secret: process.env.COOKIE_SECRET,
    cookie: {
        httpOnly: true, // httpOnly: 클라이언트에서 쿠키를 확인하지 못하게 함
        secure: false, // secure: false는 https가 아닌 환경에서도 사용 가능 - 배포할 때는 true로 
    },
}));

// 웹 소켓을 express에 연결
const server = app.listen(app.get('port'), () => {
    console.log(app.get('port'), '번 포트에서 대기 중');
});

webSocket(server);

=> 위의 ws모듈 사용에서 연결(app.js)과 동일하다

Git [gif-chat-ws/socket.js] 中 Socket.IO 주요 코드

// 웹 소켓 로직이 들어있음
const SocketIO = require('socket.io');

module.exports = (server) => {
    const io = new SocketIO(server, { path: '/socket.io' }); // socket.io를 불러와 express와 연결
                                                             // SocketIO의 두 번째 인수로 옵션 객체를 넣어 서버에 관한 여러가지 설정 가능
                                                             // path: 클라이언트가 접속할 경로 설정(클라이언트에서도 이 경로와 일치하는 path를 넣어야 함)
        // 웹 소켓 연결 후 이벤트 리스너를 붙힘
        // io와 socket객체가 Socket.IO의 핵심임
        io.on('connection', (socket) => { // connection: 클라이언트가 접속했을 때 발생, 콜백으로 소켓 객체(socket) 제공
        const req = socket.request; // socket.request: "요청" 객체에 접근 가능, socket.request.res: "응답" 객체에 접근 가능

        const ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress;// req.headers['x-forwarded-for'] || req.connection.remoteAddress: 클라이언트의 IP를 알아내는 유명한 방법 중 하나
                                                                                  // express에서는 IP 확인 시 proxy-addr 패키지를 사용하므로 이 패키지(proxy-addr) 사용해도 괜찮음
                                                                                  // localhost 접속 시 크롬에서 IP가 ::1로 뜸, 다른 브라우저는 ::1외에 다른 IP가 뜰 수 있음

        console.log('새로운 클라이언트 접속', ip, socket.id, req.ip); // socket.id: 소켓의 고유 아이디(소켓 주인이 누군지 특정 가능) 가져옴
        socket.on('disconnect', () => { // 연결 종료 시
            console.log('클라이언트 접속 해제', ip, socket.id);
            clearInterval(socket.interval);
        });
        socket.on('error', (error) => { // 에러 시
            console.error(error);
        });
        socket.on('reply', (data) => { // 사용자가 직접 만든 이벤트(views/index.html), 클라이언트로부터 메시지 수신 시
                                       // reply라는 이벤트명으로 데이터를 보낼 때 서버에서 받는 부분
            console.log(data);
        });

        socket.interval = setInterval(() => { // 3초마다 클라이언트로 메시지 전송
           socket.emit('news', 'Hello Socket.IO'); // emit 메서드 첫 번째 인수: 이벤트 이름, 두 번째 인수: 데이터 
                                                   // 클라이언트가 이 메시지를 받으려면 클라이언트에 news 이벤트 리스너를 만들어야 함 
        }, 3000); 
    });
};

Git [gif-chat-ws/views/index.html]

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>GIF 채팅방</title>
    </head>
    <body>
        <div>F12를 눌러 console 탭과 network 탭을 확인하세요.</div>
        <script src="/socket.io/socket.io.js"></script> {# Socket.IO에서 클라이언트로 제공하는 스크립트, 실제 파일이 아님 - 이 스크립트로 서버와 유사한 API로 웹 소켓 통신 가능 #}
        <script>
            const socket = io.connect('http://localhost:8005', { // 스크립트가 제공하는 io 객체에 서버 주소를 적어 연결, ws 프로토콜이 아닌 http 프로토콜 사용
                path: '/socket.io', // 서버의 path 옵션(socket.js의 path)과 같아야 통신 가능
                transports: ['websocket'], // 웹 소켓 형식만 주고 싶은 경우(안 적어주면 맨 처음에 폴링 방식(xhr) 실행 됨 - 웹 소켓을 호환하지 않을 수도 있음을 자동으로 대비)
            });
            socket.on('news', function (data) { // news 이벤트를 받기 위해 news 이벤트 리스너를 붙힘
                console.log(data);
                socket.emit('reply', 'Hello Node.JS'); // reply 이벤트 리스너로 답장 보냄
            });
        </script>
    </body>
</html>

=> 처음부터 웹 소켓만 사용하고 싶은 경우 위에 설명했듯 transports: ['websocket']속성을 주면 된다.

Socket.IO 실행결과

  • 크롬(클라이언트) 화면

  • 서버 실행 화면


원래 이 포스팅에서 실시간 GIF 채팅방 만들기 예제까지 다루려 했는데 글이 너무 길어질까봐 다음으로 미뤘다..!ㅎㅎ

잘못된 정보 수정 및 피드백 환영합니다!!

profile
TISTORY로 이사중! https://delay100.tistory.com

0개의 댓글