본문 바로가기
웹개발/Node.js

[Node.js] 채팅 기능 구현하기

by 철없는민물장어 2023. 3. 2.
728x90
반응형

당근마켓을 이용해본 적 있다면 익숙할 기능.

채팅하기 기능이다.

 

어떠한 게시물에서 '채팅하기'버튼을 누르면

글 작성자와 1대1 채팅을 시작할 수 있다.

그리고, /chat 접속시 내가 참여중인 채팅방을 보여주자.

 


채팅기능 구현을 위한 단계는 이렇다.

 

채팅방 개설.

1. list.ejs 에 '채팅하기'버튼을 추가하고, 버튼 클릭시 post요청(채팅방 개설)

2. server.js에서 1번의 post요청 처리

3. chat.ejs 디자인

4. server.js에서 /chat으로의 get요청 처리(이 때, 현재 로그인한 사용자가 속해있는 채팅방들의 정보를 전달)

 

여기까지 했다면 이제부터 채팅방의 메세지들을 처리하는 코드를 추가.

5. /chat에서 메세지 전송시 post요청(어떤 채팅방에서,언제,어떤 내용인지 등을 DB에 저장)

6. server.js에서 5번의 post요청 처리(message콜렉션에 데이터저장)

 

이제 각 채팅방을 열 면 해당하는 메세지가 나오도록 하는데, 이 때 SSE방식을 이용한 실시간소통을 한다.

7. server.js에서 실시간채널 열고 데이터 보내기

8. chat.ejs에서 실시간채널 접속하여 데이터 받아 처리하기

 

 

이제 한 단계씩 살펴보자.


1. list.ejs 에 '채팅하기'버튼을 추가하고, 버튼 클릭시 /chatroom으로  post요청(채팅방 개설)

 

(list.ejs)
. . .
<button class="btn btn-danger chatting" data-id="<%=posts[i].writer %>" data-title="<%=posts[i].title %>">채팅하기</button>
. . .
<script>
	$('.chatting').click(function (e) {
        var 글작성자 = e.target.dataset.id;
        var 현재요소 = $(this);
        
        var 제목 = e.target.dataset.title;

        $.post('/chatroom',{'글작성자':글작성자,'제목':제목}).then(()=>{
          console.log('완료')
        })
        
      })
</script>

 

버튼 태그를 이용하여 채팅하기 버튼을 추가했고,

태그 안에는 data-id, data-title 데이터를 넣어서

글 작성자(writer), 글 제목(title)을 담았다.

 

스크립트태그 내에서는 .chatting 요소의 이벤트리스너를 추가하여 채팅하기 버튼의 클릭을 감지한다.

버튼태그에 심어둔 dataset으로 글작성자,제목 데이터를 가져와서

/chatroom경로로 데이터와함께 post요청을 한다.

 


2. server.js에서 1번의 post요청 처리
(server.js)

app.post('/chatroom',로그인했니,function(요청,응답){
    var 저장할거 = {
        member:[ObjectId(요청.body.글작성자), 요청.user._id],
        date: new Date(),
        title: 요청.body.제목
    }
    db.collection('chatroom').insertOne(저장할거).then((결과)=>{
        응답.send('성공')
    })
    
})

 

/chatroom으로의 post요청을 처리한다. 이때 로그인상태인지 검증하는 미들웨어 '로그인했니'를 사용한다.

이제 DB의 chatroom 콜렉션에 채팅방 정보를 저장하는데,

 

저장할 내용은 다음과 같다.

member: [글작성자(요청.body.글작성자), 채팅 건 사람(요청.user._id)]

date: new Date()를 이용하여 현재날짜 저장

title: 글제목(요청.body.제목)

 


