본문 바로가기
Javascript

NestJS + Websocket으로 채팅만들기 #4 (feat. 채팅 구색갖추기)

by for2gles 2021. 7. 26.
반응형

이번에는 채팅에 필요한 기능들을 정리하여 제작해보려고 한다.

 

일단 기본적으로 내가 만들고 싶은 채팅방은

  1. 입장했을 시 lobby에 입장한다. 물론 로비에 있는 다른 회원과 대화가 가능하다.
  2. 채팅방을 임의로 제작할 수 있고, 채팅방을 만들면 자동으로 채팅방 목록에서 채팅방이 추가된다.
  3. 아무도 없는 채팅방은 자동으로 소멸된다.
  4. 1유저는 1개의 채팅방에만 들어갈 수 있다.
  5. 닉네임은 언제든지 변경이 가능하고, 창을 껐다 켰을시에도 해당 닉네임이 유지되야한다.

이 형태의 채팅방이고 부가적인 기능인 방장이 나갔을 경우 랜덤으로 방장이 바뀐다거나, 채팅방 혹은 닉네임 중복 방지와 같은 부가기능은 일단 제외하고 기본적으로 채팅방으로서의 형태를 제작해보려고 한다.

 

이렇게 되면 만들어야 하는 함수목록이 대강 정리가 된다.

  • 메시지 전송(sendMessage)
  • 닉네임을 로컬스토리지에 저장하고있다가, 최초 접속시 닉네임설정(setInit)
  • 닉네임 변경(setNickname)
  • 채팅방 목록 가져오기(getChatRoomList)
  • 채팅방 생성하기(createChatRoom)
  • 채팅방 들어가기(enterChatRoom)

백엔드에서는 이정도 함수를 제작 해 놓으면 채팅의 기본적인 구색은 갖추지 않았을까 생각한다.

 

chatBackEnd.gateway.ts

import {
    SubscribeMessage,
    WebSocketGateway,
    WebSocketServer,
    OnGatewayConnection,
    OnGatewayDisconnect,
} from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';
import { ChatRoomService } from './chatRoom.service';
import { setInitDTO } from './dto/chatBackEnd.dto';

@WebSocketGateway(5000, {
    cors: {
        origin: 'http://localhost:3000',
    },
})
export class ChatBackEndGateway
    implements OnGatewayConnection, OnGatewayDisconnect
{
    constructor(private readonly ChatRoomService: ChatRoomService) {}
    @WebSocketServer()
    server: Server;

    //소켓 연결시 유저목록에 추가
    public handleConnection(client: Socket): void {
        console.log('connected', client.id);
        client.leave(client.id);
        client.data.roomId = `room:lobby`;
        client.join('room:lobby');
    }

    //소켓 연결 해제시 유저목록에서 제거
    public handleDisconnect(client: Socket): void {
        const { roomId } = client.data;
        if (
            roomId != 'room:lobby' &&
            !this.server.sockets.adapter.rooms.get(roomId)
        ) {
            this.ChatRoomService.deleteChatRoom(roomId);
            this.server.emit(
                'getChatRoomList',
                this.ChatRoomService.getChatRoomList(),
            );
        }
        console.log('disonnected', client.id);
    }

    //메시지가 전송되면 모든 유저에게 메시지 전송
    @SubscribeMessage('sendMessage')
    sendMessage(client: Socket, message: string): void {
        const { roomId } = client.data;
        client.to(roomId).emit('getMessage', {
            id: client.id,
            nickname: client.data.nickname,
            message,
        });
    }

    //처음 접속시 닉네임 등 최초 설정
    @SubscribeMessage('setInit')
    setInit(client: Socket, data: setInitDTO): setInitDTO {
        // 이미 최초 세팅이 되어있는 경우 패스
        if (client.data.isInit) {
            return;
        }

        client.data.nickname = data.nickname
            ? data.nickname
            : '낯선사람' + client.id;

        client.data.isInit = true;

        return {
            nickname: client.data.nickname,
            room: {
                roomId: 'room:lobby',
                roomName: '로비',
            },
        };
    }

    //닉네임 변경
    @SubscribeMessage('setNickname')
    setNickname(client: Socket, nickname: string): void {
        const { roomId } = client.data;
        client.to(roomId).emit('getMessage', {
            id: null,
            nickname: '안내',
            message: `"${client.data.nickname}"님이 "${nickname}"으로 닉네임을 변경하셨습니다.`,
        });
        client.data.nickname = nickname;
    }

    //채팅방 목록 가져오기
    @SubscribeMessage('getChatRoomList')
    getChatRoomList(client: Socket, payload: any) {
        client.emit('getChatRoomList', this.ChatRoomService.getChatRoomList());
    }

    //채팅방 생성하기
    @SubscribeMessage('createChatRoom')
    createChatRoom(client: Socket, roomName: string) {
        //이전 방이 만약 나 혼자있던 방이면 제거
        if (
            client.data.roomId != 'room:lobby' &&
            this.server.sockets.adapter.rooms.get(client.data.roomId).size == 1
        ) {
            this.ChatRoomService.deleteChatRoom(client.data.roomId);
        }

        this.ChatRoomService.createChatRoom(client, roomName);
        return {
            roomId: client.data.roomId,
            roomName: this.ChatRoomService.getChatRoom(client.data.roomId)
                .roomName,
        };
    }

    //채팅방 들어가기
    @SubscribeMessage('enterChatRoom')
    enterChatRoom(client: Socket, roomId: string) {
        //이미 접속해있는 방 일 경우 재접속 차단
        if (client.rooms.has(roomId)) {
            return;
        }
        //이전 방이 만약 나 혼자있던 방이면 제거
        if (
            client.data.roomId != 'room:lobby' &&
            this.server.sockets.adapter.rooms.get(client.data.roomId).size == 1
        ) {
            this.ChatRoomService.deleteChatRoom(client.data.roomId);
        }
        this.ChatRoomService.enterChatRoom(client, roomId);
        return {
            roomId: roomId,
            roomName: this.ChatRoomService.getChatRoom(roomId).roomName,
        };
    }
}

