웹 소켓으로 익명 채팅 만들기(GIF파일 전송 가능)

백지연·2022년 3월 14일
2

NodeJS

목록 보기
24/26
post-thumbnail

이번 포스팅에서는 Socket.IO를 이용해 익명 채팅을 만들어보겠다!
이전 포스팅에서 웹 소켓과 Socket.IO에 대한 기본적인 설명과 예제를 적어두었으니 보고 오면 좋다.

책 Node.js 교과서(개정 2판) 책의 12장의 내용을 참고했다.
+모든 코드는 github주소에 있다.

개발 환경

GIF 채팅방 만들기

주의: 소켓은 버전에 따라 에러를 많이 내므로, 본 포스팅에 있는 버전과 일치하지 않는 경우 에러가 발생할 수 있음!

1. 뼈대 구축하기(npm, DB, 추가html/css)

1. npm 설치

Git [package.json]

{
  "name": "gif-chat",
  "version": "1.0.0",
  "main": "app.js",
  "scripts": {
    "start": "nodemon app"
  },
  "author": "delay100",
  "license": "ISC",
  "dependencies": {
    "axios": "^0.26.0",
    "color-hash": "^2.0.1",
    "cookie-parser": "^1.4.6",
    "cookie-signature": "^1.2.0",
    "dotenv": "^16.0.0",
    "express": "^4.17.3",
    "express-session": "^1.17.2",
    "mongoose": "^6.2.3",
    "morgan": "^1.10.0",
    "multer": "^1.4.4",
    "nunjucks": "^3.2.3",
    "socket.io": "^2.4.1",
    "ws": "^8.5.0"
  },
  "devDependencies": {
    "nodemon": "^2.0.3"
  },
  "description": ""
}

2. DB구축 - 몽구스(Mongoose)

+ 몽구스(Mongoose) 설명

보안을 위해 MongoDB의 ID와 PASSWORD는 .env파일로 분리했다. (.env 파일이 없다면 생성해주자)

Git [schemas/index.js] - 몽구스와 몽고디비 연결

// mongodb와 연결
const mongoose = require('mongoose');

const { MONGO_ID, MONGO_PASSWORD, NODE_ENV } = process.env; // 보안을 위해 정보 분리
const MONGO_URL = `mongodb://${MONGO_ID}:${MONGO_PASSWORD}@127.0.0.1:27017/admin`;

const connect = () => {
     // 배포 환경이 아닌 경우(ex. 개발 환경) debug를 세팅함 - 몽구스가 생성하는 쿼리를 콘솔에 출력
    if (NODE_ENV !== 'production') {
        mongoose.set('debug', true);
    }
    // 몽구스와 몽고디비 연결
    mongoose.connect(MONGO_URL, {
        dbName: 'gifchat', // 접속을 시도하는 주소의 데이터베이스
        useNewUrlParser: true, // 굳이 없어도 되는데 콘솔에 에러 뜨는 것 없애기1 
        // useCreateIndex: true, // 굳이 없어도 되는데 콘솔에 에러 뜨는 것 없애기2
    }, (error) => {
        if (error) {
            console.log('몽고디비 연결 에러', error);
        } else {
            console.log('몽고디비 연결 성공');
        }
    });
};

// 이벤트 리스너
mongoose.connection.on('error', (error) => {
    console.log('몽고디비 연결 에러', error); // 에러 발생 시 에러 내용을 기록하고,
});

mongoose.connection.on('disconnected', () =>{
    console.log('몽고디비 연결이 끊겼습니다. 연결을 재시도합니다.'); // 연결 종료 시 재연결 시도 
    connect();
});

module.exports = connect;

=> useCreateIndex: true 내용은 주석을 해두었는데, 주석을 풀면 에러가 나므로 주석을 풀지 말 것!!

Git [schemas/room.js] - 채팅방 스키마 구현

// 채팅방 스키마

const mongoose = require('mongoose');

