본문 바로가기

블록체인 기반 핀테크 및 응용 SW개발자 양성과정 일기

[56일차 복습] Javascript websocket 채팅 구현하기 / 자바스크립트 웹소켓 node.js express 채팅 / 채팅 안읽음 표시 구현

반응형

 

 

 

로그인을 한 사람에게만 채팅 가능하도록 만들기 

-> auth.js 미들웨어를 추가

 

server.js - auth 추가

app.get('/chat', auth, (req, res) => {
    res.render('chat.html')
})

auth.js - res.redirect 를 -> res.json으로 바꾸기 

토큰이 없으면 (로그인이 안되었으면 ) -> res 응답을 json 으로 보낸다 {result:false, msg:'로그인이 필요합니다'} 라는 객체를 (요 객체는 express가 자동으로 String으로 변환해서 보내준다.) 

    let {AccessToken} = req.cookies // 클라이언트의 cookie.accesstoken
    if(AccessToken == undefined){
        console.log('로그인을 진행해주세요')
        //res.redirect('/?msg=로그인을 진행해주세요')
        res.json({result:false, msg:'로그인이 필요합니다'})
        return
    }

쿠키 값을 지우고 localhost:3000 -> 채팅 버튼을 누르면 위의 내용이 String값으로 그대로 뜬다. 

 

 

 

 

 

 

chat.js

const chatBtn=document.querySelector('#chatBtn');
const chatRoom = document.querySelector('#chatRoom');


chatBtn.addEventListener('click',async ()=>{
    let url = 'http://localhost:3000/chat'
    let options = {
        method:'GET'
    }
    let response = await fetch (url,options);
    let result = await response.text();

    chatRoom.innerHTML=result;
})

fecth 로 가져온 promise 객체 반환값에 .text() 로 안 body의 내용 그대~로 가져옴=> 위의 브라우저에서 쓴 그대로 

만약 body의 내용을 string으로 바로 가져다 쓸 때는 .json() 사용 

 

성공시에는 html 을 넘겨주고 (html - text 형태 ) 

실패시에는 json 형태로 응답해준다

-> 요걸 사용해서 json 타입이면 - 실패 / 아니면 - 성공 알고리즘을 만들기 

 

chat.js

chatBtn.addEventListener('click',async ()=>{
    let url = 'http://localhost:3000/chat'
    let options = {
        method:'GET'
    }
    let response = await fetch (url,options);
    let res_body = await response.text();

    if(isJson(res_body)){ // 응답이 json인 경우 : 로그인 실패한 경우 
        let {result,msg} = JSON.parse(res_body);
        if (result==false) alert(msg);
        return
    }else{
        // 응답이 html 인 경우 (render잘 되면) : 로그인 성공한 경우
        chatRoom.innerHTML=res_body;
    }
})

function isJson(str){
    try{
        let json = JSON.parse(str);
        return (typeof json =='object') //return : True
    }catch(e){
        return false; 
    }
}

 

여기까지의 내용: 

'채팅' 버튼을 클릭하면 -> fetch로 /chat 요청하고 -> 받은 응답res_body를 함수 isJson에 실행 

1. (로그인 상태) 받은 응답이 html 이라면 -> JSON.parse 불가 -> catch -> return false -> res_body(응답을)  chatRoom HTML로 넣기

2. (비로그인 상태) 받은 응답(res_body)를 JSON.parse 를 하고 그 값의 타입이 '객체'라면 return True -> parse 해서 그 안의 result, msg 키값을 변수에 저장 후 , 사용   

 

 

 

 

  .text() .json() 비교  

json -> Text형태로 바꾸기 : JSON.stringify()

Text -> json 형태로 바꾸기 : JSON.parse() 

 

로그인 후 

console.log(response)

console.log(res_body) <---response.text()

-> 여기서 res_body는 html text형태라 JSON.parse 가 안됨 

 

로그인 안했을 때 

console.log(response) 

console.log(res_body))   < --- = response.text()

