반응형
해당 글에서는 Socket.io 기반의 Node Server 내에서 Room, Namespace 별로 분리된 공간에서 소켓 통신을 수행하는 방법에 대해 알아봅니다.
💡 [참고] 아래의 Socket.io와 관련된 글들을 참고하시면 도움이 됩니다
분류 | 링크 |
Socket.io 기반 소켓 서버 구축 방법 -1 : 구성 요소 및 흐름 + React 기반 채팅 화면 구성 | https://adjh54.tistory.com/548 |
Socket.io 기반 소켓 서버 구축 방법 -2 : Room, Namespace 별 소켓 통신 방법 | https://adjh54.tistory.com/549 |
Socket.io 기반 소켓 서버 구축 방법 -3: PM2 클러스터링 + Redis 기반 분산 처리 | https://adjh54.tistory.com/567 |
PM2(Process Manager 2) 이해하고 주요 특징 알아보기 | https://adjh54.tistory.com/551 |
Github : Socket.io 기반 웹 소켓 소스코드 | https://github.com/adjh54ir/blog-codes/tree/main/node-wss-server |
Github : Socket.io를 이용하는 웹 클라이언트 소스코드(채팅) | https://github.com/adjh54ir/blog-codes/tree/main/react-chatting |
1) Socket.io
💡 Socket.io
- 서버-클라이언트 간의 실시간 양방향 통신을 가능하게 하는 라이브러리입니다. 주로 웹 애플리케이션에서 클라이언트와 서버 간의 실시간 데이터 전송을 위해 사용이 됩니다.
- 이는 WebSocket 프로토콜을 사용하지만, 브라우저 호환성을 위해 필요한 경우에는 풀백(fallback) 메커니즘을 사용합니다.
[ 더 알아보기 ]
💡 풀백 메커니즘
- 주어진 기술이나 프로토콜이 작동하지 않을 경우, 대체할 수 있는 다른 방법을 사용하는 것을 의미합니다.
- 예를 들어, Socket.io는 WebSocket을 기본적으로 사용하지만, 만약 클라이언트의 브라우저가 WebSocket을 지원하지 않는다면, HTTP 롱 폴링 등 다른 통신 방식을 사용하여 여전히 실시간 통신을 가능하게 합니다.
2) Socket.io 주요 구성 요소
1. Room
💡 Room
- Socket.io에서 클라이언트들을 그룹으로 묶는 기능을 제공합니다.
- 클라이언트는 여러 Room에 참여할 수 있으며, 서버는 특정 Room에 속한 클라이언트들에게만 메시지를 전송할 수 있습니다.
- 이를 통해 메시지를 특정 그룹의 클라이언트에게만 효율적으로 전달할 수 있습니다.
메서드 | 상세 분류 | 설명 | 예시 |
join(room) | 룸 가입 | 소켓을 특정 방에 가입시킵니다. | socket.join('room1'); |
leave(room) | 룸 나가기 | 소켓을 특정 방에서 탈퇴시킵니다. | socket.leave('room1'); |
to(room).emit(event, data) | 룸 이벤트 전송 | 특정 룸(Room)에 속한 클라이언트들에게 이벤트와 데이터를 전송하는 데 사용됩니다. | io.to('room1').emit('message', 'Hello Room!'); |
💡 Room 개념 예시
- 소켓 서버와 클라이언트 A, B, C 간의 관계에서 사용됩니다.
1. 클라이언트 A와 B는 소켓이 연결된 이후 "myroom"이라는 Room에 참여합니다(Join). 따라서 클라이언트 A와 B는 "myroom"이라는 그룹에 속하게 됩니다.
2. 이제 서버에서 "myroom" 그룹으로 특정 메시지 이벤트와 메시지를 전송합니다.
3. 해당 "myroom"에 속한 클라이언트 A와 B에게 메시지가 ‘일괄적’으로 전송됩니다.
io.on("connection", socket => {
// 클라이언트를 특정 Room에 참여시키기
socket.join('myroom');
}
// 특정 Room에 있는 모든 클라이언트(클라이언트 A, B)에게 메시지 전송
io.to('myroom').emit('message', 'Hello My Room');
// or
// 여러 room1, room2, room3에 속해 있는 클라이언트 들에게 모두 메시지 전송
io.to("room1").to("room2").to("room3").emit("some event");
💡 Room 방 나가기 예시
- 소켓 서버와 클라이언트 A, B, C 간의 관계에서 사용됩니다. 이전 관계에서는 클라이언트 A, B가 myroom이라는 그룹에 소속되어 있었고, 클라이언트 A는 해당 그룹을 나가게 됩니다.
1. 기존의 클라이언트 A와 B는 “myroom”이라는 Room에 참여했었습니다. 그런데, 클라이언트 A가 그룹을 나갑니다.
2. 서버는 “myroom” 그룹으로 특정 메시지와 이벤트를 전송합니다.
3. 해당 “myroom” 그룹에 속한 클라이언트 B에게만 메시지가 전송됩니다.
2. Namespace
💡 Namespace
- Socket.io에서 기본 네임스페이스(/) 외에 추가적인 네임스페이스를 생성하여 클라이언트 연결을 분리하는 기능을 제공합니다. 이를 통해 서로 다른 네임스페이스 간에 독립적인 이벤트 처리가 가능합니다.
- 예를 들어, 채팅 애플리케이션에서 일반 채팅과 관리자 채팅을 분리할 때 이용합니다. 또는 게임에서 로비와 게임 플레이 구역을 분리할 때 이용됩니다.
메서드 | 상세 분류 | 설명 | 예시 |
of(namespace) | 네임스페이스 가입 | 특정 네임스페이스를 지정하여 이벤트를 수신하거나 전송합니다. | const nsp = io.of('/mynamespace'); |
of(namespace).on(event, callback) | 네임스페이스 이벤트 수신 | 특정 네임스페이스에서 이벤트를 수신하여 처리하는 이벤트 리스너를 설정합니다. | io().of(”mynamespace”).on('message', (data) => { console.log(data); }); |
of(namespace).emit(event, data) | 네임스페이스 이벤트 전송 | 특정 네임스페이스로 이벤트와 데이터를 전송합니다. | io().of(”mynamespace”).emit('message', 'Hello World!'); |
💡 Socket.io 네임스페이스
- “admin”이라는 네임스페이스를 생성하고, 해당 네임 스페이스에 이벤트 리스너를 설정하는 예시입니다
1. 기존 default namespace가 아닌 별도 구성한 “admin”이라는 네임스페이스로 연결을 합니다.
2. 연결이 완료되어 발생하는 소켓을 기반으로 message라는 이벤트를 등록(수신) 설정합니다.
3. 클라이언트로부터 “admin” 네임스페이스의 “message” 이벤트가 발생하면 해당 이벤트에 데이터 로그가 출력됩니다.
// 새로운 네임스페이스 생성
const adminNamespace = io.of('/admin');
// 네임스페이스 내에서 이벤트 리스너 설정
adminNamespace.on('connection', (socket) => {
console.log('Admin connected');
socket.on('message', (data) => {
console.log('Admin message:', data);
});
});
3) 환경 구성하기
1. 사용 라이브러리
분류 | 라이브러리 | 버전 | 설명 |
공통 | Node | 21.7.1 | JavaScript 런타임 환경 |
Node 소켓 서버 | npm | 10.5.0 | Node.js의 패키지 관리자 |
React 클라이언트 | socket.io-client | 4.7.5 | Socket.IO 클라이언트 라이브러리 |
Node 소켓 서버 | socket.io | 4.7.5 | 실시간 양방향 통신 라이브러리 |
Node 소켓 서버 | cors | 2.8.5 | Cross-Origin Resource Sharing 처리 미들웨어 |
Node 소켓 서버 | express | 4.19.2 | 웹 애플리케이션 프레임워크 |
2. 초기 환경 구성
💡 초기 환경 구성
- 아래의 구성한 환경을 기반으로 구성하였습니다.
4) Room 구성 및 결과 확인
💡 Room 구성 및 결과 확인
- 기존 소켓 서버에 연결된 모든 사용자들에게 메시지가 전달되는 점이 있었습니다.
- room을 이용하여서 특정 방에 입장한 사용자 들간에 메시지를 주고받을 수 있도록 구성합니다.
1. Node Socket Server : roomApp.js
💡 Node Socket Server
1. WebSocket Server 설정 : port, cors 지정
- 해당 서버로 접속할 포트를 지정하고, cors()를 통해서 다른 도메인에서의 모든 요청에 대해서 허용합니다.
2. WebSocket Server 설정 값 및 socket.io 구성
- HTTP 서버를 생성하고 지정된 포트로 접근 가능하도록 설정합니다.
- 생성된 HTTP 서버를 기반으로 소켓 서버를 설정하고 socket.io에 대한 cors에 대해 모두 허용을 설정합니다.
3. 루트 엔드포인트로 접속 시 health Check 수행
- 루트 경로(”/”)로 접근 시 “Hello world!” 메시지를 출력하고 로그를 출력합니다.
4. 소켓 연결 시작
- io.on("connection") : 클라이언트가 소켓 서버에 연결되었을 때 실행됩니다.
- socket.on("message") : "message" 이벤트 발생 시, "message" 이벤트를 등록한 소켓에게 메시지 전달합니다.
- socket.on(joinRoom-${i}) : "joinRoom-x" 이벤트 발생 시, "room-x"라는 방에 참가시키고 socket.room 속성에 방 이름 저장
- socket.on(leaveRoom-${i}): "leaveRoom-x" 이벤트 발생 시, "room-x"라는 방에 나갑니다.
- socket.on("roomMessage") : "roomMessage" 이벤트 발생 시, socket.room 속성의 특정 방에 "roomMessage" 이벤트를 저장한 소켓에게 전달
- socket.on("disconnect") : "disconnect" 이벤트 발생 시, 소켓 연결을 종료 및 방을 나갑니다.
// import library
const app = require("express")(); // 외부 라이브러리 사용
const cors = require("cors"); // 외부 라이브러리 사용
const http = require("http"); // Node 제공 모듈 사용
/*
* 1. WebSocket Server 설정 : port, cors 지정
*/
const port = 5001;
app.set("port", port); // 웹 소켓 포트를 지정합니다
app.use(cors({ origin: "*" }));
// cors를 지정해줍니다.
/*
* 2. WebSocket Server 설정값 및 socket.io 구성
*/
const server = http.createServer(app).listen(port); // 설정 값에 따르는 서버 정보 구축
const io = require("socket.io")(server, {
cors: {
origin: "*",
methods: ["*"], // 모든 메서드 허용
},
}); // 설정 값에 따라 socket.io 구축
/*
* 3. 루트 엔드포인트로 접속 시 health Check 수행
*/
app.get("/", (req, res, next) => {
res.send("Hello world!"); // 화면상으로 출력
console.log("====== Health Check ======="); // 로그 출력
});
/*
* 4. 소켓 연결 시작
*/
io.on("connection", (socket) => {
console.log("a user connected");
/**
* [ALL] "message" 이벤트 발생 시, "message" 이벤트를 등록한 소켓에게 메시지 전달합니다.
*/
socket.on("message", (msg) => {
io.emit("message", msg);
});
/**
* [임의구성] Socket 10개의 방 생성합니다.
*/
for (let i = 1; i <= 10; i++) {
const roomName = `room-${i}`;
/**
* [ALL] "joinRoom-x" 이벤트 발생 시, "room-x"라는 방에 참가시키고 socket.room 속성에 방 이름 저장합니다.
*/
socket.on(`joinRoom-${i}`, () => {
console.log(`User joined ${roomName}`);
socket.join(roomName); // 방 참가
socket.room = roomName; // socket 정보 중 room 속성에 참가한 방 이름 지정
});
//
/**
* [ALL] "leaveRoom-x" 이벤트 발생 시, "room-x"라는 방에 나갑니다.
*/
socket.on(`leaveRoom-${i}`, () => {
console.log(`User leaved ${roomName}`);
socket.leave(roomName);
});
}
/**
* [ANY ROOM] "roomMessage" 이벤트 발생 시, socket.room 속성의 특정 방에 "roomMessage" 이벤트를 저장한 소켓에게 전달합니다.
*/
socket.on("roomMessage", (msg) => {
console.log(`${socket.room} 번 방에 전달된 메시지 :`, msg);
io.to(socket.room).emit("roomMessage", msg);
});
/**
* [ALL] "disconnect" 이벤트 발생 시, 소켓 연결을 종료 및 방을 나갑니다.
*/
socket.on("disconnect", () => {
console.log("a user disconnected");
socket.disconnect();
socket.leave(socket.room);
});
});
2. React 클라이언트 : SocketIoRoomComponet.tsx
💡 React 클라이언트
- socket.io-client를 기반으로 Socket에 접속하며, 특정 room에 참가하고 room 내에서 메시지를 주고받으며, 작업이 완료되면 방을 떠납니다.
1. socket.io-client를 통해서 소켓 서버에 연결합니다.
- 사전에 구성한 webScocket 서버를 연결합니다. (* cors 도메인에 대한 허용 했기에 접근이 가능합니다.)
2. 상태관리
- message라는 입력하는 메시지 값과 messages라는 누적되는 메시지를 관리하는 상태관리 state를 선언하였습니다.
- 또한, 해당 컴포넌트 내에서 전역으로 사용하기 위한 방에 대한 번호 및 입장여부를 관리하는 state를 선언하였습니다.
3. enterRoom() : 방 입장
- [Socket] "joinRoom-x"로 이벤트를 발생시키며, 특정 방에 참가합니다.
- [Socket] "roomMessage" 이벤트 발생 시, 값을 수신하고 상태관리에 저장합니다.
(* 해당 부분에서 서버에서는 특정 room 간에서만 메시지 전달이 가능하도록 구성이 되어있습니다)
4. leaveRoom() : 방 나가기
- [Socket] "leaveRoom-x" 이벤트를 발생시키며, 특정 방에 나갑니다.
- [Socket] "roomMessage" 이벤트의 메시지 수신을 종료합니다.
5. sendMessage() : 소켓을 통한 메시지 전송
- ‘send’ 버튼을 눌렀을 때, 상대방에게 소켓을 통한 메시지 전송을 하는 메서드입니다.
- [Socket] "roomMessage" 이벤트를 발생시키며, 소켓 연결이 되어있는 관계에서 값을 전달합니다.
import React, { useEffect, useState } from 'react';
import io from 'socket.io-client';
/**
* Socket.io의 구성요소 중 Room을 이용한 채팅
* @returns
*/
const socket = io('<http://localhost:5001>'); // socket.io-client를 통해서 소켓 서버에 연결합니다.
const SocketIoRoomComponet = () => {
const roomArr = Array.from({ length: 10 }, (_, i) => i + 1); // 방 개수 구성
// 상태 관리
const [message, setMessage] = useState<string>(''); // 입력되는 메시지
const [messages, setMessages] = useState<{ id: string; text: string }[]>([]); // 누적되는 메시지 관리
// 방 정보를 관리합니다.
const [roomInfo, setRoomInfo] = useState<{
isEnter: boolean;
nowRoomNum: number;
}>({ isEnter: false, nowRoomNum: 0 });
useEffect(() => {
return () => {
socket.off('roomMessage'); // [Socket] "roomMessage" 이벤트의 메시지 수신을 종료합니다.
};
}, []);
/**
* 방 입장
* @param choiceRoomNum
* @return void
*/
const enterRoom = (choiceRoomNum: number): void => {
console.log(`${choiceRoomNum} 채팅방을 입장하셨습니다.`);
setRoomInfo({ isEnter: true, nowRoomNum: choiceRoomNum });
socket.emit(`joinRoom-${choiceRoomNum}`); // [Socket] "joinRoom-x"로 이벤트를 발생시키며, 특정 방에 참가합니다.
// [Socket] "roomMessage" 이벤트 발생 시, 값을 수신하고 상태관리에 저장합니다.
socket.on('roomMessage', (message: { id: string; text: string }) => {
console.log('메시지를 수신합니다.', message);
setMessages((prevMessages) => [...prevMessages, message]);
});
};
/**
* 방 나가기
* @return void
*/
const leaveRoom = (): void => {
console.log(`${roomInfo.nowRoomNum} 채팅방에 퇴장하였습니다..`);
socket.emit(`leaveRoom-${roomInfo.nowRoomNum}`); // [Socket] "leaveRoom-x" 이벤트를 발생시키며, 특정 방에 나갑니다.
socket.off('roomMessage'); // [Socket] "roomMessage" 이벤트의 메시지 수신을 종료합니다.
setRoomInfo({ isEnter: false, nowRoomNum: 0 });
setMessages([]);
};
/**
* 소켓을 통한 메시지 전송
* @param {React.FormEvent} e
* @return void
*/
const sendMessage = (e: React.FormEvent): void => {
e.preventDefault();
const newMessage = { id: socket.id, text: message };
socket.emit('roomMessage', newMessage); // [Socket] "roomMessage" 이벤트를 발생시키며, 값을 전달합니다.
setMessage(''); // 전송 이후 입력한 텍스트를 지워줍니다.
};
return (
<>
{roomInfo?.isEnter === false ? (
<div
style={{
flex: 1,
flexDirection: 'column',
}}>
<h2 style={{ width: '100%', textAlign: 'center' }}>
Socket.io의 Websocket 채팅방에 오신것을 환영합니다.
</h2>
<div
style={{
marginRight: 20,
marginTop: 60,
flex: 1,
flexDirection: 'row',
flexWrap: 'wrap',
}}>
{roomArr.map((item, idx) => {
return (
<button
key={`room-${idx}`}
style={{
height: 100,
marginRight: 20,
marginBottom: 20,
width: 300,
}}
onClick={() => enterRoom(item)}>
{item} 번 채팅방
</button>
);
})}
</div>
</div>
) : (
<div>
<div
style={{
marginLeft: 200,
flex: 1,
flexDirection: 'column',
}}>
<h2 style={{ marginLeft: 320 }}>{roomInfo?.nowRoomNum} 번 채팅방</h2>
</div>
<h1>Chat</h1>
<div
style={{
width: 500,
height: 300,
backgroundColor: '#f5d682',
border: '1px solid red',
}}>
<div style={{ flex: 1, flexDirection: 'column' }}>
{messages.map((item, index) => (
<div key={index}>
<strong>{item.id === socket.id ? 'You' : item.id}:</strong> {item.text}
</div>
))}
</div>
</div>
<form onSubmit={sendMessage}>
<input
type='text'
value={message}
onChange={(e) => setMessage(e.target.value)}
/>
<button type='submit'>Send</button>
</form>
<div style={{ marginTop: 200 }}>
<button onClick={leaveRoom}>방 나가기 </button>
</div>
</div>
)}
</>
);
};
export default SocketIoRoomComponet;
3. 결과 화면 : 최초 화면 출력 및 방 입장
💡 결과 화면 : 최초 화면 출력 및 방 입장
- 최초 해당 화면에 접근하고 “8번 채팅방”을 선택하여서 입장하였습니다.
4. 결과 화면: 동일한 채팅방
💡 결과 화면: 동일한 채팅방
- 동일한 방에서 서로 간의 채팅이 잘됨을 확인하였습니다.
5. 결과 화면: 서로 다른 채팅방
💡 결과 화면: 서로 다른 채팅방
- 왼쪽의 브라우저는 8번 채팅방, 오른쪽 브라우저는 1번 채팅방에 접속이 되어 있을 때, 서로 다른 room에 대해서 소켓 통신이 안 되는 분리된 공간으로 됨을 확인하였습니다.
5) Namespace 환경 구성 및 결과 확인
💡 Namespace 환경 구성 및 결과 확인
- 기존 소켓 서버에 연결된 모든 사용자들에게 메시지가 전달되는 점이 있었습니다.
- namespace 내에서는 이러한 소켓 공간 자체를 분리하여서 별도의 소켓으로 구성하여서 각각을 관리할 수 있습니다.
1. Node Socket Server: namespaceApp.js
💡 Node Socket Server: namespaceApp.js
1. WebSocket Server 설정 : port, cors 지정
- 해당 서버로 접속할 포트를 지정하고, cors()를 통해서 다른 도메인에서의 모든 요청에 대해서 허용합니다.
2. WebSocket Server 설정 값 및 socket.io 구성
- HTTP 서버를 생성하고 지정된 포트로 접근 가능하도록 설정합니다.
- 생성된 HTTP 서버를 기반으로 소켓 서버를 설정하고 socket.io에 대한 cors에 대해 모두 허용을 설정합니다.
3. 루트 엔드포인트로 접속 시 health Check 수행
- 루트 경로(”/”)로 접근 시 “Hello world!” 메시지를 출력하고 로그를 출력합니다.
4. “일반 사용자” Namespace 소켓 연결 시작 : io.of("/normal");
- socket.on("event:message") : normal namespace 내에서 "event:message" 이벤트 발생 시, namespace에서 "message"를 수신하는 사용자에게 메시지를 전달합니다.
- socket.on(”disconnection”) : "disconnect" 이벤트 발생 시, 소켓 연결을 종료합니다.
5. "관리자" Namespace 소켓 연결 시작 : io.of("/admin");
- socket.on("event:message") : admin namespace 내에서 "event:message" 이벤트 발생 시, namespace에서 "message"를 수신하는 사용자에게 메시지를 전달합니다.
- socket.on(”disconnection”) : "disconnect" 이벤트 발생 시, 소켓 연결을 종료합니다.
// import library
const app = require("express")(); // 외부 라이브러리 사용
const cors = require("cors"); // 외부 라이브러리 사용
const http = require("http"); // Node 제공 모듈 사용
/*
* 1. WebSocket Server 설정 : port, cors 지정
*/
const port = 5001;
app.set("port", port); // 웹 소켓 포트를 지정합니다
app.use(cors({ origin: "*" }));
// cors를 지정해줍니다.
/*
* 2. WebSocket Server 설정값 및 socket.io 구성
*/
const server = http.createServer(app).listen(port); // 설정 값에 따르는 서버 정보 구축
const io = require("socket.io")(server, {
cors: {
origin: "*",
methods: ["*"], // 모든 메서드 허용
},
}); // 설정 값에 따라 socket.io 구축
/*
* 3. 루트 엔드포인트로 접속 시 health Check 수행
*/
app.get("/", (req, res, next) => {
res.send("Hello world!"); // 화면상으로 출력
console.log("====== Health Check ======="); // 로그 출력
});
/*
* 4. "일반 사용자" Namespace 소켓 연결 시작
*/
const nnsp = io.of("/normal");
nnsp.on("connection", (socket) => {
console.log("[+] 일반 사용자가 소켓 서버에 연결되었습니다.");
/**
* normal namespace 내에서 "event:message" 이벤트 발생 시, namespace에서 "message"를 수신하는 사용자에게 메시지를 전달합니다.
*/
socket.on("event:message", (msg) => {
console.log("메시지가 도착했습니다 ", msg);
nnsp.emit("event:message", msg);
});
/**
* "disconnect" 이벤트 발생 시, 소켓 연결을 종료합니다.
*/
socket.on("disconnect", () => {
console.log("a user disconnected");
socket.disconnect();
});
});
/*
* 5. "관리자" Namespace 소켓 연결 시작
*/
const nsp = io.of("/admin");
nsp.on("connection", (socket) => {
console.log("[+] 관리자가 소켓 서버에 연결되었습니다.");
/**
* admin namespace 내에서 "event:message" 이벤트 발생 시, namespace에서 "message"를 수신하는 사용자에게 메시지를 전달합니다.
*/
socket.on("event:message", (msg) => {
nsp.emit("event:message", msg);
});
/**
* "disconnect" 이벤트 발생 시, 소켓 연결을 종료합니다
*/
socket.on("disconnect", () => {
console.log("a user disconnected");
socket.disconnect();
});
});
2. React 클라이언트 : SocketIoNamespaceComponent.tsx
💡 React 클라이언트 : SocketIoNamespaceComponent.tsx
- socket.io-client를 기반으로 분리된 Namespace를 기반으로 접속하며, 각각 분리된 Namespace에 접근하여 메시지를 주고받고, 작업이 완료되며 Namespace를 나갑니다.
1. socket.io-client를 통해서 소켓 서버에 연결합니다.
- socketNormal : webSocket 서버에 /normal을 붙인 namespace를 가지는 소켓에 접근합니다.** (* cors 도메인에 대한 허용 했기에 접근이 가능합니다.)
- socketAdmin : webSocket 서버에 /admin을 붙인 namespace를 가지는 소켓에 접근합니다.
2. 상태관리
- message라는 입력하는 메시지 값과 messages라는 누적되는 메시지를 관리하는 상태관리 state를 선언하였습니다.
- 또한, 해당 컴포넌트 내에서 전역으로 사용하기 위한 name에 대한 구분 타입 및 입장여부 등을 관리하는 state를 선언하였습니다.
3. enterNamespace() : 네임스페이스 연결
- 파라미터에 따라서 일반 사용자 채팅방/ 관리자 채팅방으로 구분이 되며, 이에 대해 접근합니다.
- [CASE1] "일반 사용자" 채팅방으로 접속하는 경우 : "normal" namespace 내에 "event:message"가 발생하였을 때, State의 상태를 갱신합니다.
- [CASE2] "관리자" 채팅방으로 접속하는 경우 : "admin" namespace 내에 "event:message"가 발생하였을 때, State의 상태를 갱신합니다.
4. leaveNamespace() : 네임스페이스 나가기
- 네임스페이스 연결 시에 지정해 둔 state 값에 따라서 일반 사용자/ 관리자로 구분을 하여 이벤트 리스너 연결을 제거합니다.
- "normal" namespace 내에 "event:message" 이벤트 리스너를 제거합니다.
- "admin" namespace 내에 "event:message" 이벤트 리스너를 제거합니다.
5. sendMessage() : 소켓을 통한 메시지 전송
- ‘send’ 버튼을 눌렀을 때, 상대방에게 namespace에 따라 각각 공간에서 메시지 전송을 합니다.
- 또한, 네임스페이스 연결 시에 지정해 둔 state 값에 따라 일반 사용자/관리자로 구분하여 이벤트를 발생시킵니다.
import React, { useEffect, useState } from 'react';
import { io, Socket } from 'socket.io-client';
const socketNormal = io('<http://localhost:5001/normal>'); // socket.io-client를 통해서 "normal" 네임스페이스의 소켓 서버에 연결합니다.
const socketAdmin = io('<http://localhost:5001/admin>'); // socket.io-client를 통해서 "admin" 네임스페이스의 소켓 서버에 연결합니다.
/**
* Namespace를 기반으로 일반 사용자 채팅방과 관리자 채팅방을 분리합니다.
* @returns
*/
const SocketIoNameSpaceComponent = () => {
// 상태 관리
const [message, setMessage] = useState<string>(''); // 입력되는 메시지
const [messages, setMessages] = useState<{ id: string; text: string }[]>([]); // 누적되는 메시지 관리
// 방 정보를 관리합니다.
const [namespaceInfo, setNamespaceInfo] = useState<{
isEnter: boolean;
name: '일반 사용자' | '관리자';
type: 'normal' | 'admin';
socket: Socket;
}>({
isEnter: false,
name: '일반 사용자',
type: 'normal',
socket: socketNormal,
});
useEffect(() => {
return () => {
socketNormal.off('event:message'); // [Socket] "event:message" 이벤트의 메시지 수신을 종료합니다.
socketNormal.off('event:message'); // [Socket] "event:message" 이벤트의 메시지 수신을 종료합니다.
};
}, []);
/**
* namespace에 입장하고 이벤트를 등록합니다.
* @param type
*/
const enterNamespace = (type: 'normal' | 'admin') => {
const typeTxt = type === 'normal' ? '일반 사용자' : '관리자';
console.log(`${typeTxt} 사용자가 입장하였습니다.`);
// namespace를 관리하는 상태를 변경합니다.
setNamespaceInfo({
isEnter: true,
type: type,
name: typeTxt,
socket: type === 'normal' ? socketNormal : socketAdmin,
});
// [CASE1] "일반 사용자" 채팅방으로 접속하는 경우
if (type === 'normal') {
// [Socket] "normal" namespace 내에 "event:message"가 발생하였을때, State의 상태를 갱신합니다.
socketNormal.on('event:message', (message: { id: string; text: string }) => {
setMessages((prevMessages) => [...prevMessages, message]);
});
}
// [CASE2] "관리자" 채팅방으로 접속하는 경우
else if (type === 'admin') {
// [Socket] "admin" namespace 내에 "event:message"가 발생하였을때, State의 상태를 갱신합니다.
socketAdmin.on('event:message', (message: { id: string; text: string }) => {
setMessages((prevMessages) => [...prevMessages, message]);
});
}
};
/**
* namespace를 떠납니다.
* @return void
*
*/
const leaveNamespace = (): void => {
const { type } = namespaceInfo;
if (type === 'normal') {
// [Socket] "normal" namespace 내에 "event:message" 이벤트 리스너를 제거합니다.
socketNormal.off('event:message');
} else if (type === 'admin') {
// [Socket] "admin" namespace 내에 "event:message" 이벤트 리스너를 제거합니다.
socketNormal.off('event:message'); // 모든 이벤트 리스너 제거
}
setNamespaceInfo({
isEnter: false,
name: '일반 사용자',
type: 'normal',
socket: socketNormal,
});
setMessages([]);
};
/**
* 소켓을 통한 메시지 전송
* @param {React.FormEvent} e
* @return void
*/
const sendMessage = (e: React.FormEvent): void => {
e.preventDefault();
const { socket, type } = namespaceInfo;
console.log('보내려는 namespace : ', type);
if (type === 'normal') {
const newMessage = { id: socket.id, text: message };
socket.emit('event:message', newMessage); // [Socket] "normal" namespace 내에" "roomMessage" 이벤트를 발생시키며, 값을 전달합니다.
}
// 관리자인 경우 관리자 소켓에 접속
else if (type === 'admin') {
const newMessage = { id: socket.id, text: message };
socket.emit('event:message', newMessage); // [Socket] "admin" namespace 내에 "roomMessage" 이벤트를 발생시키며, 값을 전달합니다.
}
setMessage(''); // 전송 이후 입력한 텍스트를 지워줍니다.
};
return (
<>
{namespaceInfo?.isEnter === false ? (
<div
style={{
flex: 1,
flexDirection: 'column',
}}>
<h2 style={{ width: '100%', textAlign: 'center' }}>
Socket.io의 Websocket 채팅방에 오신것을 환영합니다.
</h2>
<div
style={{
width: '100%',
textAlign: 'center',
flex: 1,
flexDirection: 'row',
alignContent: 'cneter',
alignItems: 'center',
}}>
<div
style={{
marginRight: 20,
marginTop: 60,
flex: 1,
flexDirection: 'row',
flexWrap: 'wrap',
}}>
<button
style={{
height: 100,
marginRight: 20,
marginBottom: 20,
width: 300,
}}
onClick={() => enterNamespace('normal')}>
일반 사용자 채팅방
</button>
<button
style={{
height: 100,
marginRight: 20,
marginBottom: 20,
width: 300,
}}
onClick={() => enterNamespace('admin')}>
관리자 채팅방
</button>
</div>
</div>
</div>
) : (
<div>
<div
style={{
marginLeft: 200,
flex: 1,
flexDirection: 'column',
}}>
<h2 style={{ marginLeft: 320 }}>{namespaceInfo.name} 채팅방</h2>
</div>
<h1>Chat</h1>
<div
style={{
width: 500,
height: 300,
backgroundColor: '#f5d682',
border: '1px solid red',
}}>
<div style={{ flex: 1, flexDirection: 'column' }}>
{messages.map((item, index) => (
<div key={index}>
<strong>
{item.id === namespaceInfo.socket.id ? 'You' : item.id}:
</strong>{' '}
{item.text}
</div>
))}
</div>
</div>
<form onSubmit={sendMessage}>
<input
type='text'
value={message}
onChange={(e) => setMessage(e.target.value)}
/>
<button type='submit'>Send</button>
</form>
<div style={{ marginTop: 200 }}>
<button onClick={leaveNamespace}>{namespaceInfo.name} 채팅방 나가기 </button>
</div>
</div>
)}
</>
);
};
export default SocketIoNameSpaceComponent;
3. 결과 화면 : 최초 화면 출력 및 네임스페이스 별 입장(일반 사용자/관리자)
💡 결과 화면 : 최초 화면 출력 및 네임스페이스 별 입장(일반 사용자/관리자)
- 최초 화면으로 '일반 사용자 채팅방'과 '관리자 채팅방'으로 두 개를 선택하며, '일반 사용자 채팅방'을 선택하여 채팅방에 입장하였습니다.
4. 결과 화면: 동일한 네임스페이스
💡 결과 화면: 동일한 네임스페이스
- 동일한 네임스페이스 간에 서로 간의 채팅이 잘됨을 확인하였습니다.
5. 결과 화면: 서로 다른 네임스페이스
💡 결과 화면: 서로 다른 네임스페이스
- 왼쪽의 브라우저는 /normal이라는 네임스페이스를 가지고, 오른쪽 브라우저는 /admin이라는 네임스페이스에 접속이 되어 있을 때, 서로 다른 namespace에 대해서 소켓 통신이 안 되는 분리된 공간임을 확인하였습니다.
💡 [참고] 해당 Node 기반 웹 소켓 서버로 이용된 소스코드는 아래의 Repository에서 확인이 가능합니다.
💡 [참고] 해당 React 기반의 웹 소켓 클라이언트로 이용된 소스코드는 아래의 Repository에서 확인이 가능합니다.
오늘도 감사합니다. 😀
반응형
'Node > 이해하기' 카테고리의 다른 글
[Node] Socket.io 기반 소켓 서버 구축 방법 -3: PM2 클러스터링 + Redis 기반 분산 처리 (1) | 2024.09.15 |
---|---|
[Node] PM2(Process Manager 2) 이해하고 주요 특징 알아보기 (0) | 2024.09.01 |
[Node] Socket.io 기반 소켓 서버 구축 방법 -1 : 구성 요소 및 흐름 + React 기반 채팅 화면 구성 (0) | 2024.08.18 |
[Node] Node 버전 상황에 따라 변경 방법 : 라이브러리 n 활용 (0) | 2024.04.22 |
[Node] 자바스크립트 패키지 매니져(npm/yarn) 이해하기 -1 (2) | 2022.06.26 |