채팅 기능 구현

 

 

기본 동작은 이해했으니 진도를 쭉 나가겠습니다.

구현해야하는 기본 기능을 먼저 확인해보겠습니다.

 

 

> 추가할 기능 

 

TODO

- 사용자가 들어오고 나가는 부분을 캐치하여 서버가 안내하는 말을 노출한다.

- 메세지를 전송하고 받아야한다.

- 스타일을 입힌다.

 

 

> 사용자가 들어오고 나가는 부분을 캐치하여 서버가 안내하는 말을 노출한다.

 

입장할 때 작성했던 connection이 기억나는가요? 

퇴장할 때 이벤트도 같은 곳에 추가합니다.

사용자가 입장 시(소켓연결) 발생하는 이벤트 호출에 대한 바인딩 작업은 app.js의 connect 콜백함수 내부에서 작성되었습니다.

// app.js

io.sockets.on('connection', function(socket){
    socket.on('newUserConnect', function(name){
        socket.name = name;
        var message = name + '님이 접속했습니다';

        io.sockets.emit('updateMessage', {
            name : 'SERVER',
            message : message
        });
    });

    socket.on('disconnect', function(){
        var message = socket.name + '님이 퇴장했습니다';
        socket.broadcast.emit('updateMessage', {
            name : 'SERVER',
            message : message
        });
    });
});

 

코드 설명 

 

> newUserConnect 접속한다 -> 대화명 저장 -> 메세지 설정 -> 메세지 업데이트 함수호출

newUserConnect와 매우 유사한 동작이기 때문에 코드또한 비슷합니다.

socket.on('disconnect', function(){
    var message = socket.name + '님이 퇴장했습니다';

    socket.broadcast.emit('updateMessage', {
        name : 'SERVER',
        message : message
    });
});

다만

emit시에 io.sockets을 쓰던 newUserConnect와 다르게

disconnect시에는 socket.broadcast라는 객체를 사용합니다.

 

여기서 io.sockets과 socket.broadcast의 차이를 알 수 있습니다. 

io.sockets은 나를 포함한 전체 소켓이고

socket.broadcase은 나를 제외한 전체 소켓을 뜻합니다.

 

내가 접속을 종료하는데 나에게 emit할 필요가 없기 때문입니다.

 

 

> 나를 제외한 전체 소켓에 updateMessage 이벤트를 호출하며 data를 객체리터럴로 전달합니다.

disconnect 이벤트 역시 서버에서 전달하는 것이므로 name에는 SERVER로 적어줍니다.

socket.broadcast.emit('updateMessage', {
    name : 'SERVER',
    message : message
});

 

서버를 종료했다가 시작한 후 브라우저 두개의 창에서 각각 localhost:8080으로 접속하여 줍니다. 

// cdm

node app.js

그 후 하나의 창을 닫으면 열려있는 창에서 disconnect시 문구가 확인됩니다.

 

> 메세지를 전송하고 받아야한다.

 

이제 구조를 전체적으로 수정하겠습니다.

// index.html

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>chat-app</title>
</head>
<body>
    <div>
        <div id="info"></div>
        <div id="chatWindow"></div>
        <div>
            <input id="chatInput" type="text">
            <button id="chatMessageSendBtn">전송</button>
        </div>
    </div>

<script src="/socket.io/socket.io.js"></script>
<script src="/js/index.js"></script>
</body>
</html>

 

코드 설명

 

>

info 

서버가 전달하는 텍스트를 노출하는 영역으로 현재 작업한 코드에서는 connect, disconnect 시에 SERVER가 updataMessage를 호출하고 있습니다.

chatWindow 

사용자간의 채팅 텍스트를 노출하는 영역

chatInput 

사용자가 채팅 텍스트를 작성할 input

chatMessageSendBtn 

사용자가 채팅 텍스트를 작성 후 전송할 버튼

<div>
    <div id="info"></div>
    <div id="chatWindow"></div>
    <div>
        <input id="chatInput" type="text">
        <button id="chatMessageSendBtn">전송</button>
    </div>
</div>

 

// index.js

'use strict';

var socket = io();

socket.on('connect', function(){
    var name = prompt('대화명을 입력해주세요.', '');
    socket.emit('newUserConnect', name);
});

socket.on('updateMessage', function(data){
    if(data.name === 'SERVER'){
        var info = document.getElementById('info');
        info.innerHTML = data.message;
    }else{

    }
});