3. chat.ejs 디자인
<%- include('nav.html') %>

    <div class="container p-4 detail">

        <div class="row">
          <div class="col-3">
            <ul class="list-group chat-list">
              <p><%= currentUserId %></p>
              <% for(let i=0;i< data.length ;i++){ %>
                <li class="list-group-item" data-id="<%=data[i]._id%>" data-user="<%= currentUserId %>">
                <h6><%= data[i].title %></h6>
                <h6 class="text-small"><%= data[i].member[0]%></h6>
              </li>
              <% } %>
             </ul>
           </div>
      
           <div class="col-9 p-0">
             <div class="chat-room">
                <ul class="list-group chat-content">
                  
                </ul>
              <div class="input-group">
                <input class="form-control" id="chat-input">
                <button class="btn btn-secondary" id="send">전송</button>
              </div>
            </div>
          </div>
        </div>
      
      </div>

 

ejs문법을 이용하여 자바스크립트 for문을 활용,

전달받은 데이터를 채팅방 목록으로 출력한다.

(dataset으로 넣어둔 데이터들은 추후 이용하기 위함이다)


4. server.js에서 /chat으로의 get요청 처리(이 때, 현재 로그인한 사용자가 속해있는 채팅방들의 정보를 전달)

 

app.get('/chat',로그인했니,function(요청,응답){
    db.collection('chatroom').find({member:요청.user._id}).toArray(function(에러,결과){
       // console.log(결과)
        응답.render('chat.ejs',{data:결과, currentUserId: 요청.user._id})
    })
    
})

chat.ejs파일을 만들어두었으니

/chat경로로 접속시 chat.ejs파일을 렌더링해준다.

이 때 채팅방은 로그인한 유저만 사용할 수 있으므로, 미들웨어에 '로그인했니'라는 함수를 사용했다.

또한 렌더링시 현재 유저가 속한 채팅방들의 정보를 넘겨주어야 하므로

db의 find함수를 사용하여 데이터를 찾고 toArray를 이용하여 배열로 변환해서 그 결과값을 data로 보내주었다.

(currentUserId는 chat.ejs파일에서 현재유저가 누구인지 알기 위해 추가해 넣었다. 왜 알고싶었냐면..나중에 메세지들을 불러올 때, 이 메세지가 내가 보낸 메세지인지 상대방이 보낸 메세지인지 판별하고싶었기 때문이다.)


여기까지 했다면 이제부터 채팅방의 메세지들을 처리하는 코드를 추가.

 

5. /chat에서 메세지 전송시 post요청(어떤 채팅방에서,언제,어떤 내용인지 등을 DB에 저장)
(chat.ejs)

<script>
    $('#send').click(function(){
            var 채팅내용 = $('#chat-input').val();
            var 보낼거 = {
              parent : 채팅방id,
              content: 채팅내용,

            }
            $.post('/message',보낼거).then((결과)=>{
              console.log('전송완');
            })
	      })
</script>

메세지 전송버튼 클릭을 감지하는 이벤트리스너를 추가하고,

/message경로로 post요청을 한다.

이 때 보낼 데이터에는 {parent: 채팅방의 id, content: 채팅내용}을 넣어야 한다.

 

이 메세지정보는 DB상의 별도의 콜렉션: message에 저장될 것이기 때문에

메세지정보를 꺼내서 사용할 때, 채팅방id를 저장해두어야 

각 채팅방마다의 메세지를 구별해 사용할 수 있다.


6. server.js에서 5번의 post요청 처리(message콜렉션에 데이터저장)
app.post('/message',로그인했니,function(요청,응답){
    
    var 저장할거={
        parent: 요청.body.parent,
        content : 요청.body.content,
        userid: 요청.user._id,
        date: new Date()
    }
    db.collection('message').insertOne(저장할거).then(()=>{

    })
})

아까전 5번 과정에서 /message경로로 {parent,content}의 정보를 post요청했다.

여기에 현재 시각(date)와 메세지를 발행한 유저 정보(요청.user._id)를 추가로 묶어 db에 저장한다.


이제 채팅방 메세지들을 보내서 chat.ejs에서 보여주면 될 것 같은데..

일반적인 get,post요청은 1번 요청하면 끝이기때문에 실시간으로 만들기 힘들다.

 

