socket.io란?
실시간, 양방향, 이벤트에 기반한 통신 등을 모두 지원하는 라이브러리다 .
웹소켓과 유사하지만 엄연히 다른, '웹소켓을 활용하는 라이브러리' 라고 이해하면 좋다.
1. socket.io 적용 준비하기
1-1) server.js 코드 수정
- ws와 관련된 코드 모두 삭제
import http from 'http' ;
import express from 'express' ;
const app = express ();
app . set ( "view engine" , "pug" );
//dirname은 Node.js기본 전역변수로, 현재 실행되는 폴더의 경로를 의미
app . set ( "views" , __dirname + "/views" );
app . use ( "/public" , express . static ( __dirname + "/public" ));
// '/'를 받으면 home으로 가게 설정
app . get ( "/" , ( req , res ) => res . render ( "home" ));
//만약홈이 아닌 다른 주소로 get요청을 보내더라도 홈으로 리다이렉션하게 예외처리함
app . get ( "/*" , ( req , res ) => res . redirect ( "/" ));
const server = http . createServer ( app );
const handleListen = () => console . log ( "Listening on http://localhost:3000" );
server . listen ( 3000 , handleListen );
1-2) socket.io 다운로드
1-3) socket.io에 맞게 server.js코드 재수정
import http from 'http' ;
import SocketIO from "socket.io" ;
import express from 'express' ;
const app = express ();
app . set ( "view engine" , "pug" );
//dirname은 Node.js기본 전역변수로, 현재 실행되는 폴더의 경로를 의미
app . set ( "views" , __dirname + "/views" );
app . use ( "/public" , express . static ( __dirname + "/public" ));
// '/'를 받으면 home으로 가게 설정
app . get ( "/" , ( req , res ) => res . render ( "home" ));
//만약홈이 아닌 다른 주소로 get요청을 보내더라도 홈으로 리다이렉션하게 예외처리함
app . get ( "/*" , ( req , res ) => res . redirect ( "/" ));
const httpServer = http . createServer ( app );
const wsServer = SocketIO ( httpServer ) l
const handleListen = () => console . log ( "Listening on http://localhost:3000" );
httpServer . listen ( 3000 , handleListen );
설명 : socket.io를 이용해 웹소켓 서버를 만드는 방식은 기존ws 방식과 유사.
HTTP 서버와 웹소켓 서버를 명확히 구분하기 위해 이름을 각각 httpServer, wsServer로 정했음
wsServer를 만들기 위해 우리는 SocketIO에 httpServer를 넘겨주고 있음.
<실행결과>
잘 실행되어 콘솔로그("Listening on http://localhost:3000") 역시 잘 출력됨을 확인할 수 있다.
2. socket.io 적용하기
사용자가 채팅에 참여하고 싶다면 먼저 채팅룸부터 만들고 그 안에서 메시지를 교환할 수 있게 할 예정.
2-1) home.pug수정하기
home.pug에서 socket.io 설치 작업도 진행될 예정.
doctype html
html ( lang = "en" )
head
meta ( charset = "UTF-8" )
meta ( http-equiv = "X-UA-Compatible" , content = "IE=edge" )
meta ( name = "viewport" , content = "width=device-width, initial-scale=1.0" )
title Zoom
//MVP.css는 우리가 태그에 class나 id같은 특성을 추가하지 않아도 자동으로 스타일을 적용해주는 라이브러리다.
body
//h1 It works!
header
h1 Zoom
main
script ( src = "/socket.io/socket.io.js" )
script ( src = "/public/js/app.js" )
설명: script ( src = "/socket.io/socket.io.js" )
이 코드가 가능한 이유는 socket.io가 단순히 설치하여 서버를 만들어주기만 하여도 우리에게 url을 제공해주기 때문.
브라우저를 열고 주소 창에 http://localhost:3000/socket.io/socket.io.js를 입력하면
다음과 같이 socket.io의 소스 코드를 제공하고 있는 url임을 확인할 수 있음.
이렇게 소스코드를 제공받음으로써, 사용자는 socket.io가 제공하는 기능을 브라우저에 적용할 수 있게 됨.
2-2) app.js 수정하기
여태껏 코드에서 new WebSocket(`ws://${window.location.host}`)가 바로 서버와 연결을 시도하는 부분이였는데,
이제는 socket.io를 이용하기 때문에 io라는 함수로 대체하게 됨.
코드를 전부 지우고 다음의 코드로 대체
io()는 알아서 서버를 찾고 기능을 제공하는 꿀같은 명령어임!
2-3) server.js 수정하기
서버 쪽에서 연결(connection)이벤트 핸들러를 만들어서 연결을 확인해보려고 함.
wsServer . on ( "connection" , ( socket ) => {
console . log ( socket );
});
다음의 코드를 추가하고 브라우저에 접속해 보자.
<터미널(콘솔) 화면>
socket에 대한 방대한 내용이 잘 출력됨을 확인가능하다.
2-4) home.pug 수정하기
채팅룸에 접속할 때 채팅룸의 이름을 입력할 폼부터 만들어 보자.
이미 존재하는 이름 - 거기에 참가하는 것
새로운 이름 - 채팅룸이 생성되면서 첫 번째 참가자가 되는 것
doctype html
html ( lang = "en" )
head
meta ( charset = "UTF-8" )
meta ( http-equiv = "X-UA-Compatible" , content = "IE=edge" )
meta ( name = "viewport" , content = "width=device-width, initial-scale=1.0" )
title Zoom
//MVP.css는 우리가 태그에 class나 id같은 특성을 추가하지 않아도 자동으로 스타일을 적용해주는 라이브러리다.
body
//h1 It works!
header
h1 Zoom
main
div #welcome
form
input ( placeholder = "room name" , required , type = "text" )
button Enter Room
script ( src = "/socket.io/socket.io.js" )
script ( src = "/public/js/app.js" )
2-5) app.js 코드 수정
const socket = io ();
const welcome = document . getElementById ( "welcome" );
const form = welcome . querySelector ( "form" );
function handleRoomSubmit ( event ){
event . preventDefault ();
const input = form . querySelector ( "input" );
socket . emit ( "enter_room" , { payload : input . value });
input . value = "" ;
}
form . addEventListener ( "submit" , handleRoomSubmit );
설명 : submit 발생시 handleRoomSubmit 함수가 실행됨.
socket.emit 메서드는 이벤트를 발생시키는 역할을 함. (emit = 발생시키다)
socket.emit에 해당하는 이벤트를 설명하자면
enter_room이라는 이름으로 이벤트를 발생시켜 실제값(payload)으로 input값을 담아서 보낸다는 뜻임.
여기서 실제값 = 방이름을 의미한다.
2-6) 이벤트 핸들링 테스트하기
server.js에서 app.js에서 보낸 enter_room에 해당하는 이벤트를 받아서 처리해야함
wsServer에 해당하는 코드를 다음과 같이 고침
wsServer . on ( "connection" , ( socket ) => {
socket . on ( "enter_room" , ( roomName ) => console . log ( roomName ));
});
enter_room 이라는 이름을 가진 이벤트를 받아 로그로 찍어봄.
<결과화면>
여기서 Enter Room을 누르면
값이 잘 전달됨을 알 수 있다.
2-7) app.js 수정하기
서버 쪽에서 실행할 수 있는 콜백 함수를 socket.emit으로 대체할 수 있음.
다음과 같이 코드 수정
const socket = io ();
const welcome = document . getElementById ( "welcome" );
const form = welcome . querySelector ( "form" );
function handleRoomSubmit ( event ){
event . preventDefault ();
const input = form . querySelector ( "input" );
socket . emit ( "enter_room" , input . value , () => {
console . log ( "server is done!" );
});
input . value = "" ;
}
form . addEventListener ( "submit" , handleRoomSubmit );
설명 : 첫 번째 인자로는 이벤트명, 두 번째 인자로는 서버에 전송할 데이터 , 세 번째 인자로 서버에서 호출할 콜백 함수 .
2-8) server.js 수정하기
이벤트가 발생할 때 전달받은 콜백 함수를 서버 쪽에서 호출해 보자.
<전체 코드>
import http from 'http' ;
import SocketIO from "socket.io" ;
import express from 'express' ;
const app = express ();
app . set ( "view engine" , "pug" );
//dirname은 Node.js기본 전역변수로, 현재 실행되는 폴더의 경로를 의미
app . set ( "views" , __dirname + "/views" );
app . use ( "/public" , express . static ( __dirname + "/public" ));
// '/'를 받으면 home으로 가게 설정
app . get ( "/" , ( req , res ) => res . render ( "home" ));
//만약홈이 아닌 다른 주소로 get요청을 보내더라도 홈으로 리다이렉션하게 예외처리함
app . get ( "/*" , ( req , res ) => res . redirect ( "/" ));
const httpServer = http . createServer ( app );
const wsServer = SocketIO ( httpServer );
wsServer . on ( "connection" , ( socket ) => {
socket . on ( "enter_room" , ( roomName , done ) => {
console . log ( roomName );
setTimeout (() => {
done ();
}, 5000 );
});
});
const handleListen = () => console . log ( "Listening on http://localhost:3000" );
httpServer . listen ( 3000 , handleListen );
설명 : event_room 이벤트 핸들러에 매개변수 done을 추가함. (콜백 함수를 전달받는 역할)
일단 전달된 메시지를 콘솔에 출력한 다음 setTimeout 메서드를 통해서 5초 뒤에 done 호출한다는 뜻.
→ 여기에서 중요한 사실은, done에 전달된 콜백 함수를 호출하는 것은 서버지만, 콜백 함수가 정의된 곳은 프론트앤드(app.js)라는 점이다. 따라서 enter_room 이벤트가 발생하고 나면 5초 후에 app.js에서 콜백함수가 동작하게 됨.
<테스트 결과 화면>
브라우저에서 방 이름을 등록하면 5초 후에 브라우저 콘솔창에 server is done! 이 출력되어야 함.
잘 출력됨을 확인할 수 있다.
server.js의 done이라는 매개체는 5초 후에 app.js의
() => {
console . log ( "server is done!" );
를 실행한다는 매커니즘을 이해하자!
3. 채팅룸 만들기
현재 접속한 모든 사용자가 서로 대화를 나눌 필요가 없음.
우리는 서로 소통할 수 있는 웹소켓 그룹이 필요함.
socket.io는 채팅룸 서비스를 제공할 때 사용할 만한 유용한 기능들을 제공하는데, 룸 단위로 묶는 기능도 제공함.
3-1) server.js 수정하기
wsServer . on ( "connection" , ( socket ) => {
socket . on ( "enter_room" , ( roomName , done ) => {
console . log ( roomName );
socket . join ( roomName );
});
});
단순히 join 메서드만 이용하면 쉽게 룸단위로 접근 가능함.
why?)
socket.io에서 서버에 연결된 개별 소켓은 다양한 속성을 포함하고 있음.
콘솔을 찍어보며 채팅룸 기능이 어떻게 동작하는지 확인해보자
wsServer . on ( "connection" , ( socket ) => {
socket . on ( "enter_room" , ( roomName , done ) => {
console . log ( roomName );
console . log ( socket . id );
console . log ( socket . rooms );
socket . join ( roomName );
console . log ( socket . rooms );
});
});
설명 : id 속성은 해당 소켓 만의 고유한 값. - 여러명이 서버에 접속해도 서로 구별 가능
rooms 속성은 소켓이 현재 어떤 룸에 있는지를 나타내는데 소켓이 접속한 방이 하나가 아닐수도 있다는 점을 인지해야함.
<테스트 해보기>
다음과 같이 입력하면
방 이름이 먼저 출력되고
그 다음으로 id가 출력
그 다음으로 join 전의 출력
그 다음은 join 후의 출력 임을 확인할 수 있다.
여기서 핵심 !
룸에 join하기도 전에 set(1)을 보면 방이 하나 조회되는 것을 볼 수 있다.
어? 방을 만들지도 않았는데 왜 조회되지?
그 이유는 socket.io에서는 join을 이용해 어딘가에 접속하지 않더라도, 개별 소켓은 서버에서 제공하는 개인 공간에 들어가 있는 상태이다. 이러한 개인 공간은 소켓과 서버 사이에 형성된 채팅룸이라고 할 수 있음 → private room(프라이빗룸)
결론 : 맨 처음 접속할 때, 프라이빗 룸에만 머무르던 소켓이 join을 이용하면 다른 소켓과 그룹을 형성해 채팅룸을 만들 수 있는데 , join에는 룸 이름이 전달된다. 전달된 이름이 만약 서버에 존재하면 소켓은 그 방에 합류하고, 서버에 존재하지 않으면 방이 새롭게 만들어지고 그 방에 합류하게 된다.
3-2) home.pug 수정하기