var sendButton = document.getElementById('chatMessageSendBtn');
var chatInput = document.getElementById('chatInput');

sendButton.addEventListener('click', function(){
    var message = chatInput.value;
    
    if(!message) return false;

    socket.emit('sendMessage', {
        message
    });

    chatInput.value = '';
});

 

코드 설명

 

> 사용자가 input에 채팅 텍스트를 입력 후 전송 버튼을 클릭했을 때, 실행될 함수를 작성해줍니다.

var sendButton = document.getElementById('chatMessageSendBtn');
var chatInput = document.getElementById('chatInput');

sendButton.addEventListener('click', function(){
    var message = chatInput.value;
    
    if(!message) return false;
   
    socket.emit('sendMessage', {
        message
    });

    chatInput.value = '';
});

 

 

> message는 input의 value값이 되며 메세지가 비었을 경우는 전송되면 안되니까 return false 해줍니다.

var message = chatInput.value;

if(!message) return false;

 

 

> 메세지가 비어있지않고 정상적이면 sendMessage라는 이벤트를 호출하며

사용자가 입력한 텍스트를 파라미터로 전달합니다.

(sendMessage 이벤트 호출 시 실행될 함수는 현재 작업한 적이 없기 때문에 app.js에서 추가 작업해줘야합니다.)

socket.emit('sendMessage', {
    message
});

 

 

> emit 후에 input의 valuer 값을 비워줍니다.

비워주지않으면 전송 후에도 input에 메세지가 그대로 있습니다.

   chatInput.value = '';
});

 

// app.js

io.sockets.on('connection', function(socket){
    socket.on('newUserConnect', function(name){
        socket.name = name;
        
        io.sockets.emit('updateMessage', {
            name : 'SERVER',
            message : name + '님이 접속했습니다.'
        });
    });

    socket.on('disconnect', function(){
        io.sockets.emit('updateMessage', {
            name : 'SERVER',
            message : socket.name + '님이 퇴장했습니다.'
        });
    });

    socket.on('sendMessage', function(data){
        data.name = socket.name;
        io.sockets.emit('updateMessage', data);
    });
});

 

코드 설명

 

> index.js에서 호출한 sendMessage 이벤트 함수입니다.

채팅 텍스트를 파라미터로 전송하였는데,

name값이 있어야 updateMessage의 데이터 구조에 맞기 때문에(또한 채팅시 name값이 있어야하기에) sendMessage에서 data.name값을 담아서 updateMessage를 호출해줍니다.

최초 소켓 연결 시 newUserConnect 함수에서 socket.name값을 저장했기 때문에 여기서 사용해줍니다.

socket.on('sendMessage', function(data){
    data.name = socket.name;
    io.sockets.emit('updateMessage', data);
});

 

 

> updateMessage 부분을 수정해주겠습니다.

index.js

'use strict';

var socket = io();

socket.on('connect', function(){
    var name = prompt('대화명을 입력해주세요.', '');
    socket.emit('newUserConnect', name);
});

var chatWindow = document.getElementById('chatWindow');
socket.on('updateMessage', function(data){
    if(data.name === 'SERVER'){
        var info = document.getElementById('info');
        info.innerHTML = data.message;

        setTimeout(() => {
            info.innerText = '';
        }, 1000);

    }else{
        var chatMessageEl = drawChatMessage(data);
        chatWindow.appendChild(chatMessageEl);
    }
});

function drawChatMessage(data){
    var wrap = document.createElement('p');
    var message = document.createElement('span');
    var name = document.createElement('span');

    name.innerText = data.name;
    message.innerText = data.message;

    name.classList.add('output__user__name');
    message.classList.add('output__user__message');

    wrap.classList.add('output__user');
    wrap.dataset.id = socket.id;
   
    wrap.appendChild(name);
    wrap.appendChild(message);

    return wrap;
}

var sendButton = document.getElementById('chatMessageSendBtn');
var chatInput = document.getElementById('chatInput');
sendButton.addEventListener('click', function(){
    var message = chatInput.value;

    if(!message) return false;    

    socket.emit('sendMessage', {
        message
    });

    chatInput.value = '';
});

 

 

>> 채팅이 작성될 객체를 가져와서 updateMessage 내부에 if else로 분기해줍니다.

if에는 서버에서 작성되어 전달되어오는 텍스트를 위한 작업,

그리고 else에는 사용자가 작성하여 전달되어오는 텍스트를 위한 작업입니다.