console.log(JSON.parse(res_body) -- res_body 가 객체 형태 String 이어서 JSON.parse 가능 

 

 

.text()말고 json()으로 작성했을 때  (대신 로그인을 안한 상태여야함. html이 들어오면 안되므로 ) 

    let response = await fetch (url,options);
    console.log(response)
    let res_body = await response.json();
    console.log(res_body)

String이 아닌 json 객체 형태로 들어와서 바로 안의 key값으로 value에 접근 가능 

즉, a= {String(객체형태)}.text();  JSON.parse(a)  == a = {Stirng(객체형태)}.json

 

 


 

 

auth.js - res.json 으로 수정 / token 삭제 추가

    if(sign != signature){
        console.log('부적절한 토큰')
        //res.redirect('/?msg=부적절한 토큰')
        res.clearCookie('AccessToken');
        res.json({result:false,msg:'부적절한 토큰입니다.'})
        return
    }
    let {userid,exp} = JSON.parse(Buffer.from(payload,'base64').toString())

    let nexttime = new Date().getTime();
    if(nexttime > exp){
        console.log('토큰 만료')
        res.clearCookie('AccessToken')
        //res.redirect('/?msg=토큰만료')
        res.json({result:false, msg:'토큰만료'})
        return
    }
    req.userid = userid;
    next()

토큰이 일치하지 않을 때, 토큰이 만료 되었을 때 응답을 redirec에서 -> res.json으로 수정 / token 삭제 추가 

-> 토큰 network 에서 임시로 변경해서 '채팅'버튼 다시 눌러봐 -> 부적절한 토큰 alert가 뜸 

 

 

 

 

 

 


 

 

채팅 한번 더 click 하면 닫히도록 만들기 

 

chat.js

let flag=undefined;

chatBtn.addEventListener('click',async ()=>{
    switch(flag){
        case true:
            chatRoom.style.display="none";
        break;
        case false:
            chatRoom.style.display="block";
        break;
        case undefined:
            getChatRoom()
        break;
    }
})

async function getChatRoom(){
    let url = 'http://localhost:3000/chat'
    let options = {
        method:'GET'
    }
    let response = await fetch (url,options);
    let res_body = await response.text();

    if(isJson(res_body)){ // 응답이 json인 경우 : 로그인 실패한 경우 
        let {result,msg} = JSON.parse(res_body);
        if (result==false) alert(msg);
        return
    }else{
        // 응답이 html 인 경우 (render잘 되면) : 로그인 성공한 경우
        chatRoom.innerHTML=res_body;
    }
}

click Event의 내용을 다른 function으로 옮기고 (코드가 너무 복잡해져서) -> 이 때 ** 옮기는 새로운 함수 : getChatRoom 에도 async 를 붙여줘야함 

flag 전역변수 =undefined 를 주고 맨 처음 load 되었을 때 flag =undefined -> switch 문에서 case undefined 가 실행 -> 함수 실행 

 

이 경우 버튼 클릭할 때마다 network에 req가 찍힘 : fetch 요청은 url 변경과 같음 (req & res 발생)

-> 서버의 소스 낭비  => 아래처럼 수정

chatBtn.addEventListener('click',async ()=>{
    switch(flag){
        case true:
            flag = false;
            chatRoom.style.display="none";
        break;
        case false:
            flag = true;
            chatRoom.style.display="block";
        break;
        case undefined:
            flag = true;
            getChatRoom()
        break;
    }
})

-> 한번 클릭하면 undefined -> true (열린 상태 ) 그뒤로 flag 값과 style 값만 변경 (req,res 는 없다! -> 리소스 효율화) 

 

 

 


css 입히기

public - css - chat.css 생성

index.html 에 css 연결

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

chat.html div 추가

<div id="chat">
    <div>
        <span class="me">안녕하세요</span>
    </div>
    <div>
        <span class="you"> 네 안녕 </span>
    </div>
</div>
<div id="chatInputArea">
    <input type="text" id="msg">
    <button id="chatSend">전송</button>
</div>

public-css-chat.css 코드 작성

#chat{
    width:400px;
    height:500px;
    background:wheat;
}

#chat>div>span{
    display:inline-block;
    padding:7px 14px;
    border-radius:15px;
}

#chat>div{
    height:30px;
    padding:5px;
}

