...
Namespace 개념
네임스페이스란 Express의 라우팅처럼 url에 지정된 위치에 따라 신호의 처리를 다르게 하는 기술이다.
서버와 클라이언트가 연결되면 실시간 데이터 공유가 가능한데, socket을 그냥 사용하면 데이터가 모든 socket으로 들어가게 된다.
하지만 특정 페이지에서 소켓이 보내주는 모든 실시간 메세지를 모두 받을 필요는 없다. 불필요하며 낭비이기 때문이다.
그래서 특정 노드끼리만 연결해주는 것이 namespace 이다.
지금까지는 기본 네임스페이스인 / 에 신호를 전송하고 수신했지만, 다른 네임스페이스를 만들어서 신호를 각기 독립적으로 처리할 수도 있는 것이다.
즉, 지정한 Namespace에 있는 소켓 끼리만 통신 한다는 개념이다.
Namespace 단어 말 그대로 이름이 붙은 공간이며, 소켓을 묶어주는 단위라고 생각하면 된다.
Namespace 예제 코드
[서버]
// 네임스페이스 등록
const room = io.of('/room');
const chat = io.of('/chat');
// room 네임스페이스 전용 이벤트
room.on('connection', (socket) => {
console.log('room 네임스페이스에 접속');
socket.on('disconnect', () => {
console.log('room 네임스페이스 접속 해제');
});
socket.emit('newroom', '방 만들어'); // 같은 room 네임스페이스 소켓으로만 이벤트가 날라간다.
});
// chat 네임스페이스 전용 이벤트
chat.on('connection', (socket) => {
console.log('chat 네임스페이스에 접속');
socket.on('disconnect', () => {
console.log('chat 네임스페이스 접속 해제');
socket.leave(roomId);
});
socket.emit('join', '참여') // 같은 chat 네임스페이스 소켓으로만 이벤트가 날라간다.
});
> 네임스페이스 설정 io.of('/something')
io.of 를 통해 namespace를 /room과 /chat 으로 나누어 지정해주었다.
네임스페이스의 연결 처리는 제각각이라 연결 콜백(connection)을 따로따로 등록 해준다
이렇게 되면 namespace 객체는 클라이언트에서 /room 혹은 /chat 네임스페이스를 사용하는 소켓과만 통신을 하게 된다.
[클라이언트 1]
<script src="/socket.io/socket.io.js"></script>
<script>
const socket = io.connect('http://localhost:8005/room', { // room 네임스페이스
path: '/socket.io'
});
//* newRoom 이벤트 시 room 네임스페이스 에서만 통신 하게 된다.
socket.on('newRoom', function (data) {
// ...
});
// removeRoom 이벤트 시 room 네임스페이스 에서만 통신 하게 된다.
socket.on('removeRoom', function (data) {
// ...
});
</script>
[클라이언트 2]
<script src="/socket.io/socket.io.js"></script>
<script>
const socket = io.connect('http://localhost:8005/chat', {path: '/socket.io'}); // chat 네임스페이스
//* join 이벤트 시 chat 네임스페이스 에서만 통신 하게 된다.
socket.on('join', function (data) {
// ...
});
//* exit 이벤트 시 chat 네임스페이스 에서만 통신 하게 된다.
socket.on('exit', function (data) {
// ...
});
</script>
클라이언트 코드를 보면 네임스페이스는 url주소로 연결되는 것을 알 수 있다. 그렇게 같은 경로를 가진 서버와 클라이언트 소켓들이 서로 송수신을 하게 되는 것이다.
외부 파일에서 특정 Namespace 에 이벤트 보내기
핵심은 io 객체를 전역 변수 더나아가 전 파일에서 공통으로 사용할 수 있는 public 변수로 만들어주는 것이다.
public 변수로 만들면, 외부 파일에서 만일 io객체에 접근해 사용할 필요가 있다면 간단하게 req.app.get('io') 로 가져와 소켓IO 기능을 마음껏 사용할 수 있는 것이다.
socket.js
const SocketIO = require('socket.io');
const io = SocketIO(server, { path: '/socket.io' });
// path를 지정한 고유한 io객체를 전역으로 등록.
// 전역변수로 등록함으로서, 다른 파일에서 바로 io객체를 가져와 소켓 설정을 할 수 있다.
app.set('io', io);
외부파일.js
//^ req.app.get('io') / res.app.get('io') 를 사용해서 익스프레스 객체로 io를 가져와 사용할 수 있게 된다.
const io = req.app.get('io'); // 전역변수로 등록해논 io객체를 가져옴
io.of('/room').emit('newRoom', "message"); // 특정 room네임스페이스에게만 newRoom 이벤트와 메세지를 보냄
Namespace 종류
Default namespace
Default namespace를 / 라고 부르며, 기본적으로 연결되는 Socket.IO 클라이언트와 서버가 기본적으로 수신하는 클라이언트다.
이 네임 스페이스는 io.sockets 또는 간단히 io로 식별된다.
// 기본 네임스페이스 이벤트 수신
io.on('connection', socket => {
socket.on('disconnect', () => {});
});
// 기본 네임스페이스 이벤트 송신
io.sockets.emit('hi', 'everyone');
io.emit('hi', 'everyone'); // short form
Custom namespaces
사용자 정의 네임 스페이스를 설정하려면 io.of 함수를 사용한다.
const nsp = io.of('/my-namespace');
nsp.on('connection', socket => {
console.log('someone connected');
});
nsp.emit('hi', 'everyone!');
Namespace middleware
모든 Socket에 대해 실행되는 함수로, 소켓과 다음 등록된 미들웨어로 실행을 선택적으로 조절 할 수 있다.
즉, express의 app.use 의 소켓IO 버젼이라고 생각하면 된다.
우리가 이때까지 쓴 express의 미들웨어가 그러했듯이, next() 를 통해 다음 메소드로 넘길수 있으며, 또한 다른 미들웨어를 소켓 메소드 내에 선언해서 사용할 수 있다.
const cookieParser = require('cookie-parser');
const io = SocketIO(server, { path: '/socket.io' });
// default namespace
io.use((socket, next) => {
if (isValid(socket.request)) {
// 외부모듈 미들웨어를 안에다 쓰일수 있다. 미들웨어 확장 원칙에 따라 res, req인자를 준다 (후술)
cookieParser(process.env.COOKIE_SECRET)(socket.request, socket.request.res, next);
next();
} else {
// next(인수)를 하면 바로 똑같이 Handling middleware error로 넘어가게 된다. (후술)
next(new Error('invalid'));
}
});
// custom namespace
io.of('/admin').use(async (socket, next) => {
const user = await fetchUser(socket.handshake.query);
if (user.isAdmin) {
socket.user = user;
next();
} else {
next(new Error('forbidden'));
}
});
Handling middleware error
next 메소드가 Error 객체와 함께 호출되면 클라이언트는 connect_error 이벤트를 수신한다.
express의 에러처리 미들웨어의 소켓IO 버젼 이라고 보면 된다.
// ... next(new Error('inavlid'))
socket.on('connect_error', (err) => {
console.log(err.message); // prints the message associated with the error, e.g. "thou shall not pass" in the example above
});
Compatibility with Express middleware
네임스페이스 미들웨어 안에서 기존 Express 미들웨어 모듈과 함께 호환시켜 사용하려면,
미들웨어 확장 절칙에 따라 (res, req, next) 인자를 주어야 한다.
io.use((socket, next) => {
// 외부모듈 미들웨어를 안에다 쓰일수 있다. 미들웨어 확장 원칙에 따라 res, req인자를 준다 (후술)
cookieParser(process.env.COOKIE_SECRET)(socket.request, socket.request.res, next);
});
공식문서에는 wrap 이라는 helper함수 기법을 제공해주는데, 이 기법을 사용하면 app.use() 와 같이 좀더 깔끔하게 미들웨어들을 배치할 수 있게 된다.
물론 어디까지나 선택적인 방법일 뿐이다.
// 외부 모듈 미들웨어
const session = require('express-session');
const passport = require('passport');
// 미들웨어를 인자로 받아 자동으로 (req, res, next) 인자를 붙여줘서 호출하게 만드는 helper 함수
const wrap = (middleware) => (socket, next) => middleware(socket.request, socket.request.res, next);
// app.use() 와 같이 깔끔하게 미들웨어들을 배치할수 있게 된다.
io.use(wrap(session({ secret: 'cats' })));
io.use(wrap(passport.initialize()));
io.use(wrap(passport.session()));
io.use((socket, next) => {
if (socket.request.user) {
next();
} else {
next(new Error('unauthorized'))
}
});
Room 개념
Room은 Namespace의 하위 개념이다. (IO -> NameSpace -> Room -> Socket )
namespace안에 있는 소켓들을 room으로 쪼개어 나눈 것이다.
예를 들어 카톡 단톡방 두개가 있다고 치면,
단톡방1에 메시지를 보내면 단톡방1의 사용자들은 메시지를 받지만 / 다른 단톡방의 사용자들은 그 메시지를 받지 못한다.
소켓들의 룸도 단톡방과 비슷하다.
특정 룸에 신호를 보내면 룸 안의 소켓들은 신호를 받지만, 다른 룸에 소속된 소켓들은 신호를 받지 못한다.
이를 통해 room에 join되어 있는 클라이언트 만의 데이터 송수신이 가능하게 된다.
즉, 각 클라이언트는 socket을 가지게 되며, 이 socket은 namespace를 가지고, 각 namespace는 room을 가진다.
네임스페이스를 통해 큰 줄기의 데이터 통신을 만들고 룸을 통해 미세하게 소켓을 연결할 수 있는 것이다.
위 코드를 살펴보면 독특하게 join, to, leave 등 친숙한 동사, 전치사 메서드들이 보이는데,
이는 socket.io에서 채팅방의 논리를 미리 메서드로 구현해놓은 것이다.
socket.join()과 leave(), to()는 Socket.IO에서 준비해둔 메서드이다.
req.headers.referer 에는 요청 주소 uri가 들어 있다. 이를 정규식으로 파싱해서 roomID 만 뺴온 후,
socket.join 의 인자로 넣어 특정방에 입장할 수 있다.
그리고 socket.leave 로 방에서 나갈 수 있다.
socket.to 를 통해 특정 룸에게만 이벤트를 emit할 수 있다.
즉, join, leave, to의 개념은, socket.io의 Room(채팅방)을 참가/떠나다/한테 개념으로 보면 된다.
외부파일에서 네임스페이스 안의 룸에 이벤트를 보내기
네임스페이스 포스팅에서 한 것과 같이 똑같이 접근해서, to(rooId) 메소드로 룸ID를 특정해서 보내면 된다.
req.app.get('io').of('/chat').to(roomId).emit('chat', chat);
Namespace & Room 총정리
- IO : 전역
- 네임스페이스 <path>: 전역에서 나눈 공간
- 룸ID : 네임스페이스에서 세분히 나눈 공간
- 소켓ID : 어느 특정 사용자와 소통용
즉, 같은 네임스페이스 안에서 소통 하여, 소켓 송수신을 분리 할 수있고
또 네임스페이스 안의 같은 룸 안에서만 소통 하게 하여, 더 세분하게 소켓 송수신을 분리 할 수 있다.
// 전체 보내기
req.app.get('io').emit('이벤트', 메세지);
// 네임스페이스에 있는 유저한테만 보내기
req.app.get('io').of('네임스페이스').emit('이벤트', 메세지);
// 네임스페이스에 있으면서, 그안에 룸에 있는 유저한테만 보내기
req.app.get('io').of('네임스페이스').to(roomId).emit('이벤트', 메세지);
// 특정 유저한테만 보내기 (1:1대화, 귓속말)
req.app.get('io').to(socket.id).emit('이벤트', 메세지);
// 나를 제외한 모든 유저에게 보내기
req.app.get('io').broadcast.emit('event_name', msg);
# 참고자료
https://berkbach.com/node-js%EC%99%80-socket-io%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%B1%84%ED%8C%85-%EA%B5%AC%ED%98%84-2-ce5ac35bb007
https://fred16157.github.io/node.js/nodejs-socketio-communication-room-and-namespace/
이 글이 좋으셨다면 구독 & 좋아요
여러분의 구독과 좋아요는
저자에게 큰 힘이 됩니다.