[SeSACx코딩온] Socket.IO를 활용한 실시간 채팅
Node.js와 Socket.IO로 실시간 채팅 만들기
1. Socket.IO 의 기본 메서드
1) emit
과 on
emit
: 이벤트를 발생시키는 메서드입니다. 이벤트와 함께 데이터를 전송할 수 있습니다. 즉, 이벤트를 “발신”하는 역할을 합니다.on
: 특정 이벤트가 발생했을 때 실행할 콜백 함수를 등록하는 메서드입니다. 즉, 이벤트를 “수신”하는 역할을 합니다.
2) socket
과 io
socket
: 개별 클라이언트와의 연결을 나타냅니다. 각 클라이언트마다 고유한socket
객체가 생성됩니다.socket
객체는 클라이언트와의 실시간 통신을 관리합니다.io
: 서버 전체를 나타내며, 여러 클라이언트를 관리하는 역할을 합니다.io
객체는 Socket.IO 서버의 인스턴스입니다.
3) socket.emit
, io.emit
, socket.on
, io.on
의 의미
-
socket.emit(event, data)
: 특정 클라이언트에게 이벤트를 발송합니다.socket
객체를 통해 개별 클라이언트에 이벤트와 데이터를 보냅니다.socket.emit('event_name', { key: 'value' });
-
io.emit(event, data)
: 모든 클라이언트에게 이벤트를 발송합니다.io
객체를 통해 서버에 연결된 모든 클라이언트에 이벤트와 데이터를 보냅니다.io.emit('event_name', { key: 'value' });
-
socket.on(event, callback)
: 특정 클라이언트로부터 이벤트를 수신하고, 콜백 함수를 실행합니다. 개별 클라이언트와 관련된 이벤트를 처리합니다.socket.on('event_name', (data) => { console.log(data); });
-
io.on(event, callback)
: 서버에 연결된 모든 클라이언트로부터 이벤트를 수신하고, 콜백 함수를 실행합니다. 서버 전체와 관련된 이벤트를 처리합니다.io.on('connection', (socket) => { console.log('A client connected'); });
2. 초기 설정
1) 프로젝트 폴더 구조
socket.io/
│
├── views/
│ └── client.ejs
├── server.js
└── package.json
2) 필요한 패키지 설치
터미널에서 다음 명령어를 실행하여 필요한 패키지를 설치합니다:
npm install express socket.io ejs
express
: Node.js 웹 애플리케이션 프레임워크입니다.socket.io
: 실시간 양방향 통신을 위한 라이브러리입니다.ejs
: 서버 사이드 템플릿 엔진입니다.
3. 기본 서버 설정
1) 서버 설정 (server.js
)
server.js
const express = require('express'); // Express 모듈 가져옴
const http = require('http'); // node.js 기본 내장 모듈인 HTTP 모듈 가져옴 (HTTP 서버와 클라이언트 기능을 제공)
const { send } = require('process');
// node.js 기본 내장 모듈인 'http' 불러오기.
// 'http' 모듈은 HTTP 서버와 클라이언트 기능을 제공.
const socketIO = require('socket.io');
// 'socket.io' 모듈 불러오기.
// WebSocket 기반, 실시간 양방향 데이터 전송 지원 라이브러리.
const app = express(); // Express 애플리케이션 생성
const server = http.createServer(app); // HTTP 서버 생성
const io = socketIO(server)
// HTTP 서버 'server'를 'socket.io'에 바인딩하여 WebSocket 기능을 추가한 서버를 생성.
// 이러면 클라이언트-서버 간에 실시간 양방향 통신을 할 수 있음.
const nickObjs = {}; // 닉네임을 저장할 객체 생성
app.set('view engine', 'ejs'); // 템플릿 엔진을 EJS로 설정
app.set('views', path.join(__dirname, 'views')); // 뷰 디렉토리를 설정
app.get('/', (req, res) => {
res.render('client'); // 클라이언트 템플릿을 렌더링
});
server.listen(3000, () => {
console.log('서버가 3000번 포트에서 실행 중입니다.'); // 서버가 실행되었음을 알림
});
express
,http
,socket.io
,path
모듈을 가져와서 서버를 설정합니다.nickObjs
객체를 사용하여 닉네임을 저장합니다.- 클라이언트가 접속하면
client.ejs
템플릿을 렌더링합니다.
2) 클라이언트 설정 (client.ejs
)
views/client.ejs
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>socket.io 채팅</title>
<!-- cdn -->
<!-- 서버가 실행 중이지 않아도 클라이언트 라이브러리를 사용 할 수 있음. -->
<!-- <script src="https://cdn.socket.io/4.7.5/socket.io.min.js" integrity="sha384-2huaZvOR9iDzHqslqwpR87isEmrfxqyWOF7hr7BY6KG0+hVKLoEXMPUJw3ynWuhO" crossorigin="anonymous"></script> -->
<!-- Socket.IO 서버가 제공하는 클라이언트 라이브러리 -->
<!-- 서버에서 자동 제공됨. Socket.IO 서버가 실행 중일 때만 작동함. -->
<script src="/socket.io/socket.io.js"></script>
<style>
/* 채팅 UI 스타일 설정 */
.chat-list {
background-color: skyblue;
height: 500px;
padding: 10px;
overflow-y: auto;
}
.chat-list div {
margin-top: 3px;
}
.chat-list div div {
display: inline-block;
padding: 3px;
}
.my-chat {
text-align: right;
}
/* 내 채팅 배경색상 */
.my-chat div {
background-color: yellow;
}
/* 상대 채팅 배경색상 */
.other-chat div {
background-color: white;
}
/* DM 배경색상 */
.secret-chat div {
background-color: pink;
}
.notice {
text-align: center;
color: #333;
font-size: 12px;
}
.d-none {
display: none;
}
</style>
</head>
<body>
<h1>My Chat</h1>
<!-- 닉네임 입력 UI -->
<div class="entry-box">
<input
type="text"
id="nickname"
placeholder="닉네임"
autofocus
onkeypress="if(window.event.keyCode==13){join()}" />
<button type="button" onclick="join()">입장</button>
</div>
<!-- 채팅 UI -->
<main class="chat-box d-none">
<div class="chat-list"></div>
<select id="nick-list"></select>에게
<input
type="text"
id="message"
onkeypress="if(window.event.keyCode==13){send()}"
/>
<button type="button" onclick="send()">전송</button>
</main>
<script>
let socket = io(); // 서버와의 소켓 연결을 생성
let myNick; // 사용자 닉네임을 저장하는 변수
</script>
</body>
</html>
client.ejs
파일은 닉네임 입력과 채팅 메세지를 주고받을 UI입니다.- 상단
script
태그에 Socket.IO 클라이언트 라이브러리가 작성되어 있습니다.
4. 닉네임 설정 기능
1) 서버 설정 (server.js
)
server.js
파일에 클라이언트가 닉네임을 설정할 수 있도록 하는 기능을 추가합니다.
server.js
닉네임 설정 추가
io.on('connection', (socket) => {
console.log('연결됨:', socket.id); // 클라이언트가 연결되었을 때 메세지를 출력
socket.on('setNick', (nick) => {
// nickObjs라는 리스트에서 nick에 해당하는 값이 있으면 0으로 시작하는 값이 출력됨.
if (Object.values(nickObjs).indexOf(nick) > -1) {
socket.emit('error', '이미 사용 중인 닉네임입니다.'); // 닉네임 중복 시 에러 메세지를 보냄
} else {
nickObjs[socket.id] = nick; // 닉네임을 객체에 저장
socket.emit('entrySuccess', nick); // 닉네임 설정 성공 메세지를 보냄
io.emit('notice', `${nick}님이 입장하셨습니다.`); // 다른 클라이언트에게 입장 메세지를 보냄
io.emit('updateNicks', nickObjs); // 닉네임 목록을 업데이트
}
});
socket.on('disconnect', () => {
if (nickObjs[socket.id]) {
io.emit('notice', `${nickObjs[socket.id]}님이 퇴장하셨습니다.`); // 다른 클라이언트에게 퇴장 메세지를 보냄
delete nickObjs[socket.id]; // 닉네임을 객체에서 삭제
io.emit('updateNicks', nickObjs); // 닉네임 목록을 업데이트
}
});
});
socket.on('setNick', (nick) => {...});
함수는 클라이언트로부터 닉네임을 받아 중복 확인 후 설정합니다.socket.on('disconnect', () => {...});
함수는 클라이언트가 연결을 끊었을 때 닉네임을 객체에서 삭제합니다.
2) 클라이언트 설정 (client.ejs
)
client.ejs
파일에 닉네임 설정 기능을 추가합니다.
views/client.ejs
닉네임 설정 추가
<script>
let socket = io(); // 서버와의 소켓 연결을 생성
let myNick; // 사용자 닉네임을 저장하는 변수
// 서버와 연결되었을 때 실행
socket.on('connect', () => {
console.log('클라이언트 연결 완료 ::', socket.id);
});
// 서버로부터 공지 메세지를 수신하여 화면에 표시
socket.on('notice', (msg) => {
console.log(msg);
document.querySelector('.chat-list').insertAdjacentHTML('beforeend', `<div class="notice">${msg}</div>`)
});
// 닉네임을 서버에 설정
function join() {
socket.emit('setNick', document.querySelector('#nickname').value);
}
// 닉네임 중복 시 에러 메세지를 표시
socket.on('error', (data) => {
alert(data);
});
// 닉네임 설정 성공 시 UI를 업데이트
socket.on('entrySuccess', (nick) => {
myNick = nick;
console.log('myNick > ', myNick);
document.querySelector('#nickname').disabled = true;
document.querySelector('.entry-box > button').disabled = true;
document.querySelector('.chat-box').classList.remove('d-none');
});
// 사용자 목록을 업데이트
socket.on('updateNicks', (nickObjs) => {
let options = `<option value="all">전체</option>`;
for (let key in nickObjs) {
if (key !== socket.id) { // 현재 소켓의 ID와 다른 ID만 추가
options += `<option value="${key}">${nickObjs[key]}</option>`;
}
}
document.querySelector('#nick-list').innerHTML = options;
});
</script>
- 클라이언트가 닉네임을 입력하고 입장할 수 있는 UI입니다.
- 닉네임이 설정되면, 서버와 통신하여 닉네임을 저장하고, 중복된 닉네임일 경우 에러 메세지를 표시합니다.
5. 채팅 기능
클라이언트가 메세지를 주고받을 수 있도록 채팅 기능을 추가합니다.
1) 서버 설정 (server.js
)
server.js
파일에 메세지 전송 기능을 추가합니다.
server.js
메세지 전송 추가
socket.on('send', (data) => {
if(data.dm === 'all') {
// "전체" 발송
io.emit('newMessage', {nick : data.myNick, msg: data.msg })
} else {
// "DM" 발송
let dmSocketId = data.dm;
const sendData = {
nick: data.myNick,
msg: data.msg,
dm:'(속닥속닥) ',
}
io.to(dmSocketId).emit('newMessage', sendData) // DM을 보내야하는 타겟(소켓아이디)한테 메세지 전송.
socket.emit('newMessage', sendData);
console.log("sendData >>> ", sendData );
}
});
socket.on('send', (data) => {...});
함수는 클라이언트로부터 메세지를 받아서 전체 또는 특정 클라이언트에게 메세지를 전송합니다.dm:'(속닥속닥) '
에 따라서 개인 메세지은 메세지 제일 앞에(속닥속닥)
이라는 접두사가 추가됩니다.
2) 클라이언트 설정 (client.ejs
)
client.ejs
파일에 메세지 전송 기능을 추가합니다.
views/client.ejs
메세지 전송 추가
<script>
// 메세지를 전송
function send() {
const data = {
dm: document.querySelector('#nick-list').value,
myNick,
msg: document.querySelector('#message').value,
};
socket.emit('send', data);
document.querySelector('#message').value = '';
}
// 새로운 메세지를 수신하여 화면에 표시
socket.on('newMessage', (data) => {
let chatList = document.querySelector('.chat-list');
let div = document.createElement('div');
let divChat = document.createElement('div');
if(myNick === data.nick) {
// data에 있는 nick이 본인 것이라면
// 오른쪽에 표시하기 위해 my-chat 클래스 추가
div.classList.add('my-chat');
} else {
// data에 있는 nick이 본인 것이 아니라면
// 왼쪽에 표시하기 위해 other-chat 클래스 추가
div.classList.add('other-chat');
}
if (data.dm) {
// 만약 data에 dm 이 있다면
// 다른 스타일을 적용하여 표시하기 위해 secret-chat 클래스 추가
div.classList.add('secret-chat');
divChat.textContent = data.dm;
}
divChat.textContent += `${data.nick} : ${data.msg}`;
div.append(divChat);
chatList.append(div);
chatList.scrollTop = chatList.scrollHeight;
});
</script>
send()
함수는 메세지를 전송하는 역할을 합니다.socket.on('newMessage', (data) => {...});
함수는 서버로부터 새로운 메세지를 수신하여 화면에 표시하는 역할을 합니다.
6. 개인 메세지 (DM) 기능
특정 사용자에게 개인 메세지를 보내는 기능을 추가합니다.
1) 클라이언트 설정 (client.ejs
)
views/client.ejs
개인 메세지 추가
<select id="nick-list"></select>에게
<input
type="text"
id="message"
onkeypress="if(window.event.keyCode==13){send()}"
/>
<button type="button" onclick="send()">전송</button>
<select id="nick-list"></select>
: 개인 메세지를 보낼 대상을 선택하는 드롭다운 목록입니다.
2) 서버 설정 (server.js
)
server.js
파일의 메세지 전송 부분에 개인 메세지 기능을 추가합니다.
server.js
개인 메세지 추가
socket.on('send', (data) => {
if(data.dm === 'all') {
// "전체" 발송
io.emit('newMessage', {nick : data.myNick, msg: data.msg })
} else {
// "DM" 발송
let dmSocketId = data.dm;
const sendData = {
nick: data.myNick,
msg: data.msg,
dm:'(속닥속닥) ',
}
io.to(dmSocketId).emit('newMessage', sendData) // DM을 보내야하는 타겟(소켓아이디)한테 메세지 전송.
socket.emit('newMessage', sendData);
console.log("sendData >>> ", sendData );
}
});
socket.on('send', (data) => {...});
함수는 클라이언트로부터 메세지를 받아서 전체 또는 특정 클라이언트에게 메세지를 전송합니다.- 개인 메세지는 선택된 사용자의
socket.id
를 이용하여 특정 클라이언트에게만 메세지를 전송합니다. dm:'(속닥속닥) '
에 따라서 개인 메세지은 메세지 제일 앞에(속닥속닥)
이라는 접두사가 추가됩니다.
참고 사항
- 여기서 중요한 점은 클라이언트에서 자신을 제외한 사용자 목록을 제공한다는 것입니다.
- 즉, 메세지를 보낼 때 자신이 자신에게 메세지를 보낼 수 없도록 되어있습니다.
클라이언트 코드에서 key !== socket.id
조건을 통해 현재 소켓의 ID, 즉 자신의 ID를 제외한 사용자 목록만을 추가합니다.
socket.on('updateNicks', (nickObjs) => {
let options = `<option value="all">전체</option>`;
// 포인트 (자신을 제외한 사용자 목록 표시)
for (let key in nickObjs) {
if (key !== socket.id) { // 현재 소켓의 ID와 다른 ID만 추가
options += `<option value="${key}">${nickObjs[key]}</option>`;
}
}
document.querySelector('#nick-list').innerHTML = options;
});
7. 전체 코드
socket.io/package.json
{
"dependencies": {
"ejs": "^3.1.10",
"express": "^4.19.2",
"socket.io": "^4.7.5"
}
}
socket.io/views/client.ejs
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>socket.io 채팅</title>
<!-- cdn -->
<!-- 서버가 실행 중이지 않아도 클라이언트 라이브러리를 사용 할 수 있음. -->
<!-- <script src="https://cdn.socket.io/4.7.5/socket.io.min.js" integrity="sha384-2huaZvOR9iDzHqslqwpR87isEmrfxqyWOF7hr7BY6KG0+hVKLoEXMPUJw3ynWuhO" crossorigin="anonymous"></script> -->
<!-- Socket.IO 서버가 제공하는 클라이언트 라이브러리 -->
<!-- 서버에서 자동 제공됨. Socket.IO 서버가 실행 중일 때만 작동함. -->
<script src="/socket.io/socket.io.js"></script>
<style>
/* [실습 2] 채팅창 UI 만들기 */
.chat-list {
background-color: skyblue;
height: 500px;
padding: 10px;
overflow-y: auto;
}
.chat-list div {
margin-top: 3px;
}
.chat-list div div {
display: inline-block;
padding: 3px;
}
.my-chat {
text-align: right;
}
.my-chat div{
background-color: yellow;
}
.other-chat div{
background-color: white;
}
/* [실습 3] 채팅창 입장 안내 문구 */
.notice {
text-align: center;
color: #333;
font-size: 12px;
}
.d-none {
display: none;
}
/* [실습 5] DM 기능 추가 */
.secret-chat div {
background-color: pink;
}
</style>
</head>
<body>
<h1>My Chat</h1>
<!-- [실습 1] 각 버튼을 누를 때 마다 서버로 메시지 보내기 -->
<!-- <button onclick="sayHello()">Hello</button>
<button onclick="saySutdy()">Study</button>
<button onclick="sayBye()">Bye</button>
<p id="from-server"></p> -->
<!-- [실습 3-2] socket.id => nickname // 닉네임 입력 폼 -->
<div class="entry-box">
<input
type="text"
id="nickname"
placeholder="닉네임"
autofocus
onkeypress="if(window.event.keyCode==13){join()}" />
<button type="button" onclick="join()">입장</button>
</div>
<!-- (참고) keyCode 아는 방법 -->
<!-- <input type="text" id="myInput"> -->
<!-- [실습 2] 채팅 UI 만들기. -->
<main class="chat-box d-none">
<div class="chat-list">
<!-- 임시 대화 데이터 -->
<!-- <div class="my-chat">
<div>user1 : msg1</div>
</div>
<div class="other-chat">
<div>user2 : msg2</div>
</div> -->
</div>
<!-- 메시지 전송 영역 -->
<select id="nick-list"></select>에게
<input
type="text"
id="message"
onkeypress="if(window.event.keyCode==13){send()}"
/>
<button type="button" onclick="send()">전송</button>
</main>
<script>
// (참고) keycode 아는 방법.
// document.getElementById('myInput').addEventListener('keydown', (e) => {
// console.log('key down >>> ', e.keycode);
// });
let socket = io();
// 소켓 사용을 위한 객체 생성.
let myNick; // 내 닉네임 [실습 3-2-2]
socket.on('connect', () => {
console.log('클라이언트 연결 완료 ::', socket.id);
// console.log(socket);
})
// (참고) 소켓 연결 완료.
// [실습 1]
// function sayHello() {
// socket.emit('hello', {who: 'client', msg: 'hello'})
// }
// socket.on('hellokr', (data) => {
// console.log("data >> ", data); // {who: 'hello', msg: '안녕!!!'}
// document.querySelector('#from-server').textContent = `${data.who} : ${data.msg}`
// })
// [실습 3] 채팅창 입장 안내 문구
socket.on('notice', (msg) => {
console.log(msg);
document.querySelector('.chat-list').insertAdjacentHTML('beforeend', `<div class="notice">${msg}</div>`)
})
// insertAdjacentHTML vs innerHTML
// insertAdjacentHTML : 삽입할 위치를 명시적으로 지정할 수 있음. 특정 위치에 HTML을 추가하려면 <-- 사용
// (beforeend, beforebegin, afterbegin, afterend)
// innerHTML : 요소의 전체 내용을 대체하게 됨. 요소의 내용을 한 번에 대체하거나 가져올 수 있음. 해당 요소의 내용을 변경.
// [실습 3-2] 채팅창 입장 문구 socket.id -> nickname
function join() {
socket.emit('setNick', document.querySelector('#nickname').value);
}
// [실습 3-2-1] 채팅창 입장 문구 socket.id -> nickname
// 닉네임 중복 --> alert 띄우기.
socket.on('error', (data) => {
alert(data);
})
// [실습 3-2-2]
// 입장 성공 : 닉네임 입력창 비활성화
socket.on('entrySuccess', (nick) => {
myNick = nick; // 내 닉네임 저장
console.log('myNick > ', myNick);
document.querySelector('#nickname').disabled = true; // 인풋창 비활성화
document.querySelector('.entry-box > button').disabled = true; // 버튼 비활성화
document.querySelector('.chat-box').classList.remove('d-none'); // 채팅창 보이기
})
// [실습 3-2-3]
// 유저 목록 업데이트 (select 박스의 option 태그 개수 변경)
socket.on('updateNicks', (nickObjs) => {
// console.log("클라이언트 nickObjs >> ", nickObjs); // {PHjAHGtqmmo6oVVKAAAH: 'ㅇㅇㅇ'}
let options = `<option value="all">전체</option>`
// TODO : nickObjs를 반복 돌아서 option 태그에 추가.
// option 태그의 value 속성 값은 socket.id ,
// option 태그의 컨텐츠는 nick 값
for (let key in nickObjs) {
// [추가 실습] 나에게 DM 시 메세지 2번 찍히는 이슈 (option 태그에서 본인을 제외해야함!)
if (key !== socket.id) { // = if (myNick !== nickObjs[key]) {
options += `<option value="${key}">${nickObjs[key]}</option>`
}
}
document.querySelector('#nick-list').innerHTML = options;
})
// [실습 4] 채팅창 메시지 전송
function send() {
// "send" 이벤트 전송 {닉네임, 입력창 내용}
const data = {
// 전체: all, 그 외 다른 닉네임 : socket.id
// 위에 options에서 innerHTML 할 때 value 값을 정해줬음!
dm: document.querySelector('#nick-list').value,
myNick, // 내 닉네임
msg: document.querySelector('#message').value, // 메시지 내용
}
console.log("data > ", data);
socket.emit('send', data);
document.querySelector('#message').value = ''; // input 초기화.
}
// [실습 4-2] 채팅창 메시지 전송
// newMessage 이벤트를 받아서 {닉네임, 입력창 내용}
// 내가 보낸 메시지 -> 오른쪽
// 다른 사람이 메시지 -> 왼쪽
socket.on('newMessage', (data) => {
console.log(" 클라이언트 측 :: newmessage data >> ", data);
// {nick: 'ㄴㅇㄹ', msg: '안녕'}
// "내가 생성해야할 채팅 구조"
// <div class="my-chat">
// <div>user1 : msg1</div>
// </div>
// <div class="other-chat">
// <div>user2 : msg2</div>
// </div>
let chatList = document.querySelector('.chat-list');
let div = document.createElement('div'); // .my-chat or .other-chat
let divChat = document.createElement('div'); // 가장 안쪽 div - 대화 내용
if(myNick === data.nick) {
// 내가 보낸 메시지
div.classList.add('my-chat');
} else {
// 다른 사람이 보낸 메시지
div.classList.add('other-chat');
}
// [실습 5] DM 기능 추가하기.
if (data.dm) {
div.classList.add('secret-chat');
divChat.textContent = data.dm; // '속닥속닥'
}
// [실습 4-2] 실제로 대화내용이 추가되는 부분.
divChat.textContent += `${data.nick} : ${data.msg}`;
div.append(divChat);
chatList.append(div);
// 메세지가 많아져서 스크롤이 생기더라도 하단에 고정
chatList.scrollTop = chatList.scrollHeight;
})
</script>
</body>
</html>
socket.io/server.js
// TCP : 연결 지향적이며, 신뢰성이 높은 데이터 전송 지원
// - 데이터 전송을 하기 전에 연결을 설정하고, 전송 후에 연결 해제 - 파일전송, 데이터의 정확성 or 순서
// UDP : 비연결 지향적, 데이터 전송속도가 빠르지만 데이터의 유실 가능성
// - 일단 바로 데이터 전송 - 실시간 데이터 전송에 적합
// - 게임, 스트리밍
// npm i socket.io
const express = require('express');
const http = require('http');
const { send } = require('process');
// node.js 기본 내장 모듈인 'http' 불러오기.
// 'http' 모듈은 HTTP 서버와 클라이언트 기능을 제공.
const socketIO = require('socket.io');
// 'socket.io' 모듈 불러오기.
// WebSocket 기반, 실시간 양방향 데이터 전송 지원 라이브러리.
const app = express();
const server = http.createServer(app);
// Express 애플리케이션 기반으로 HTTP 서버 생성.
const io = socketIO(server)
// HTTP 서버 'server'를 'socket.io'에 바인딩하여 WebSocket 기능을 추가한 서버를 생성.
// 이러면 클라이언트-서버 간에 실시간 양방향 통신을 할 수 있음.
const PORT = 8080;
app.set('view engine', 'ejs');
app.get('/', (req, res) => {
res.render('client');
})
// [실습 3-2-1]
// 사용자 닉네임 모음 객체
const nickObjs = {};
// [실습 3-2-3]
// 유저 목록 업데이트
function updateList() {
io.emit('updateNicks', nickObjs) // 전체 사용자 닉네임 모음 객체 전달
}
// io.on() : socket 관련한 통신 작업을 처리
io.on('connection', (socket) => {
// connection 이벤트는 클라이언트가 접속 했을 때 발생
console.log('서버 연결 완료 :: ', socket.id);
// socket.id : 소켓 고유 아이디 (브라우저 탭 단위)
// (참고) 소켓 연결 완료.
// [실습 1]
// socket.on('hello', (data) => {
// console.log(data);
// console.log(`${data.who} : ${data.msg}`);
// socket.emit('hellokr', {who: 'hello', msg: '안녕!!!'})
// })
// [실습 3] 채팅창 입장 안내 문구
// io.emit('notice', `${socket.id} 님이 입장하셨습니다.`);
// [실습 3-2] 채팅창 입장 문구 socket.id -> nickname
// emit() from server
// - socket.emit(event_name, data) : 해당 클라이언트에게만 이벤트, 데이터를 전송.
// - io.emit(event_name, data) : 서버에 접속된 모든(all) 클라이언트 전송
// - io.to(소켓아이디).emit(event_name, data) : 소켓아이디에 해당하는 클라이언트 에게만 전송. (귓속말)
socket.on('setNick', (nick) => {
console.log(`닉네임 설정 완료 :: ${nick} 님 입장`);
// [실습 3-2-1]
// 프론트에서 입력한 nick이 nickObjs 객체에 존재하는지 검사.
// 이미 존재 : error 이벤트 + '이미 존재하는 닉네임 입니다'
// => 클라이언트 : error 이벤트 받으면 alert 띄우기.
// 새 닉네임 : notice 이벤트 + ${nick} 님이 입장하셨습니다.
if (Object.values(nickObjs).indexOf(nick) > - 1) {
// 이미 존재하는 닉네임 있음.
socket.emit('error', '이미 존재하는 닉네임 입니다.');
} else {
// 새로운 닉네임
nickObjs[socket.id] = nick;
console.log('접속 유저 목록 :: ', nickObjs);
io.emit('notice', `${nick} 님이 입장하셨습니다.`); // 전체 공지
// [실습 3-2-2]
socket.emit('entrySuccess', nick); // 해당 소켓 데이터를 전송.
updateList();
}
})
// [실습 3-3] 클라이언트 퇴장시
// "notice" 이벤트로 퇴장 공지
socket.on('disconnect', () => {
console.log("접속 끊김 :: ", `${nickObjs[socket.id]} 님 퇴장 ::` , socket.id);
io.emit('notice', `${nickObjs[socket.id]} 님이 퇴장하셨습니다.`);
delete nickObjs[socket.id]; // 닉네임 삭제
updateList();
})
// [실습 4] 채팅창 메시지 전송
// send 이벤트를 받아서
// 모두에게 newMessage 이벤트로 {닉네임, 입력창 내용} 데이터를 전송.
socket.on('send', (data) => {
// { dm: '?', myNick: 'A', msg: 'B' } 형식
// console.log("서버 측 - data :: ", data);
// "전체 발송"
// io.emit('newMessage', {nick : data.myNick, msg: data.msg }) -- DM 기능, IF문 시 주석.
// [실습 5] DM 기능 추가하기
// DM 인지 아닌지 구분해서
// io.to(소켓아이디).emit(event_name, data) : 소켓아이디에 해당하는 클라이언트에게'만' 전송.
if(data.dm === 'all') {
// "전체" 발송
io.emit('newMessage', {nick : data.myNick, msg: data.msg })
} else {
// "DM" 발송
let dmSocketId = data.dm;
const sendData = {
nick: data.myNick,
msg: data.msg,
dm:'(속닥속닥) ',
}
io.to(dmSocketId).emit('newMessage', sendData) // DM을 보내야하는 타겟(소켓아이디)한테 메세지 전송.
socket.emit('newMessage', sendData);
console.log("sendData >>> ", sendData ); // { nick: '파이리', msg: '안녕', dm: '(속닥속닥)' }
}
})
})
server.listen(PORT, () => {
console.log(`http://localhost:${PORT}`);
})
socket.io/hint.js
(설명을 위한 예시 코드)
// 빈 객체 생성.
const nickObjs = {};
console.log('nickObjs > ', nickObjs);
// Id 속성을 가진 객체로 생성.
const socket = {id:'asdfqwetasdf234'};
console.log("socket > ", socket);
// js 에서 obj에 key, value 추가하는 방법
// [1] `.` 연산자를 사용한 속성 추가.
nickObjs.hello = '안녕';
nickObjs.hi = '안녕2';
// [2] `[]`를 사용한 속성 추가.
nickObjs['apple'] = '사과';
// [3] 변수를 이용한 속성 추가.
nickObjs[socket.id] = 'damon';
console.log("nickObjs >> ", nickObjs);
// js에서 obj 에 특정 key가 존재하는지 검사
const nickObjs2 = { hello: '안녕', apple: '사과'};
const nick1 = '안녕';
const nick2 = '사과';
const nick3 = '오렌지';
console.log("nickObjs2 >> ", nickObjs2);
// Object.values() : object에서 value만 뽑아서 배열로 만들어줌.
// JS 내장함수
console.log(Object.values(nickObjs2));
console.log(Object.values(nickObjs2).indexOf(nick1));
console.log(Object.values(nickObjs2).indexOf(nick2));
console.log(Object.values(nickObjs2).indexOf(nick3));
// nick3은 nickObjs2에 존재하지 않기 때문에 -1
console.log('------------------------');
// for in 반복문
for (let key in nickObjs2) {
console.log(key, nickObjs2[key]); // key, value 출력
}
// object 요소 삭제
console.log('삭제 전 >' , nickObjs2);
delete nickObjs2['hello'];
console.log('삭제 후 >' , nickObjs2);