이번 포스팅에서는 웹에서의 실시간 통신인 소켓을 이용해보자! 대부분의 설명은 주석으로, 추가적인 코멘트가 필요한 경우 블로그에 적었다.
책 Node.js 교과서(개정 2판) 책의 12장의 내용을 참고했다.
+모든 코드는 github주소에 있다.
폴링(polling)
- 단방향
- 클라이언트 -> 서버
- 클라이언트에서 주기적으로 서버에 업데이트 있는지 확인하는 요청을 보냄
서버센트 이벤트(SSE; Sever Sent Event)
- 단방향
- 서버 -> 클라이언트
- 한 번 연결하면 서버가 클라이언트에 지속적으로 데이터를 보냄
- 클라이언트에서 서버로 데이터를 보낼 수 없음!
그렇다면, 웹 소켓은?
웹 소켓(Web Socket)
- 양방향
- 서버 <-> 클라이언트
- 한 번 웹 소켓이 연결하면 계속 연결된 상태로 있어서 따로 업데이트가 있는지 요청을 보낼 필요가 없음!
- node의 모듈 및 라이브러리:
ws
,Socket.IO
ws는 간단한 웹 소켓 사용에서 좋다.
Github주소: https://github.com/delay-100/study-node/tree/main/ch12/gif-chat-ws
설치할 모듈: ws
npm i ws
+그 외는 package.json
의 dependencies
와 devDependecies
참고
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>
서버 실행 화면
Console 탭 화면
Network 탭
웹 소켓 내용 확인(네트워크 - 메시지 탭)
구현하려는 서비스가 복잡해지면 Socket.IO의 사용이 좋다.
- 핵심:
io
와socket
객체
- 사용자가 직접 이벤트 명을 줄 수 있음
서버 측
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.json
의 dependencies
와 devDependecies
참고
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']
속성을 주면 된다.
크롬(클라이언트) 화면
서버 실행 화면
원래 이 포스팅에서 실시간 GIF 채팅방 만들기 예제까지 다루려 했는데 글이 너무 길어질까봐 다음으로 미뤘다..!ㅎㅎ
잘못된 정보 수정 및 피드백 환영합니다!!