const { Schema } = mongoose;
const roomSchema = new Schema({
    title: { // 방 제목
        type: String,
        required: true,
    },
    max: { // 최대 수용 인원
        type: Number,
        required: true,
        default: 10, // 기본적으로 10명
        min: 2, // 최소 인원 2명
    },
    owner: { // 방장
        type: String,
        required: true,
    },
    password: String, // 비밀번호, 없어도 되므로 required 속성 필요x
    createdAt: { // 생성 시간
        type: Date,
        default: Date.now,
    },
});

module.exports = mongoose.model('Room', roomSchema);

Git [schemas/chat.js] - 채팅 스키마 구현

// 채팅 스키마

const mongoose = require('mongoose');

const { Schema } = mongoose;
const { Types: { ObjectId }} = Schema;
const chatSchema = new Schema({
    room: { // 채팅방 아이디
        type: ObjectId,
        required: true,
        ref: 'Room', // Room 스키마와 연결해 Room 컬렉션의 ObjectId가 들어가게 됨
    },
    user: { // 채팅을 한 사람
        type: String,
        required: true,
    },
    // chat과 gif는 둘 중 하나만 저장하면 돼서 required 속성x
    chat: String, // 채팅 내역
    gif: String, // GIF 이미지 주소
    createdAt: { // 채팅 시간
        type: Date,
        default: Date.now,
    },
});

module.exports = mongoose.model('Chat', chatSchema);

Git [app.js]

...
const connect = require('./schemas');
...
connect();

이렇게 세팅이 되었다면, MongoDB를 켜서 chatsrooms 컬렉션을 만들어주자!

MongoDB 명령어

DB 생성 - use [데이터베이스 명]

use gifchat

컬렉션 생성 - db.createCollection('[컬렉션명'])

> db.createCollection('[chats'])
> db.createCollection('[rooms'])

MongoDB를 다루기 어렵다면 링크한 글을 읽고 만들어보자.

  • 생성된 MongoDB의 GUI(컴퍼스) 화면

3. 추가 html/css

부수적인 파일들을 한데 모았다.

Git [views/layout.html]

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>{{title}}</title>
    <link rel="stylesheet" href="/main.css">
</head>
<body>
    {% block content %}
    {% endblock %} 
    {% block script %}
    {% endblock %}
</body>
</html>

Git [views/error.html]

{% extends 'layout.html' %}

{% block content %}
<h1>{{message}}</h1>
<h2>{{error.status}}</h2>
<pre>{{error.stack}}</pre>
{% endblock %}

Git [public/main.css]

* { box-sizing: border-box; }
.mine { text-align: right; }
.system { text-align: center; }
.mine img, .other img {
    max-width: 300px;
    display: inline-block;
    border: 1px solid silver;
    border-radius: 5px;
    padding: 2px 5px;
}
.mine div:first-child, .other div:first-child { font-size: 12px; }
.mine div:last-child, .other div:last-child {
    display: inline-block;
    border: 1px solid silver;
    border-radius: 5px;
    padding: 2px 5px;
    max-width: 300px;
}
#exit-btn { position: absolute; top: 20px; right: 20px; }
#chat-list { height: 500px; overflow: auto; padding: 5px; }
#chat-form { text-align: right; }
label[for='gif'], #chat, #chat-form [type='submit'] {
    display: inline-block;
    height: 30px;
    vertical-align: top;
}
label[for='gif'] { cursor: pointer; padding: 5px; }
#gif { display: none; }
table, table th, table td {
    text-align: center;
    border: 1px solid silver;
    border-collapse: collapse;
}

2. main 화면 구현하기

Git [routes/index.js] 中 GET / 라우터 - 메인 화면 렌더링

const Room = require('../schemas/room');
...

// GET / 라우터 - 메인 화면 렌더링
router.get('/', async (req, res, next) => {
    try {
        const rooms = await Room.find({});
        res.render('main', { rooms, title: 'GIF 채팅방'});
    } catch (error) {
        console.error(error);
        next(error);
    }
});

=2-1> render main에 의해 views/main.html이 실행됨

render 항목

  • rooms: 모든 Room 정보

Git [views/main.html] - http://127.0.0.1:8005/ 화면

{% extends 'layout.html' %}

{% block content %}