https://github.com/for2gles/realtime-chat/blob/8e701b1f69b659ebb9f6de48b5c291166be89da1/src/chatBackEnd/chatBackEnd.gateway.ts

 

아직 중복코드도 많고, 손봐야 할 부분이 아직 많다.

 

이렇게 Backend를 구성해 주고, 프론트도 제작하였다.

프론트 디자인은 

https://bootsnipp.com/snippets/1ea0N

이 템플릿을 활용하였다.

 

index.ejs

<html>
    <head>
        <link href="//maxcdn.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" rel="stylesheet" id="bootstrap-css">
        <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.css" type="text/css" rel="stylesheet">
        <link href="/style.css" type="text/css" rel="stylesheet">
        <script src="https://cdn.socket.io/3.1.3/socket.io.min.js" integrity="sha384-cPwlPLvBTa3sKAgddT6krw0cJat7egBga3DJepJyrLl4Q9/5WLra3rrnMcyTyOnh" crossorigin="anonymous"></script>
        <script src="//cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
        <script src="//maxcdn.bootstrapcdn.com/bootstrap/4.1.1/js/bootstrap.min.js"></script>
    </head>
    
    <body>
        <div class="container">
            <h3 class=" text-center">Messaging</h3>
            <div class="messaging">
                <div class="inbox_msg">
                    <div class="inbox_people">
                        <div class="headind_srch">
                            <div class="recent_heading">
                                <h4>Chat Room</h4>
                            </div>
                            <div class="chatroom_btn_place">
                                <button type="button" class="chatroom_btn setNickname"> <i class="fa fa-cog" aria-hidden="true"></i> </button>
                                <button type="button" class="chatroom_btn createChatRoom"> <i class="fa fa-plus" aria-hidden="true"></i> </button>
                            </div>
                        </div>
                        <div class="inbox_chat chatRoomList">
                        </div>
                    </div>
                    <div class="mesgs">
                        <div class="msg_history chat">
                        </div>
                        <div class="type_msg">
                            <div class="input_msg_write">
                                <input type="text" class="write_msg" placeholder="Type a message" />
                                <button class="msg_send_btn sendMessage" type="button"><i class="fa fa-paper-plane-o" aria-hidden="true"></i></button>
                            </div>
                        </div>
                    </div>
                </div>
            
            
            <p class="text-center top_spac"> Design by <a target="_blank" href="https://www.linkedin.com/in/sunil-rajput-nattho-singh/">Sunil Rajput</a></p>
            
            </div>
        </div>
    </body>
    <script>
        let myInfo = {
            nickname: null,
            id: null,
            room: {
                roomId: null,
                roomName: null
            }
        }
        const socket = io('http://localhost:5000');
        socket.on('connect', function() {
            console.log('Connected');
            //연결 완료 후 로컬스토리지를 확인하여 닉네임 세팅
            const nickname = localStorage.getItem('nickname');
            socket.emit('setInit', { nickname }, response =>
                {
                    myInfo.nickname = response.nickname;
                    myInfo.id = socket.id
                    myInfo.room = response.room;
                    $('.nickname').val(myInfo.nickname);
                    $('.roomName').text(myInfo.room.roomName);
                }
            );
            socket.emit('getChatRoomList', null);
        });
        socket.on('getMessage', function({id, nickname, message}) {
            let html = '';
            if(myInfo.id == id){
                html += '<div class="outgoing_msg">';
                html += '<div class="sent_msg">';
                html += '<p>'+message+'</p>';
                html += '<span class="time_date"> '+nickname+'    | 11:01 AM    |    June 9</span>';
                html += '</div>';
                html += '</div>';
            } else {
                html += '<div class="incoming_msg">';
                html += '<div class="received_msg">';
                html += '<div class="received_withd_msg">';
                html += '<p>'+message+'</p>';
                html += '<span class="time_date"> '+nickname+'    | 11:01 AM    |    June 9</span>';
                html += '</div>';
                html += '</div>';
                html += '</div>';
            }
            $('.chat').append(html);
            $('.chat').scrollTop($('.chat')[0].scrollHeight);
        });
        socket.on('getChatRoomList', function(response) {
            let html = '';
            for(const {roomId, roomName} of Object.values(response)){
                html += '<div class="chat_list '+(myInfo.room.roomId === roomId ? 'active_chat' : 'enterChatRoom')+'" data-roomId="'+roomId+'">';
                html += '<div class="chat_people">';
                html += '<div class="chat_ib">';
                html += '<h5>'+roomName+'</h5>';
                html += '</div>';
                html += '</div>';
                html += '</div>';
            }
            $('.chatRoomList').html(html);
        });
        socket.on('disconnect', function() {
            $('.chatRoomList').html('');
            console.log('Disconnected');
        });

        //채팅방 생성
        $('.createChatRoom').on('click', function(){
            const roomName = prompt('채팅방 이름을 입력해주세요.');
            if(!roomName){
                return false;
            }
            $('.chat').html('');
            socket.emit('createChatRoom', roomName,  (res) => {
                if(!res) return;
                myInfo.room = res;
                $('.roomName').text(myInfo.room.roomName);
                $('.chat').html('');
            });
            socket.emit('getChatRoomList', null);
        })

        //채팅방 입장
        $(document).on('click', '.enterChatRoom', function(){
            const thisRoomId = $(this).attr('data-roomId');
            socket.emit('enterChatRoom', thisRoomId, (res) => {
                if(!res) return;
                myInfo.room = res;
                $('.roomName').text(myInfo.room.roomName);
                $('.chat').html('');
            });
            socket.emit('getChatRoomList', null);
        })

        //메시지 전송
        $('.sendMessage').on('click', function(){
            socket.emit('sendMessage', $('.write_msg').val());
            $('.write_msg').val('');
        })

        //닉네임 설정
        $('.setNickname').on('click', function(){
            const nickname = prompt('변경할 닉네임을 입력해주세요.');
            if(!nickname){
                return false;
            }
            socket.emit('setNickname', nickname);
            localStorage.setItem('nickname', nickname);
        })
    </script>
