[11일차] Node.js, MongoDB - Part 3 : 실시간(SSE) 채팅 기능 만들기

흑염소·2023년 9월 20일

📕 채팅방 만들기

이론적으로는 댓글용 Collection 하나를 새로 만들어서 모든 댓글을 저장하면 된다.
근데 Collection 안에 대충 넣어버리면 무슨 글에 달린 댓글인지 구분이 모호하므로
모든 댓글은 부모 게시물 정보를 담아서 함께 넣는다.
부모를 기록하는 작업을 전문용어로 "document 또는 collection끼리 관계를 맺는다~" 라고 한다.
어떤 게시물에 종속된 게시물이 필요할 때 이런 식으로 관계 지정을 해보자.


추후에 게시물A의 댓글만 불러오고 싶을 때 {부모:'게시물A'} 가진 댓글만 가져와달라고 query를 줄 수도 있다.

채팅기능 어떻게 만드나

위에서 설명한 게시물 + 댓글을 그대로 사용하면 된다.
채팅방을 고르고 해당 채팅방의 댓글들이 쭉 나오는 형식임
글작성 + 댓글과 똑같은 기능이다. (여기다 실시간 기능을 곁들이면 됨)

채팅방에 들어갈 정보를 담은 document는 다음과 같다.

(DB에 저장될 채팅방 게시물 document 모습)

{
  member : [채팅당한 유저의 _id, 채팅건 유저의 _id],
  date : 지금날짜,
  title : 채팅방이름(아무거나)
}

list.ejs 파일 수정

만들어뒀던 게시판 리스트에서 해당 리스트 글의 채팅버튼을 클릭하면 말 걸수있게 설정해보자.
기존 list.ejs에서 버튼 만들고 POST 요청하는 기능 구현한다.

(list.ejs)

<div class="container">
    <ul class="list-group">
      <% for (var i=0; i < posts.length ; i++){ %>
        <li class="list-group-item">
          <p>글번호 : <%= posts[i]._id %></p>
          <h4>할일 제목 : <%= posts[i].제목 %></h4>
          <p>할일 마감날짜 : <%= posts[i].날짜 %></p>
          <button class="btn btn-danger delete" data-id="<%= posts[i]._id %>">삭제</button>
          <button class="btn btn-secondary chat" data-id="<%= posts[i].작성자 %>">채팅하기</button>
        </li>
        <% } %>
    </ul>
</div>


<script>
    $('.chat').click(function(e){
      var _id = e.target.dataset.id;
      $.post('/chatroom', {당한사람id : _id})
      .then(()=>{
        console.log('채팅방 게시물 생성완료')
      })
    });
</script>
  1. list.ejs 파일엔 작성자 정보가 숨겨진 버튼을 추가
  2. 그 버튼 누르면 post 요청을 보내달라고 코드 짜기 (ajax post 요청 쉽게하려면 $.post 써도 됨)
  3. post 요청시 현재 글 작성한 유저의 _id까지 실어서 보내기

서버에 요청하기

서버에서 post 요청 받은거 DB로 보내주자.

(server.js)

app.post('/chatroom', function(요청, 응답){

  var 저장할거 = {
    title : '무슨무슨채팅방',
    member : [ObjectId(요청.body.당한사람id), 요청.user._id],
    date : new Date()
  }

  db.collection('chatroom').insertOne(저장할거).then(function(결과){
    응답.send('저장완료')
  });
});
  1. 서버는 /chatroom으로 post 요청을 받으면 데이터를 만들어준다.
  2. 채팅당한사람, 현재 로그인한 사람의 _id들은 [ ] 배열 안에 담는다.
  3. 날짜는 new Date() 해서 저장한다.
  4. 저장 완료하면 '저장완료' 메세지를 보내주고 추가로 에러체크 같은 것도 해주면 좋다.

[참고]