<h1>GIF 채팅방</h1>
<fieldset>
    <legend>채팅방 목록</legend>
    <table>
        <thead>
        <tr>
            <th>방 제목</th>
            <th>종류</th>
            <th>허용 인원</th>
            <th>방장</th>
        </tr>
        </thead>
        <tbody>
        {% for room in rooms %}
            <tr data-id="{{room._id}}">
                <td>{{room.title}}</td>
                <td>{{'비밀방' if room.password else '공개방'}}</td>
                <td>{{room.max}}</td>
                <td style="color:{{room.owner}}">{{room.owner}}</td>
                <td>
                    <button
                        data-password="{{'true' if room.password else 'false'}}"
                        data-id="{{room._id}}"
                        class="join-btn"
                    >입장
                    </button>
                </td>
            </tr>
        {% endfor %}
        </tbody>
    </table>
    <div class="error-message">{{error}}</div>
    <a href="/room">채팅방 생성</a>
</fieldset>
<script src="/socket.io/socket.io.js"></script>
<script>
    const socket = io.connect('http://127.0.0.1:8005/room', { // 네임 스페이스: 주소 뒤에 /room이 붙은 것을 말함, 서버에서 /room 네임스페이스를 통해 보낸 데이터만 받을 수 있음
        path: '/socket.io',
    });

    // 방 추가 함수
    function addBtnEvent(e) { // 방 입장 클릭 시
        if (e.target.dataset.password === 'true') {
            const password = prompt('비밀번호를 입력하세요');
            location.href = '/room/' + e.target.dataset.id + '?password=' + password;
        } else {
       		location.href = '/room/' + e.target.dataset.id;
        }
    }

    document.querySelectorAll('.join-btn').forEach(function (btn) {
        btn.addEventListener('click', addBtnEvent);
    });
</script>
{% endblock %} 

{% block script %} 
<script>
    window.onload = () => {
        if (new URL(location.href).searchParams.get('error')) {
            alert(new URL(location.href).searchParams.get('error'));
        }
    };
</script>
{% endblock %}

=2-2> 1. 입장 버튼, 2. 채팅방 생성 버튼

1. 입장 버튼

  • class="join-btn"에 의해 아래의 javascript 코드 실행
    Git [views/main.html] 中 script 일부
   // 방 추가 및 입장 함수
    function addBtnEvent(e) { 
        if (e.target.dataset.password === 'true') {
            const password = prompt('비밀번호를 입력하세요');
            location.href = '/room/' + e.target.dataset.id + '?password=' + password;
        } else {
        	location.href = '/room/' + e.target.dataset.id;
        }
    }
    document.querySelectorAll('.join-btn').forEach(function (btn) {
        btn.addEventListener('click', addBtnEvent);
    });

=> querySelectorAll이 먼저 실행된 후 addBtnEvent가 실행됨

=2-2.1> location.href에 의해 routes/index.jsGET /room/:id이 실행됨

// GET /room/:id 라우터 - 렌더링 전에 방이 존재하는지, 비밀 방이면 비밀번호가 맞는지, 허용인원 초과하지 않았는지 검사
router.get('/room/:id', async (req, res, next) => {
    try {
        const room = await Room.findOne({ _id: req.params.id });
        const io = req.app.get('io'); // socket.js에서 app.set('io', io)로 저장한 io 객체를 req.app.get('io')로 가져옴
        if(!room) {
            return res.redirect('/?error=존재하지 않는 방입니다.');
        }
        if(room.password && room.password !== req.query.password) {
            return res.redirect('/?error=비밀번호가 틀렸습니다.');
        }
        const { rooms } = io.of('/chat').adapter; // io.of('/chat').adapter.rooms: 방 목록이 들어있음
        if (rooms && rooms[req.params.id] && room.max <= rooms[req.params.id].length) { // io.of('/chat').adapter.rooms[req.params.id]: 해당 방의 소켓 목록이 나옴
                                                                                        // 소켓의 수를 세서 참가 인원의 수를 알아낼 수 있음
            return res.redirect('/?error=허용 인원이 초과하였습니다.');
        }
        const chats = await Chat.find({ room: room._id}).sort('createdAt'); // DB로부터 채팅내역을 가져옴
        return res.render('chat', {
            room,
            title: room.title,
            chats, // chats: chats
            user: req.session.color,
            // member: rooms && rooms[req.params.id],
        });
    } catch (error) {
        console.error(error);
        return next(error);
    }
});