</html>

👇CSS 원문보기

더보기

style.css

.container {
    max-width: 1170px;
    margin: auto;
}
img {
    max-width: 100%;
}
.inbox_people {
    background: #f8f8f8 none repeat scroll 0 0;
    float: left;
    overflow: hidden;
    width: 40%;
    border-right: 1px solid #c4c4c4;
}
.inbox_msg {
    border: 1px solid #c4c4c4;
    clear: both;
    overflow: hidden;
}
.top_spac {
    margin: 20px 0 0;
}

.recent_heading {
    float: left;
    width: 40%;
}
.srch_bar {
    display: inline-block;
    text-align: right;
    width: 60%;
}
.headind_srch {
    padding: 10px 29px 15px 20px;
    overflow: hidden;
    border-bottom: 1px solid #c4c4c4;
}

.recent_heading h4 {
    color: #05728f;
    font-size: 25px;
    margin: auto;
}
.srch_bar input {
    border: 1px solid #cdcdcd;
    border-width: 0 0 1px 0;
    width: 80%;
    padding: 2px 0 4px 6px;
    background: none;
}
.srch_bar .input-group-addon button {
    background: rgba(0, 0, 0, 0) none repeat scroll 0 0;
    border: medium none;
    padding: 0;
    color: #707070;
    font-size: 18px;
}
.srch_bar .input-group-addon {
    margin: 0 0 0 -27px;
}