#chat>div>span.me{
    float:right;
    background-color: yellow;
}

#chat>div>span.you{
    float:left;
    background-color: white;
}

#chatInputArea{
    width:400px;
    padding:5px 7px; 
}

#chatInputArea>input{
    width:200px;
    padding:7px 14px;
    border:1px solid 999999;
}

#chatInputArea>button{
    padding:7px 14px;
    background-color: #eee;
    border:1px solid #999999;
}


 

 

그럴듯하게 만들어짐

오른쪽 노란색 : 내가 보낸 거 

왼쪽 흰색 : 상대방이 보낸 거 

 

 

 

 


 

 

쏘켓 연결 

$npm i socket.io http

server.js   - 위의 packages 가져오는 코드 / 설정 코드/ app.listen-> server로 작성

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

Server.listen(3000, () => {    /// ***http를 가져와서 server를 만들면 꼭 server로 변경하기!!
    console.log('server start port: 3000');
})

 

 

Client쪽에 연결하기 

index.html

<script type="text/javascript" src ="/socket.io/socket.io.js"></script>

성공 

만약 안되면 저번시간에 한 것처럼 node_modules 경로 하나하나 설정 & server.js 에 app.use(express.static('node_modules') ... 이런거 써야함

 

 

성공했으니 이제 Client가 handshake to Server (소켓의 요청은 항상 Client !) 요청하는걸 Handshake라고 함 

귀엽다,,

Client 가 REQ 요청을 보낼 때는 (login 상태) REQ에 cookie 도 들어 있어서 server가 이를 복구화 시켜서 userid를 알아낼 수 있다. 

 

 

chat.js

로그인 성공한 경우에다가 함수 만들어서 socket 연결 요청 코드 작성 

const socket = io();

- 위의 코드는index.html에서 socket과 연결 script를 작성했기때문에 가능 

    }else{
        // 응답이 html 인 경우 (render잘 되면) : 로그인 성공한 경우
        chatRoom.innerHTML=res_body;
        socketChat();
    }
}

function socketChat(){
    socket.on('connect', ()=>{})
}

server.js

io.sockets.on('connection', (socket)=>{
    console.log(socket.handshake);
})

 

101 -> 소켓 연결 잘됨 

 

 

 

 

 

 

 

 

 

 

 

socket.handshake 내용

{
  headers: {
    host: 'localhost:3000',
    connection: 'keep-alive',
    'sec-ch-ua': '" Not;A Brand";v="99", "Google Chrome";v="91", "Chromium";v="91"',
    accept: '*/*',
    'sec-ch-ua-mobile': '?0',
    'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36',       
    'sec-fetch-site': 'same-origin',
    'sec-fetch-mode': 'cors',
    'sec-fetch-dest': 'empty',
    referer: 'http://localhost:3000/',
    'accept-encoding': 'gzip, deflate, br',
    'accept-language': 'ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7',
    cookie: 'user=hh; AccessToken=eyJ0cHkiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyaWQiOiJhc2RmIiwiZXhwIjoxNjIyNjUzOTU0NjA2fQ.FhmQZyd4mppRMPzv8wnqsGmZgzfvJJqIGFiAKUAFMqQ'
  },
  time: 'Thu Jun 03 2021 00:45:57 GMT+0900 (대한민국 표준시)',
  address: '::1',
  xdomain: false,
  secure: false,
  issued: 1622648757613,
  url: '/socket.io/?EIO=4&transport=polling&t=NdDPnqq',
  query: [Object: null prototype] {
    EIO: '4',
    transport: 'polling',
    t: 'NdDPnqq'
  },
  auth: {}
}

 

안에 client가 handshake를 보낼 때 보내는 REQ 요청 안에 handshake라는 키값 안에 위 내용들이 들어있다.

hadnshake 안 headers 에 cookie 가 있다. 해당 토큰값을 복구화 해서 payload안에 있는 userid값을 가져와야함 

 

 

* 실무에서는 쿠키값이 토큰말고 다른 것들이 함께 담겨있는 경우가 많다고 한다. ->  쿠키값 임의로 더 추가해주기 

network - application - 더블클릭으로 추가/ 수정

더블클릭이 안되면 console.log로 추가

 