그래서 SSE방식으로 서버에서 실시간채널을 열어본다.
app.get('/message/:id',로그인했니,function(요청,응답){

    응답.writeHead(200,{
        "Connection":"keep-alive",
        "Content-Type": "text/event-stream",
        "Cache-Control":"no-cache"
    });
    //console.log(요청.params.id)
    db.collection('message').find({parent: 요청.params.id }).toArray((에러,결과)=>{
        // console.log(결과+","+요청.user._id)
        var 보낼거 = 결과;
        console.log(보낼거)
        응답.write('event: test\n')//보낼데이터이름
        응답.write(`data: ${JSON.stringify(결과)}\n\n`);//데이터내용. 문자형만 전송가능하므로 JSON.stringify사용

    })

    const pipeline = [
        {$match:{'fullDocument.parent': 요청.params.id }}
    ];
    const collection = db.collection('message');
    const changeStream = collection.watch(pipeline);
    changeStream.on('change',(result)=>{
        응답.write('event: test\n')//보낼데이터이름
        응답.write('data:'+JSON.stringify([result.fullDocument])+'\n\n');
    })
})

/message/:id로 채팅방id를 파라미터로 받는다.

 

writeHead()를 위와같이 작성하면 지속적인 응답이 가능하다.

(작성 후

응답.write('event: abc\n')

응답.write('data: {defg}\n\n')

형식으로 보내면, abc라는 이벤트명으로 {defg}데이터를 보낸다. )

그런데 응답으로 데이터를 보낼 때, 문자열 형태여야 하기 때문에

JSON.stringify를 이용하여 문자열형태로 변환하고 보내야 한다.

 

그리하여 서버에서 채팅방id로 검색한 메시지내용들을 가지고 와 응답.write로 데이터를 보내주게 된다.

 

그런데 db에 메세지가 추가되는 경우에 서버가 자동으로 응답을 보내주는것은 아니다.

그래서 MongoDB의 changeStream을 이용해 db의 데이터가 변동될 때 데이터를 추가로 보내주어야 한다.

const pipeline = [
        {$match:{'fullDocument.parent': 요청.params.id }}
    ];
    const collection = db.collection('message');
    const changeStream = collection.watch(pipeline);
    changeStream.on('change',(result)=>{
        응답.write('event: test\n')//보낼데이터이름
        응답.write('data:'+JSON.stringify([result.fullDocument])+'\n\n');
    })

그것이 이 부분이다.

(참고: changeStream의 result는 괄호에 싸서 보내야함. 원래 괄호에 싸여있는 형태여야 하는데 얘는 기본적으로 없어서..)

 

이렇게 작성하면 'message'라는 콜렉션에서 변동사항이 생기면 데이터를 보내준다.


chat.ejs에서 실시간 채널에 입장하기
      let 채팅방id;
      let eventSource;
      $('.list-group-item').click(function(){
        채팅방id = this.dataset.id;
        //console.log('현재채팅방id'+채팅방id)

        var currentUser = this.dataset.user;//현재유저
        console.log(currentUser+"이것이 현재유저")
        if(eventSource != undefined){
          eventSource.close();//소통채널이 이미 열려있으면 닫기
        }

        $('.chat-content').html('');//기존내용 삭제

        eventSource = new EventSource('/message/'+채팅방id);
        eventSource.addEventListener('test',function(e){
          console.log(JSON.parse(e.data));
          var data = JSON.parse(e.data)
          
          data.forEach(function(element){
            if(currentUser == element.userid)
              {$('.chat-content').append(`<li><span class="chat-box mine">${element.content}</span></li>`);
            }else{
              $('.chat-content').append(`<li><span class="chat-box">${element.content}</span></li>`);
            }
          });
      })
    })

실시간채널 접속은

new EventSource('경로').addEventListener('이벤트명',콜백함수)로 할 수 있다.

이 때 서버에서 보낸 데이터는 e.data에 있는데(e는 콜백함수의 인자)

서버에서 데이터를 보낼 때 JSON.stringify를 이용하여 문자열 형태로 변환하여 보냈기 때문에

JSON.parse()함수를 이용하여 다시 원래대로 바꿔 사용하면 된다.

 

그리고 이렇게 받은 데이터들은 

chat-content요소에 html을 추가하는 형식으로 메세지를 띄울 수 있다.

 

 

 

 

728x90
반응형

댓글