참고로 db에 insert, find, delete 이런거 하고 콜백함수 대신 .then() 붙일 수 있습니다.
깔끔해보인다면 이렇게 합시다. 에러는 .catch() 붙이면 됩니다.

chat.ejs 파일 보내주기

chat.ejs 파일 안엔 내 유저_id 가 있는 채팅방 게시물들을 다 보여줘야한다.

(server.js)

app.get('/chat', 로그인했니, function(요청, 응답){ 

  db.collection('chatroom').find({ member : 요청.user._id }).toArray().then((결과)=>{
    console.log(결과);
    응답.render('chat.ejs', {data : 결과})
  })

}); 
  1. 서버는 /chat으로 접속하면 chat.ejs 만든 ejs파일 보내준다.
    근데 ejs 파일 안에 내가 속한 채팅방 게시물들이 들어가야함

  2. 내가 속한 게시물을 전부 찾으라고 find() 사용.
    [ ] 안에 있는 것들도 그냥 저렇게 찾을 수 있음

  3. 찾은 결과는 응답.render('chat.ejs', {data : 결과}) 해서 전달함

<ul class="list-group chat-list">

  <% for (var i=0; i < data.length ; i++){ %>
    <li class="list-group-item" data-id="<%= data[i]._id %>">
      <h6> <%= data[i].title %> </h6>     
      <h6 class="text-small"> <%= data[i].member[0] %> </h6>
    </li>
  <% } %>
   
  <li class="list-group-item">
    <h6>채팅방1</h6>
    <h6 class="text-small">채팅방아이디</h6>
  </li>

</ul> 

서버에서 받아온 데이터 연결해주고
반복되는 리스트 부분은 반복문으로 돌려주면 끝

📗 메세지 발행

채팅메시지 저장하기

POST 요청하기

누가 전송버튼을 누르면 서버로 메세지를 전달하고 그걸 DB에 저장한다.

(chat.ejs)
...
<input class="form-control" id="chat-input">
<button class="btn btn-secondary" id="send">전송</button>

<script>
$('#send').click(function(){
  var 채팅내용 = $('#chat-input').val();   //1, 2
  var 보낼거 = {
    parent: 지금누른채팅방id,
    content: 채팅내용,
  };
  
  //3
  $.post('/message', 보낼거).then((a) => {  
     console.log(a)
  });

});


//4
var 지금누른채팅방id;

$('.list-group-item').click(function (){
   $(this).css('background-color', '#eee');
   지금누른채팅방id = $(this).attr('data-id');
});

</script>
  1. input 안의 텍스트 담김
  2. 클릭이벤트 발생한 채팅방의 id(=parent), 채팅내용 정보를 변수에 담음
  3. 담긴 정보들을 POST로 전송함

서버는 데이터를 받았으니 이제 DB에 저장시키면 된다.

DB에 저장하기

(server.js)

app.post('/message', 로그인했니, function(요청, 응답){
  var 저장할거 = {
    parent : 요청.body.parent,
    userid : 요청.user._id,
    content : 요청.body.content,
    date : new Date(),
  }
  db.collection('message').insertOne(저장할거)
  .then((결과)=>{
    응답.send(결과);
  })
}); 
  1. 누군가 /message로 post요청을 하면
  2. 유저가 보낸 데이터를 활용해서 DB에 저장하고 싶은 데이터를 만든다.
  3. DB에 insertOne() 해서 집어넣는다.

📘 서버와의 실시간 소통 (SSE)

서버에서 채팅메세지 가져오기

채팅방을 누르면 해당 채팅메세지를 모두 가져오는 기능을 구현하자.
DB에서 "parent: 지금선택한 채팅방 게시물_id" 인 게시물을 모두 찾아달라고 하면 된다.
메세지를 계속 실시간으로 가져오는 방법은,

  1. 1초마다 서버에게 메세지 달라고 요청을 또 하기
  2. 서버랑 유저간 지속적인 소통채널 열기