=2-2.1.> 1. render('chat')에 의해 views/chat.html 실행 -> 4에서 설명

render 항목

  • room: 요청으로 들어온 room 정보
  • title: room 제목
  • chats: db로부터 가져온 채팅 내역
  • user: 요청으로 들어온 세션의 유저(색 코드)

2. 채팅방 생성 버튼 -> 3에서 설명
-> href="/room"에 의해 routes/index.jsGET /room 실행


3. 채팅방 생성 구현하기

main 화면 구현하기 中 2.2.2. 채팅방 생성 내용

2. 채팅방 생성 버튼
-> href="/room"에 의해 routes/index.jsGET /room 실행

Git [routes/index.js] 中 GET /room 라우터 - 채팅방 생성 화면 렌더링

// GET /room 라우터 - 채팅방 생성 화면 렌더링
router.get('/room', (req, res) => {
    res.render('room', { title: 'GIF 채팅방 생성'});
});

=3-1> render room에 의해 views/room.html이 실행됨

Git [views/room.html]

{% extends 'layout.html' %}

{% block content %}
    <fieldset>
        <legend>채팅방 생성</legend>
        <form action="/room" method="post">
            <div>
                <input type="text" name="title" placeholder="방 제목">
            </div>
            <div>
                <input type="number" name="max" placeholder="수용 인원(최소 2명)" min="2" value="10">
            </div>
            <div>
                <input type="password" name="password" placeholder="비밀번호(없으면 공개방)">
            </div>
            <div>
                <button type="submit">생성</button>
            </div>
        </form>
    </fieldset>
{% endblock %}

=3-2> 1. 생성 버튼

1. 생성 버튼

  • type이 submit이기 때문에 form의 action이 실행됨(method가 POST)
    -> routes/index.js에서 POST /room 실행

Git [routes/index.js] 中 POST /room

// POST /room 라우터 - 채팅방을 만들기
router.post('/room', async (req, res, next) => {
    try {
        const newRoom = await Room.create({
            title: req.body.title,
            max: req.body.max,
            owner: req.session.color,
            password: req.body.password,
        });
        const io = req.app.get('io'); // socket.js에서 app.set('io', io)로 저장한 io 객체를 req.app.get('io')로 가져옴
        io.of('/room').emit('newRoom', newRoom); // /room 네임스페이스에 연결한 모든 클라이언트에 데이터를 보내는 메서드, of 메서드: Socket.IO에 다른 네임스페이스를 부여하는 메서드
                                                 // 네임스페이스가 없는 경우에는 io.emit 메서드로 모든 클라이언트에 데이터를 보낼 수 있음
                                                 // GET / 라우터에 접속한 모든 클라이언트가 새로 생성된 채팅방에 대한 데이터를 받을 수 있음
        res.redirect(`/room/${newRoom._id}?password=${req.body.password}`); 
    } catch (error) {
        console.error(error);
        next(error);
    }
});

=3-2.1.> 1. 서버에서 클라이언트로 socket.IO 연결, 2. res.redirect에 의해 주소가 http://127.0.0.1:8005/room/생성된방id/?password=비밀번호 로 이동 -> 2.2.1에서 설명

1. 서버에서 클라이언트로 socket.IO 연결
Git [socket.js] 中 room 네임스페이스

  const io = new SocketIO(server, { path: '/socket.io' }); // socket.io를 불러와 express와 연결
                                                             // SocketIO의 두 번째 인수로 옵션 객체를 넣어 서버에 관한 여러가지 설정 가능
                                                             // path: 클라이언트가 접속할 경로 설정(클라이언트에서도 이 경로와 일치하는 path를 넣어야 함)
    app.set('io', io); // 라우터에서 io 객체를 쓸 수 있게 저장, req.app.get('io')로 접근 가능
    const room = io.of('/room'); // of 메서드: Socket.IO에 다른 네임스페이스를 부여하는 메서드, 같은 네임스페이스끼리만 데이터 전달
    // 웹 소켓 연결 후 이벤트 리스너를 붙힘
    // io(room, chat)와 socket객체가 Socket.IO의 핵심임
    // room 네임스페이스
    room.on('connection', (socket) => { // 이벤트리스너를 붙혀줌
                                        // connection: 클라이언트가 접속했을 때 발생, 콜백으로 소켓 객체(socket) 제공
        console.log('room 네임스페이스에 접속');
        socket.on('disconnect', () => {
            console.log('room 네임스페이스 접속 해제');
        });
    });