server.js

let id; 
io.sockets.on('connection', (socket)=>{
    let cookieString = socket.handshake.headers.cookie;
    if(cookieString!=undefined){ // cookieString이 존재하면 = 로그인한 경우면
        let cookieArr=cookieString.split(';');
        cookieArr.forEach(v=>{
            let [key, value] = v.trim().split('=');
            if(key=='AccessToken'){
                let payload = value.split('.')[1];
                let {userid,exp} = JSON.parse(Buffer.from(payload,'base64').toString());
                id = userid;
            }
        })    
        console.log(id)
    }
})

크롬, 파이어폭스 각각 브라우저 열어서 다른 id로 로그인 , console.log확인 -> 각각 다른 id가 뜬다 

 

 

 


 

 

전송 버튼 눌러서 채팅 뜨도록 만들기

 

 

 

평소 js 에 따로 넣었는데 chat.js 처음 page 로딩되었을 때 실행되게 됨. document.querySelector('#msg') 이렇게 가져와도 NULL 값이 뜬다. 가장 처음 index.html -> 채팅 버튼 눌러야 chat.html 이 render 됨

해결 => chat.html -onclick 이벤트를 html에 그대로 주기

chat.html

<div id="chatInputArea">
    <input type="text" id="msg">
    <button id="chatSend" onclick="send()">전송</button>
</div>

chat.js (chat.html 과 연결된 브라우저 js, Client) 

채팅창의 value 값을 send  보내기 + msgAdd 함수이용하여 내가 보낸 값 class 'me'로 appendChild하기 


function send(){
    const msg = document.querySelector('#msg');
    socket.emit('send', msg.value);
    msgAdd(msg.value, 'me');
}

function msgAdd(msgValue, who){
    const div = document.createElement('div');
    const span = document.createElement('span');
    const chat = document.querySelector('#chat');
    span.innerHTML=msgValue;
    span.classList.add(who);
    div.appendChild(span)
    chat.appendChild(div)
}

server.js

서버에서 받은 내용 다른 clients 들에게 뿌리기 - broadcast 

                let {userid,exp} = JSON.parse(Buffer.from(payload,'base64').toString());
                id = userid;
            }
        })    
        console.log(id)
    }
    
    socket.on('send',(data)=>{
        console.log(data);
        socket.broadcast.emit('msg', data);
    })
})

chat.js

받은 'msg'로 받은 data 를 'you' class로 appendChild 함수로 ㄱ ㄱ 

function socketChat(){
    socket.on('connect', ()=>{});

    socket.on('msg', (data)=>{
        msgAdd(data,'you')
    })
}

 

그럼 각각 브라우저 안에서 서로 채팅이 된다.

 

 


 

 

이제 채팅 안읽음 표시 구현하기

 

index.html - button 에 data-value를 자바스크립트로 만들어서 수가 올라가도록 만들기 

html = data-value 는 js의 dataset.value를 의미 

 

chat.js


function socketChat(){
    socket.on('connect', ()=>{});
    
    socket.on('msg', (data)=>{
        chatBtn.dataset.value = parseInt(chatBtn.dataset.value)+1; 
        if(flag==false){
            chatBtn.innerHTML=`채팅<span style="coler:red; padding:20px;">${chatBtn.dataset.value}</span>`
        }else{

        }
        msgAdd(data,'you')
    })
}

-

chat.js 윗부분

chatBtn.addEventListener('click', ()=>{
    switch(flag){
        case true:
            flag = false;
            chatRoom.style.display="none";
        break;
        case false:
            chatBtn.innerHTML=`채팅`; 
            chatBtn.dataset.value=0;
            flag = true;
            chatRoom.style.display="block";
        break;
        case undefined:
            flag = true;
            getChatRoom()
        break;
    }
})

index.html

<button id = "chatBtn" data-value="0" >채팅</button>

 

 

문제점

채팅 버튼 클릭을 해야 -> 채팅창html render -> socket 연결요청/ 연결되어 -> 채팅 dataset value 가 보임

브라우저가 '채팅'을 클릭하기 전에는 채팅 몇개와있는지 안보임 

 

 

반응형