여기서는 서버 부담이 덜한 2번으로 진행한다.
서버가 유저에게 실시간으로 정보를 보내게 하자.

get,post같은 http 요청은 1회 요청하면 1회 응답하고 끝인데
1회 응답이 아닌, 지속적으로 서버에 응답을 하고 싶은 경우,

  1. Header 라는 정보의 connection 항목을 keep-alive로 설정해주고
  2. 응답.write('안녕하세요') 이렇게 보내면 계속 유저에게 지속적으로 응답이 가능함.

/message/어쩌구 로 요청을 하면 실시간 소통 채널을 열도록 해보자.

(server.js)

app.get('/message/:parentid', 로그인했니, function(요청, 응답){

  응답.writeHead(200, {
    "Connection": "keep-alive",
    "Content-Type": "text/event-stream",
    "Cache-Control": "no-cache",
  });

  응답.write('event: test\n');
  응답.write('data: 안녕하세요\n\n');

});
  1. 서버는 응답.writeHead() 어쩌구 저렇게 쓰면 지속적 소통채널 개설 완료임
  2. 유저에게 계속 메세지를 보내고 싶을 때마다 응답.write() 하면 됨
  3. 거기다가 event: 이벤트명을 잘 작성하고 data: 전달할내용을 쓰면 됨

그리고 한줄 끝나면 \n(엔터키)를 잘 넣으면 된다.
간단한 문자 등을 바로바로 유저에게 전송하게 됐다.

(chat.ejs)

var 지금누른채팅방id;
var eventSource;   //일단변수 

$('.list-group-item').click(function(){
  지금누른채팅방id = this.dataset.id;

  //프론트엔드에서 실시간 소통채널 여는법 
  eventSource = new EventSource('/message/' + 지금누른채팅방id);
  eventSource.addEventListener('test', function (e){
    console.log(e.data);
  });

});
  1. 유저는 GET요청 이런게 아니라 new EventSource('/message/' + 지금누른채팅방id);
    이런 코드를 실행하면 서버에서 만들어놓은 실시간 채널에 입장가능하다.

  2. eventSource.addEventListener('서버에서작명한이벤트명') 이런 코드를 쓰면 서버가 보낸 데이터를 수신할 수 있다.
    그럼 서버가 응답.write() 할 때마다 내부 코드를 실행해준다.

  3. e.data 안에는 서버가 보낸 data : 전달할내용이 들어있다.

이제 실시간 채널 개설시 DB에서 기존에 저장되어있는 메세지들을 가져와서 보여주자.

(server.js)

app.get('/message/:parentid', 로그인했니, function(요청, 응답){

  응답.writeHead(200, {
    "Connection": "keep-alive",
    "Content-Type": "text/event-stream",
    "Cache-Control": "no-cache",
  });

  db.collection('message').find({ parent: 요청.params.parentid }).toArray()
  .then((결과)=>{
    console.log(결과);
    응답.write('event: test\n');
    응답.write(`data: ${JSON.stringify(결과)}\n\n`);
  });

});
  1. 서버는 누군가 /message/어쩌구로 실시간 채널에 접속하면

  2. DB에서 { parent : 어쩌구 } 를 가진 게시물을 다 찾아서 보내줌
    (근데 실시간 채널이니까 응답.write 이용해서 보내줌)

  3. 근데 찾은 자료는 [{ }, { } ... ] 이런 형태다.
    그걸 보내고 싶으면 JSON.stringify() 안에 넣어서 보내면 됨.
    왜냐면 실시간 채널 이용할 때 문자만 전송가능하기 때문입니다.

이제 채팅방 클릭할 때 마다 DB에서 요청된 데이터 전부 꺼내다가 보여준다.

📙 DB 변동사항 실시간 업데이트

지금은 DB에 메세지가 하나 추가되어도 아무런 반응이 없다.
DB에 메세지가 추가되면 그걸 서버가 응답.write()로 바로 전송해줘야한다.
그런 코드를 짜고 싶다면 MongoDB의 change stream 기능을 쓰면 된다.