4. 일반 채팅 구현하기

채팅방을 생성했을 때, 채팅방에 입장했을 때를 구현해보자!

=2-2.1.> 1. render('chat')에 의해 views/chat.html 실행 -> 4에서 설명

render 항목

  • room: 요청으로 들어온 room 정보
  • title: room 제목
  • chats: db로부터 가져온 채팅 내역
  • user: 요청으로 들어온 세션의 유저(색 코드)

Git [views/chat.html]

{% extends 'layout.html' %} 

{% block content %} 
    <h1>{{title}}</h1>
    <a href="/" id="exit-btn">방 나가기</a>
    <!-- <div class="member_list">참여자 수: </div> -->
    <fieldset>
        <!-- 채팅 내용 -->
        <legend>채팅 내용</legend>
        <div id="chat-list">
            {% for chat in chats %} 
            {% if chat.user === user %} 
                <div class="mine" style="color:{{chat.user}}">  <!-- mine: 내 메세지 -->
                    <div>{{chat.user}}</div>
                    {% if chat.gif %}  
                        <img src="/gif/{{chat.gif}}">
                    {% else %} 
                    <div>{{chat.chat}}</div>
                    {% endif %} 
                </div>
            {% elif chat.user === 'system' %} 
                <div class="system"> <!-- system: 시스템 메세지 -->
                    <div>{{chat.chat}}</div>
                </div>
            {% else %}
                <div class="other" style="color:{{chat.user}}"> <!-- other: 남의 메세지 -->
                <div>{{chat.user}}</div>
                {% if chat.gif %} 
                    <img src="/gif/{{chat.gif}}">
                {% else %}
                    <div>{{chat.chat}}</div>
                {% endif %} 
                </div>
            {% endif %} 
        {% endfor %}
        </div> 
    </fieldset>

      <!-- 채팅 전송 -->
    <form action="/chat" id="chat-form" method="post" enctype="multipart/formdata">
        <label for="gif">GIF 올리기</label>
        <input type="file" id="gif" name="gif" accept="img/gif">
        <input type="text" id="chat" name="chat">
        <button type="submit">전송</button>
    </form>

 <script>
        // 채팅을 전송하는 폼에 submit 이벤트 리스너 추가
        document.querySelector('#chat-form').addEventListener('submit', function (e) {
            e.preventDefault();
            if (e.target.chat.value) {
                axios.post('/room/{{room._id}}/chat', {
                    chat: this.chat.value,
                })
                    .then(() => {
                        e.target.chat.value = '';
                    })
                    .catch((err) => {
                        console.error(err);
                    });
            }
        });
    </script>
{% endblock %}

=4-1> 1. 방 나가기 버튼, 2. 채팅 전송 버튼

1. 방 나가기 버튼

2. 채팅 전송 버튼

  • type=submit에 의해 form의 action이 실행됨
  • action은 /chat인데 POST 방식
    -> routes/index.js의 POST /room/:id/chat 실행

Git [routes/index.js] 中 /room/:id/chat

// POST /room/:id/chat - 방 접속 시 기존 채팅 내역을 불러오도록 하는 라우터
// 채팅을 할 때마다 채팅 내용이 이 라우터로 전송되고, 라우터에서 다시 웹 소켓으로 메세지를 보냄
router.post('/room/:id/chat', async (req, res, next) => {
   try {
       const chat = await Chat.create({ // 채팅을 db에 저장
           room: req.params.id,
           user: req.session.color,
           chat: req.body.chat,
       }); 
       req.app.get('io').of('/chat').to(req.params.id).emit('chat', chat); // 같은 방(req.params.id)에 들어있는 소켓들에게 메시지 데이터를 전송
       res.send('ok');
   } catch (error) {
       console.error(error);
       next(error);
   }
});