이건 사용자 입력(updateMessage)이 있을때마다 반복됩니다.

var chatWindow = document.getElementById('chatWindow');

socket.on('updateMessage', function(data){
    if(data.name === 'SERVER'){
        var info = document.getElementById('info');
        info.innerHTML = data.message;

        setTimeout(() => {
            info.innerText = '';
        }, 1000);
        
    }else{
        var chatMessageEl = drawChatMessage(data);
        chatWindow.appendChild(chatMessageEl);
    }
});

 

 

>> 서버에서 전달되어오면 간단히 innerHTML에 메세지만 삽입해줍니다.

그리고 1초뒤에 안내메세지이기 때문에 메세지가 지워질 수 있도록 setTimeout으로 설정해줍니다.

var info = document.getElementById('info');
info.innerHTML = data.message;

setTimeout(() => {
    info.innerText = '';
}, 1000);

 

 

>> 사용자가 전달한 텍스트는 drawChatMessage()함수를 통해 객체를 생성해 chatWindow에 삽입해줍니다.

var chatMessageEl = drawChatMessage(data);
chatWindow.appendChild(chatMessageEl);

 

 

>> 채팅은 텍스트 갯수가 계속 늘어나야하기 때문에 객체로 만들어 append 해줍니다.

function drawChatMessage(data){
    var wrap = document.createElement('p');
    var message = document.createElement('span');
    var name = document.createElement('span');

    name.innerText = data.name;
    message.innerText = data.message;

    name.classList.add('output__user__name');
    message.classList.add('output__user__message');

    wrap.classList.add('output__user');
    wrap.dataset.id = socket.id;    

    wrap.appendChild(name);
    wrap.appendChild(message);

    return wrap;
}

 

>> data를 인자로 받습니다. 

data에는 data.name(대화명) 과 data.message (대화텍스트)가 담겨있습니다.