change stream

원래 데이터베이스는 수동적이다.
서버가 명령하면 데이터 입출력만 얌전히 해줄 뿐이다.
change stream 기능을 이용하면 알아서 동적으로 DB 변동사항을 감시해준다.
그리고 변동이 생기면 서버에게 업데이트 사항을 알려준다.
자기주도적으로 일하는 DB가 되는데 실시간 서비스 만들 때 쓰면 편리하다.

(server.js)

const 찾을문서 = [
    { $match: { fullDocument.name : 123 } }
];
  
const changeStream = db.collection('message').watch(찾을문서);

changeStream.on('change', (result) => {
    console.log(result.fullDocument);
});

change stream 기능을 사용할 때 기본 코드 이렇게 써놓고 시작하면 된다.

  1. 우선 컬렉션에서 원하는 document만 감시하고 싶으면 $match 이런걸 이용해서 조건식을 적어줌
    (위 코드는 DB에서 {name : 123} 인 document만 변동사항을 감시해줌)

  2. 그리고 watch(찾을문서)를 붙여준다.

  3. 그리고 on('change') 어쩌구 이벤트 리스너를 붙여주면 DB에서 변동사항이 생길 때마다 콜백함수 내부 코드를 실행해준다.

  4. 변동사항은 result.fullDocument 안에 저장되어있다.
    change stream에서 뭐 할 때는 어쩌구.fullDocument라고 자주 사용한다.
    (왜 result.fullDocument 써야하는지 궁금하면 언제나 console.log로 출력해보면 됨)

최종 서버 코드

마무리로, 지금까지 작성하던 app.get('/message:/:parentid') 부분을 아래처럼 수정하면 된다.

// 실시간 서버 개통하기
app.get('/message/:parentid', 로그인했니, function(요청, 응답) {

    응답.writeHead(200, {
        "Connection" : "keep-alive",
        "Content-Type" : "text/event-stream",
        "Cache-Control" : "no-cache",
    });

    // 조건에 일치하는 대화내용 가져오기
    db.collection('message').find({ parent : 요청.params.id }).toArray()
    .then((결과)=>{
        응답.write('event: test\n'); // event는 이벤트명 작성
        응답.write('data:' + JSON.stringify(결과) + '\n\n'); // data는 전달할내용 작성
    })

    // Change Stream 사용하기
    // -> 값 업데이트 되면 바로 반영하는 기능 (메세지 입력시 작동)
    const 찾을문서 = [
        { $match: { 'fullDocument.parent' : 요청.params.parentid } }
    ];
      
    const changeStream = db.collection('message').watch(찾을문서); // watch -> 변화 감시
    
    changeStream.on('change', (result) => {
        console.log(result.fullDocument); // 추가된 내용
        응답.write('event: test\n');
        응답.write('data:'+JSON.stringify( [result.fullDocument] )+'\n\n');
        // -> test 이벤트에 다시한번 추가시켜준다.
    });
    
});

지금까지 한거 요약해보면,

  1. 누군가 app.get('/message/:parentid') 이런 실시간 소통채널에 입장하면 DB에서 게시물 전부 가져옴
  2. change stream을 이용해서 DB 감시도 동시에 해줌
  • 우선 { parent : 요청.params.parentid } 인 게시물들만 감시함
  • 그런 게시물들에 변동사항이 생기면 [result.fullDocument] 이걸 유저에게 보내줌
  • 물론 [], {} 이런 자료들은 JSON으로 바꿔서 보내야함

이제 DB에 누군가 메세지를 추가로 전송해도 바로바로 유저에게 보내준다.
나머지 짜잘한 ejs html 부분만 수정하면 채팅기능 끝!

profile
매일 TIL 중인 비전공자 프론트 개발자

0개의 댓글