=4-2> 1. res.send('ok') - 응답으로 ok 보냄, 2. 라우터에서 서버로 socket.IO 연결

2. 라우터에서 서버로 socket.IO 연결
네임스페이스 명(of): /chat
방 아이디(to): req.params.id
이벤트 명 - 데이터 명(emit): chat - chat


5. GIF 파일 전송하기

Git [views/chat.html] 中 gif 전송 부분

    <!-- 채팅 전송 -->
    <form action="/chat" id="chat-form" method="post" enctype="multipart/formdata">
        <label for="gif">GIF 올리기</label>
        <input type="file" id="gif" name="gif" accept="img/gif">
        <input type="text" id="chat" name="chat">
        <button type="submit">전송</button>
    </form>
    
<!--  gif 의 input이 바뀌면 script의 #gif 실행 -->
<script>
        // 프런트 화면에서 이미지를 선택해 업로드하는 이벤트 리스너
        document.querySelector('#gif').addEventListener('change', function (e) {
            console.log(e.target.files);
            const formData = new FormData();
            formData.append('gif', e.target.files[0]);
            axios.post('/room/{{room._id}}/gif', formData)
                .then(() => {
                    e.target.file = null;
                })
                .catch((err) => {
                    console.error(err);
                });
        });
</script>

=5-1> axios.post로 인해 POST /room/방 아이디/gif 실행

Git [routes/index.js] 中 /room/방 아이디/gif

try {
    fs.readdirSync('uploads');
} catch (err) {
    console.error('uploads 폴더가 없어 uploads 폴더를 생성합니다.');
    fs.mkdirSync('uploads');
}

const upload = multer({
    storage: multer.diskStorage({
        destination(req, file, done) {
            done(null, 'uploads/');
        },
        filename(req, file, done) {
            const ext = path.extname(file.originalname);
            done(null, path.basename(file.originalname, ext) + Date.now() + ext); // 파일명에 타임스탬프(Date.now())를 붙힘
        },
    }),
    limits: { fileSize: 5 * 1024 * 1024}, // 용량을 5MB로 제한
});

// POST /room/{{room._id}}/gif - views/chat.html에서 보낸 gif내용 처리
router.post('/room/:id/gif', upload.single('gif'), async (req, res, next) => {
    try {
        const chat = await Chat.create({
            room: req.params.id,
            user: req.session.color,
            gif: req.file.filename,
        });
        req.app.get('io').of('/chat').to(req.params.id).emit('chat', chat);
        res.send('ok');
    } catch (error) {
        console.error(error);
        next(error);
    }
});

=5-2> 1. res.send('ok') - 응답으로 ok 보냄, 2. 라우터에서 서버로 socket.IO 연결

2. 라우터에서 서버로 socket.IO 연결
네임스페이스 명(of): /chat
방 아이디(to): req.params.id
이벤트 명 - 데이터 명(emit): chat - chat

Git [socket.js] 中 chat 네임스페이스

