Node.js와 Socket.IO로 실시간 채팅 만들기



1. Socket.IO 의 기본 메서드

1) emiton

  • emit: 이벤트를 발생시키는 메서드입니다. 이벤트와 함께 데이터를 전송할 수 있습니다. 즉, 이벤트를 “발신”하는 역할을 합니다.
  • on: 특정 이벤트가 발생했을 때 실행할 콜백 함수를 등록하는 메서드입니다. 즉, 이벤트를 “수신”하는 역할을 합니다.


2) socketio

  • 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);