function drawChatMessage(data){

 

 

>> createElement 메소드를 사용해서 p태그와 span 태그들을 생성합니다.

wrap = 전체를 감싸줄 객체

message = 메세지를 담을 객체

name = 대화명을 담을 객체

var wrap = document.createElement('p');
var message = document.createElement('span');
var name = document.createElement('span');

 

 

>> innerText를 통해 대화명과 대화를 객체에 담아줍니다.

name.innerText = data.name;
message.innerText = data.message;

 

 

>> 객체를 컨트롤 하기 위해 class나 id를 추가해줍니다.

name.classList.add('output__user__name');
message.classList.add('output__user__message');

wrap.classList.add('output__user');
wrap.dataset.id = socket.id;

 

 

>> wrap 객체안에 대화명과 메세지를 담은 객체를 넣어주고 wrap 객체를 return 해줍니다. 

리턴된 객체는 updateMassage에서 채팅창에 넣어줍니다.

wrap.appendChild(name);
wrap.appendChild(message);

return wrap;

 

 

서버를 종료했다가 다시 실행시켜서 두개의 탭을 열어 접속합니다.

node app.js

 

 

서버가 전달하는 텍스트의 영역은

객체를 생성하면서 삽입하는게 아니라 텍스트만 단순히 덮여쓰고 있으므로 줄이 늘어나지 않습니다.

 

 

사용자가 입력하는 대화내용은 대화를 입력하는 만큼 추가됩니다. 

drawChatMessage 함수를 통해 객체를 생성하여 삽입하기 때문입니다.

 

 

지금까지 진행한 부분의 흐름을 정리해보겠습니다.

 

-입장 시

connect (index.js) -> newUserConnect (app.js) -> updateMessage (index.js)

소켓연결, 대화명입력받음 ->  대화명저장, 메세지 작성, 메세지 전달 -> 브라우저에 데이터 삽입

 

-대화 시

click (index.js) -> sendMessage (app.js) -> updateMessage (index.js) 

클릭, 메세지 작성 -> 대화명저장, 메세지 전달 -> 브라우저에 데이터 삽입

 

-퇴장 시

disconnect (app.js) -> updateMessage (index.js)

소켓종료, 메세지작성, 메세지 전달 -> 브라우저에 데이터 삽입

 

 

여기까지 간단한 기본 기능을 추가한 채팅앱이었습니다.

 

 

 

외관 꾸미기

 

css를 추가해서 조금 더 나은 모습으로 바꾸어보겠습니다.

src하위로 css폴더를 생성하고 그 아래 index.css파일을 만들겠습니다.

 

폴더구조는 아래와 같습니다.

 

index.html의 태그들에 class들을 추가하겠습니다.

id로는 js를 컨트롤하고 class로는 style를 컨트롤하도록 나누어 작업하기 위함입니다.

// index.html

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <link rel="stylesheet" href="./css/index.css">
    <title>chat-app</title>
</head>
<body>
    <div class="app__wrap">
        <div id="info" class="app__info"></div>
        <div id="chatWindow" class="app__window"></div>
        <div class="app__input__wrap">
            <input id="chatInput" type="text" class="app__input" autofocus placeholder="대화를 입력해주세요.">
            <button id="chatMessageSendBtn" class="app__button">전송</button>
        </div>
    </div>

<script src="/socket.io/socket.io.js"></script>
<script src="/js/index.js"></script>
</body>
</html>

 

코드 설명

 

> input에 autofocus나 placeholder의 속성을 주겠습니다.

 

autofocus는 브라우저를 열면 포커스가 인풋에 커서가 자동으로 가게 하는 속성이고

placeholder는 인풋의 힌트를 주는 속성이며 선택사항입니다.

<input id="chatInput" type="text" class="app__input" autofocus placeholder="대화를 입력해주세요.">

 

> index.css의 링크를 로드하는것도 잊지말아야합니다.

<link rel="stylesheet" href="./css/index.css">

 

> 스타일은 따로 설명하지 않겠지만 디자인 감각이 없기때문에... 

material design에서 color tool 부분을 그대로 따라했습니다.

그대로 복사하여 사용하시면 됩니다.

index.css

@charset "utf-8";

.app__wrap{
    margin: 0 auto;
    padding: 50px 0 0;
    position: relative;
    width: 100%;
    max-width: 350px;
    min-width: 200px;
    font-size: 14px;
    border-top: 20px solid #5c007a;
    box-sizing: border-box;
    box-shadow: 1px 1px 5px rgba(0,0,0,0.1);
}

.app__info{
    position: absolute;
    top: 0;
    width: 100%;
    height: 50px;
    text-align: center;
    line-height: 50px;
    color: #fff;
    background: #8e24aa;
}

.app__window{
    overflow-y: auto;
    padding: 10px 20px;
    height: 400px;
    background: #e1e2e1;
}

.output__user{
    margin: 0;
    margin-bottom: 10px;
}

.output__user__name{
    margin-right:10px;
    font-weight:700;
}

.app__input__wrap{
    padding: 10px;
    background: #f5f5f6;
}

.app__input__wrap:after{
    content:'';
    display:block;
    clear:both;
}

.app__input{
    float: left;
    display: block;
    width: 80%;
    height: 25px;
    border: 1px solid #ccc;
    box-sizing: border-box;
}

.app__button{
    float: left;
    display: block;
    width: 20%;
    height: 25px;
    border: 1px solid #ccc;
    border-left: 0;
    background: #fff;
    box-sizing: border-box;
    cursor: pointer;
}

 

 

스타일 적용이 마치면 아래와 같아집니다.

 

> 대화가 길어지면 스크롤이 생기게되는데, 스크롤이 계속 상단을 보고 있습니다.

새로운 채팅 텍스트가 생성되면 하단을 볼수있게 스크립트 하나 추가하겠습니다.

// index.js

var chatWindow = document.getElementById('chatWindow');
socket.on('updateMessage', function(data){
    if(data.name === 'SERVER'){
        var info = document.getElementById('info');
        info.innerHTML = data.message;       

        setTimeout(() => {
            info.innerText = '';
        }, 1000);

    }else{
        var chatMessageEl = drawChatMessage(data);
        chatWindow.appendChild(chatMessageEl);

        chatWindow.scrollTop = chatWindow.scrollHeight;
    }
});

 

코드 설명

 

> updateMessage 가 실행될 때 사용자 대화일 경우, else 부분이 실행되고

chatWindow의 스크롤을 chatWindow의 스크롤 높이만큼 내려주는 부분입니다.

chatWindow.scrollTop = chatWindow.scrollHeight;

 

 

이렇게하면 길어져도 대화 입력 시 스크롤이 올라가지않고 항상 최신 대화를 볼 수 있습니다.

 

 

 


 

 

최종 코드

 

// index.html

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <link rel="stylesheet" href="./css/index.css">
    <title>chat-app</title>
</head>
<body>
    <div class="app__wrap">
        <div id="info" class="app__info"></div>
        <div id="chatWindow" class="app__window"></div>
        <div class="app__input__wrap">
            <input id="chatInput" type="text" class="app__input" autofocus placeholder="대화를 입력해주세요.">
            <button id="chatMessageSendBtn" class="app__button">전송</button>
        </div>
    </div>

<script src="/socket.io/socket.io.js"></script>
<script src="/js/index.js"></script>
</body>
</html>

 

// index.js

'use strict';

var socket = io();
var chatWindow = document.getElementById('chatWindow');
var sendButton = document.getElementById('chatMessageSendBtn');
var chatInput = document.getElementById('chatInput');

socket.on('connect', function(){
    var name = prompt('대화명을 입력해주세요.', '');
    socket.emit('newUserConnect', name);
});

socket.on('updateMessage', function(data){
    if(data.name === 'SERVER'){
        var info = document.getElementById('info');
        info.innerHTML = data.message;

        setTimeout(() => {
            info.innerText = '';
        }, 1000);

    }else{
        var chatMessageEl = drawChatMessage(data);
        chatWindow.appendChild(chatMessageEl);

        chatWindow.scrollTop = chatWindow.scrollHeight;
    }
});

sendButton.addEventListener('click', function(){
    var message = chatInput.value;

    if(!message) return false;
   
    socket.emit('sendMessage', {
        message
    });

    chatInput.value = '';
});

function drawChatMessage(data){
    var wrap = document.createElement('p');
    var message = document.createElement('span');
    var name = document.createElement('span');

    name.innerText = data.name;
    message.innerText = data.message;

    name.classList.add('output__user__name');
    message.classList.add('output__user__message');

    wrap.classList.add('output__user');
    wrap.dataset.id = socket.id;

    wrap.appendChild(name);
    wrap.appendChild(message);

    return wrap;
}

 

// app.js

const express = require('express');
const http = require('http');
const app = express();
const server = http.createServer(app);
const fs = require('fs');
const io = require('socket.io')(server);

app.use(express.static('src'));

app.get('/', function(req, res){
    fs.readFile('./src/index.html', (err, data) => {
        if(err) throw err;

        res.writeHead(200, {
            'Content-Type' : 'text/html'
        })
        .write(data)
        .end();
    });
});

io.sockets.on('connection', function(socket){
    socket.on('newUserConnect', function(name){
        socket.name = name;

        io.sockets.emit('updateMessage', {
            name : 'SERVER',
            message : name + '님이 접속했습니다.'
        });
    });

    socket.on('disconnect', function(){
        io.sockets.emit('updateMessage', {
            name : 'SERVER',
            message : socket.name + '님이 퇴장했습니다.'
        });
    });

    socket.on('sendMessage', function(data){
        data.name = socket.name;
        io.sockets.emit('updateMessage', data);
    });
});

server.listen(8080, function(){
    console.log('서버 실행중...');
});

 

// index.css

@charset "utf-8";

.app__wrap{
    margin: 0 auto;
    padding: 50px 0 0;
    position: relative;
    width: 100%;
    max-width: 350px;
    min-width: 200px;
    font-size: 14px;
    border-top: 20px solid #5c007a;
    box-sizing: border-box;
    box-shadow: 1px 1px 5px rgba(0,0,0,0.1);
}

.app__info{
    position: absolute;
    top: 0;
    width: 100%;
    height: 50px;
    text-align: center;
    line-height: 50px;
    color: #fff;
    background: #8e24aa;
}

.app__window{
    overflow-y: auto;
    padding: 10px 20px;
    height: 400px;
    background: #e1e2e1;
}

.output__user{
    margin: 0;
    margin-bottom: 10px;
}

.output__user__name{
    margin-right:10px;
    font-weight:700;
}

.app__input__wrap{
    padding: 10px;
    background: #f5f5f6;
}

.app__input__wrap:after{
    content:'';
    display:block;
    clear:both;
}

.app__input{
    float: left;
    display: block;
    width: 80%;
    height: 25px;
    border: 1px solid #ccc;
    box-sizing: border-box;
}

.app__button{
    float: left;
    display: block;
    width: 20%;
    height: 25px;
    border: 1px solid #ccc;
    border-left: 0;
    background: #fff;
    box-sizing: border-box;
    cursor: pointer;
}

 

 

 

이것으로 간단한 채팅앱 구현을 마무리하겠습니다.

 

 

 

 

+ Recent posts