.chat_ib h5 {
    font-size: 15px;
    color: #464646;
    margin: 0 0 8px 0;
}
.chat_ib h5 span {
    font-size: 13px;
    float: right;
}
.chat_ib p {
    font-size: 14px;
    color: #989898;
    margin: auto;
}
.chat_img {
    float: left;
    width: 11%;
}
.chat_ib {
    float: left;
    padding: 0 0 0 15px;
    width: 88%;
}

.chat_people {
    overflow: hidden;
    clear: both;
}
.chat_list {
    border-bottom: 1px solid #c4c4c4;
    margin: 0;
    padding: 18px 16px 10px;
    cursor: pointer;
}
.inbox_chat {
    height: 550px;
    overflow-y: scroll;
}

.active_chat {
    background: #ebebeb;
}

.incoming_msg_img {
    display: inline-block;
    width: 6%;
}
.received_msg {
    display: inline-block;
    padding: 0 0 0 10px;
    vertical-align: top;
    width: 92%;
}
.received_withd_msg p {
    background: #ebebeb none repeat scroll 0 0;
    border-radius: 3px;
    color: #646464;
    font-size: 14px;
    margin: 0;
    padding: 5px 10px 5px 12px;
    width: 100%;
}
.time_date {
    color: #747474;
    display: block;
    font-size: 12px;
    margin: 8px 0 0;
}
.received_withd_msg {
    width: 57%;
}
.mesgs {
    float: left;
    padding: 30px 15px 0 25px;
    width: 60%;
}

.sent_msg p {
    background: #05728f none repeat scroll 0 0;
    border-radius: 3px;
    font-size: 14px;
    margin: 0;
    color: #fff;
    padding: 5px 10px 5px 12px;
    width: 100%;
}
.outgoing_msg {
    overflow: hidden;
    margin: 26px 0 26px;
}
.sent_msg {
    float: right;
    width: 46%;
}
.input_msg_write input {
    background: rgba(0, 0, 0, 0) none repeat scroll 0 0;
    border: medium none;
    color: #4c4c4c;
    font-size: 15px;
    min-height: 48px;
    width: 100%;
}

.type_msg {
    border-top: 1px solid #c4c4c4;
    position: relative;
}
.msg_send_btn {
    background: #05728f none repeat scroll 0 0;
    border: medium none;
    border-radius: 50%;
    color: #fff;
    cursor: pointer;
    font-size: 17px;
    height: 33px;
    position: absolute;
    right: 0;
    top: 11px;
    width: 33px;
}
.chatroom_btn_place {
    position: relative;
}
.chatroom_btn {
    background: #05728f none repeat scroll 0 0;
    border: medium none;
    border-radius: 50%;
    color: #fff;
    cursor: pointer;
    font-size: 17px;
    height: 33px;
    float: right;
    margin-left: 10px;
    width: 33px;
}
.messaging {
    padding: 0 0 50px 0;
}
.msg_history {
    height: 516px;
    overflow-y: auto;
}

이렇게 구성하였다.

 

현재까지 제작된 부분이다.

 

  1. 채팅방을 생성할 수 있다.
  2. 채팅방 목록 페이지에서 본인의 닉네임을 설정하고, 변경은 목록페이지에서만 가능하다.
  3. 채팅방에 접속하면 이전 채팅글을 볼 수 있고, 100개단위로 이전 채팅글을 불러올 수 있다.
  4. 글자수 혹은 json size의 제한을 통해 바이너리형태의 직접적인 데이터 전송, xss 등의 보안부분도 추가해준다.
  5. 닉네임등록시 금지어를 설정하여 비속어 등을 제한한다.
  6. 사이트를 껐다 킬 경우에도 채팅방에 닉네임은 유지된다.

1번, 6번 항목을 완료하였고 나머지 항목 작업이 필요하다.

2번은 원래 목록보는 페이지, 채팅방 페이지를 구분 할 계획이었는데, 쪽지나 메시지 형태의 디자인도 나쁘지 않는것같아서 제하도록 하겠다.

 

다음에는 중복코드들을 정리하고, 읽기 좋도록 코드를 깔끔하게 정리한 후 주석작업들을 하고, 4번, 5번 작업을 진행해보고자 한다.


위 코드들은 모두 깃허브에 올려놓았습니다.

https://github.com/for2gles/realtime-chat

반응형

댓글