// chat 네임스페이스
    chat.on('connection', (socket) => { // 이벤트리스너를 붙혀줌
        console.log('chat 네임스페이스에 접속');
      
        const req = socket.request;
        const { headers: { referer }} = req; // socket.request.headers.referer: 현재 웹 페이지의 URL 가져올 수 있음, referer: 하이퍼링크를 통해서 각각의 사이트로 방문시 남는 흔적

        const roomId = referer // roomId로 같은 채팅방에 있는 사람인지 구분
            .split('/')[referer.split('/').length -1]
            .replace(/\?.+/, ''); // split과 replace로 방의 id를 가져옴

            socket.join(roomId); // 방의 id를 인수로 받음
                                 //  chat 네임스페이스에 접속 시 socket.join 메소드 실행 - 방에 들어가는 메서드

            socket.to(roomId).emit('join', { // socket.to(방 아이디) 메서드: 특정 방에 데이터를 보낼 수 있음
                user: 'system',
                chat: `${req.session.color}님이 입장하셨습니다.`,
            });

            // 접속 해제 시 
            socket.on('disconnect', () => {
                console.log('chat 네임스페이스 접속 해제');
                socket.leave(roomId); // chat 네임스페이스에 접속 해제 시 socket.join 메소드 실행 - 방에서 나가는 메서드
                                      // 연결이 끊기면 자동으로 방에서 나가지만, 확실히 하기 위해 추가
                const currentRoom = socket.adapter.rooms[roomId]; // socket.adapter.rooms[방 아이디]: 참여 중인 소켓 정보가 들어 있음
                const userCount = currentRoom ? currentRoom.length : 0;
                if (userCount === 0) { // 방에 인원수가 0명인 경우 
                    const signedCookie = req.signedCookies['connect.sid']; // req.signedCookies 내부의 쿠키들은 모두 복호화되어 있으므로 다시 암호화해서 요청에 담아보내야 함
                    const connectSID = cookie.sign(signedCookie, process.env.COOKIE_SECRET);
                    axios.delete(`http://localhost:8005/room/${roomId}`, { // 서버에서 axios 요청 시 요청자가 누구인지에 대한 정보가 없음 (브라우저는 자동으로 쿠키를 같이 넣어 보냄)
                                                                         // express-session에서는 세션 쿠키인 req.signedCookies['connect.sid']를 보고 현재 세션이 누구에게 속해있는지 판단함
                        headers: {
                            Cookie: `connect.sid=s%3A${connectSID}`, // s%3A 뒷 내용이 암호화된 내용, DELETE /room/:id 라우터에서 요청자가 누군지 확인 가능
                        },
                    })  
                        .then(() => {
                            console.log('방 제거 요청 성공'); 
                        })
                        .catch((error) => {
                            console.error(error);
                        });
                } else { // 방에 인원수가 0명이 아닌 경우 - 방에 있는 사람에게 퇴장 메세지 보냄(system)
                    socket.to(roomId).emit('exit', {
                        user: 'system',
                        chat: `${req.session.color}님이 퇴장하셨습니다.`,
                    });
                }
            });

=5-3> 1. usercount가 0이면 delete /room/방 아이디 실행, 2. usercount가 0이 아니면 서버에서 클라이언트로 socket.IO 연결

1. usercount가 0이면 delete /room/방 아이디 실행
Git [routes/index.js]
방 아이디(to): room
이벤트 명 - 데이터 명(emit): removeRoom - removeRoom

// DELETE /room/:id - 채팅방을 삭제하는 라우터
router.delete('/room/:id', async (req, res, next) => {
    try {
        await Room.remove({ _id: req.params.id });
        await Chat.remove({ room: req.params.id });
        res.send('ok');
        setTimeout(() => { // 2초뒤에 
            req.app.get('io').of('/room').emit('removeRoom', req.params.id); // 웹 소켓으로 /room 네임스페이스에 방이 삭제되었음(removeRoom)을 알림
        }, 2000);
    } catch (error) {
        console.error(error);
        next(error);
    }
});

2. usercount가 0이 아니면 서버에서 클라이언트로 socket.IO 연결
방 아이디(to): roomId
이벤트 명 - 데이터 명(emit): exit - exit
+참고) roomId = socket.request.headers.referer: 현재 웹 페이지의 URL 가져올 수 있음

Git [views/chat.html] 中 클라이언트 측 socket exit 연결 부분

        // 사용자의 퇴장을 알리는 메시지 표시
        socket.on('exit', function(data) { // 사용자의 채팅방 퇴장에 대한 데이터를 웹 소켓에 전송될 때 실행됨, 소켓에 exit 이벤트 리스너 연결
            const div = document.createElement('div');
            div.classList.add('system'); // div에 class=`system`속성 추가
            const chat = document.createElement('div');
            div.textContent = data.chat;
            div.appendChild(chat);
            document.querySelector('#chat-list').appendChild(div);
        });

실행화면

** mongoose 사용 시 mongoDB서버를 꼭 켜줘야 함
입력(cmd)

C:\Program Files\MongoDB\Server\5.0\bin> mongod --auth

잘못된 정보 수정 및 피드백 환영합니다!!

profile
TISTORY로 이사중! https://delay100.tistory.com

0개의 댓글