반응형
실시간 채팅
실시간으로 DB 데이터 가져오는 방법
- Get 요청을 매초 마다 보내기
- 단점: 유저가 많아질 수록 서버가 힘들어 함. (DDos 공격으로도 됨)
- SSE (Server Sent Events)
- 서버가 일방적으로 데이터 실시간 전송 가능
- 서버와 유저간 실시간 소통 채널 열기
// SSE
// 아래와 같이 작성하면 /sse 로 GET 요청하면 실시간으로 채널이 오픈됨
app.get("/msg/:id", isLogin, (req, res) => {
// Header를 아래와 같이 수정해달라는 코드
res.writeHead(200, {
Connection: "keep-alive",
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
});
// 일반적으로 GET, POST 요청은 1번 요청시 1번 응답을 하게 되는데
// 위와 같이 코드를 작성하게 되면 여러번 응답이 가능하게 됨
// 유저에게 데이터 전송은 event: 보낼데이터 이름 + \\n
// 유저에게 데이터 전송은 data: 보낼데이터 + \\n\\n <--- 개행을 두번 작성하는게 안정적이라고 한다.
db.collection("msg")
// params로 채팅방 id를 가져오는 이유는 get 요청이기에
// 두가지 방법인 params와 query string 둘 중 params를 사용했음.
.find({ parent: req.params.id })
.toArray()
.then((r) => {
res.write("event: chat\\n");
// 다만 서버에서 실시간 전송시 문자자료만 전송이 가능하다.
// 그래서 toArray()로 배열 상태로 들어오기에 JSON 형식으로 변경 해주기.
res.write("data: " + JSON.stringify(r) + "\\n\\n ");
});
});
<!doctype html>
<html>
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/css/bootstrap.min.css"
integrity="sha384-xOolHFLEh07PJGoPkLv1IbcEPTNtaed2xpHsD9ESMhqIYd0nLMwNLD69Npy4HI+N" crossorigin="anonymous">
<link rel="stylesheet" href="/public/main.css">
<title>NodeJS+MongoDB</title>
<style>
.chat-content {
height: 450px;
overflow-y: scroll;
padding: 10px;
}
.chat-content li {
margin-top: 10px;
list-style: none;
}
.text-small {
font-size: 12px;
color: gray;
margin-bottom: 0;
}
.chat-box {
background: #eee;
padding: 5px;
border-radius: 5px;
float: left;
}
.mine {
float: right;
}
</style>
</head>
<body>
<%- include('nav.html')%>
<div class="container p-4 detail">
<div class="row">
<div class="col-3">
<ul class="list-group chat-list">
<% for (let i = 0; i < data.length; i++ ) { %>
<li class="list-group-item" data-id="<%= data[i]._id %>">
<h6> <%= data[i].title %> </h6>
<p class="text-small"> <%= data[i].nickname[0] %>, <%= data[i].nickname[1] %>님의 채팅</p>
</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>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/js/bootstrap.bundle.min.js"
integrity="sha384-Fy6S3B9q64WdZWQUiU+q4/2Lc9npb8tCaSX9FK7E8HnRr0Jz8D6OP9dO5Vg3Q9ct" crossorigin="anonymous">
</script>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<!-- SSE -->
<script>
</script>
<script>
let clickData = ''
let eventSource
$('.list-group-item').click((e) => {
console.log(e.currentTarget);
clickData = e.currentTarget.dataset.id;
// SSE
// 해당 변수에 뭔가 있다면 즉 참가하고 있는 채팅방이 있다면 그 채팅을 닫아달라는 코드
if (eventSource != undefined) {
eventSource.close()
$('.chat-content').html('') // 내부 비워주기
}
// 유저의 데이터 수신은 아래와 같다.
// new EventSource('/경로') <--- 특별한 get 요청 방법
eventSource = new EventSource('/msg/' + clickData)
// eventSourece로 요청후 데이터를 받고자 한다면
// <------- 'test'는 아까 server.js 에서 작명헀던 것 -> event: 보낼데이터 이름 + \n
eventSource.addEventListener('chat', (
e) => {
console.log(JSON.parse(e.data)); // <--- 서버에서 보낸 데이터
JSON.parse(e.data).map((chatData) => {
$('.chat-content').append("<li><span class='chat-box'>" + chatData.content + "</span></li>")
})
})
})
$('#send').click((e) => {
const sendData = {
parent: clickData,
content: $('#chat-input').val()
}
$.ajax({
method: 'post',
url: '/msg',
data: sendData
}).then((r) => {
console.log('전송 성공');
})
})
</script>
<script>
$(function () {
$("#file").on('change', function () {
readURL(this); // 선택한 파일을 읽고, 해당 파일의 데이터 URL을 생성 후 파일을 처리
});
});
function readURL(input) {
if (input.files && input.files[0]) {
var reader = new FileReader();
reader.onload = function (e) {
$('#preview').attr('src', e.target.result);
}
reader.readAsDataURL(input.files[0]);
}
}
</script>
</body>
</html>
실시간 채팅 내역 업데이트
- 잘 안되는점
- 채팅방 입장 후 처음 남기는 채팅은 갱신이 안되고
- 이후부터 남기는 채팅은 갱신이 되지만 처음 작성한 데이터는 db에 저장만 되고 실시간 갱신이 안됨
- 해당 문제는 우선 질문에 뒀음
- 해결됐음...
- ㅋㅋㅋ 공백 하나 있는것 때문에 그런거였음...
// 아래 코드에서
// res.write("data: " + JSON.stringify(r) + "\n\n "); 를
// res.write("data: " + JSON.stringify(r) + "\n\n"); 로 변경해줘야한다
// 즉 코드를 작성할때는 공백까지 잘 확인하자! ... ㅋㅋ
// SSE
// 아래와 같이 작성하면 /sse 로 GET 요청하면 실시간으로 채널이 오픈됨
app.get("/msg/:id", isLogin, (req, res) => {
// Header를 아래와 같이 수정해달라는 코드
res.writeHead(200, {
Connection: "keep-alive",
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
});
db.collection("msg")
.find({ parent: req.params.id })
.toArray()
.then((r) => {
res.write("event: test\n");
res.write("data: " + JSON.stringify(r) + "\n\n ");
});
const pipeline = [{ $match: { "fullDocument.parent": req.params.id } }];
const collection = db.collection("msg");
const changeStream = collection.watch(pipeline);
changeStream.on("change", (r) => {
res.write("event: test\n");
res.write(`data: ${JSON.stringify([r.fullDocument])}\n\n`);
});
});
// SSE
// 아래와 같이 작성하면 /sse 로 GET 요청하면 실시간으로 채널이 오픈됨
app.get("/msg/:id", isLogin, (req, res) => {
// Header를 아래와 같이 수정해달라는 코드
res.writeHead(200, {
Connection: "keep-alive",
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
});
// 일반적으로 GET, POST 요청은 1번 요청시 1번 응답을 하게 되는데
// 위와 같이 코드를 작성하게 되면 여러번 응답이 가능하게 됨
// 유저에게 데이터 전송은 event: 보낼데이터 이름 + \\n
// 유저에게 데이터 전송은 data: 보낼데이터 + \\n\\n <--- 개행을 두번 작성하는게 안정적이라고 한다.
// 지금은 메세지들을 한번 찾아서 보내고 끝임.
// 그리고 DB는 수동적이기에 DB가 업데이트 될 때마다 유저에게 데이터를 전송해 달라는 것을 잘 못함.
// 그래서 MongoDB Change Stream 을 사용하게 되면 변경시 전송이 가능하게 된다.
// Change Stream 설정하게 되면, DB 변동시 -> 서버에게 알려줌 -> 유저에게 보낼 수 있다.
db.collection("msg")
// params로 채팅방 id를 가져오는 이유는 get 요청이기에
// 두가지 방법인 params와 query string 둘 중 params를 사용했음.
.find({ parent: req.params.id })
.toArray()
.then((r) => {
res.write("event: test\\n");
// 다만 서버에서 실시간 전송시 문자자료만 전송이 가능하다.
// 그래서 toArray()로 배열 상태로 들어오기에 JSON 형식으로 변경 해주기.
res.write("data: " + JSON.stringify(r) + "\\n\\n ");
});
// 내가 원하는 document만 감시하고 싶으면 match 안에 특정 값을 찾을 수 있는 값을 넣어주기
// 여기서 key 값으로 문자로 특정 key 값 앞에 fullDocument. 를 붙여줘야 한다.
const pipeline = [{ $match: { "fullDocument.parent": req.params.id } }];
const collection = db.collection("msg");
const changeStream = collection.watch(pipeline); // watch 하게 되면 실시간으로 감시하게 됨
changeStream.on("change", (r) => {
// console.log(r.fullDocument); // 전체 메시지 확인 방법
// 아래에서 [] 안에 넣어주는 이유는 chat.ejs 에서 배열로 다루기 때문
res.write("event: test\\n");
res.write(`data: ${JSON.stringify([r.fullDocument])}\\n\\n`);
});
});
<!doctype html>
<html>
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/css/bootstrap.min.css"
integrity="sha384-xOolHFLEh07PJGoPkLv1IbcEPTNtaed2xpHsD9ESMhqIYd0nLMwNLD69Npy4HI+N" crossorigin="anonymous">
<link rel="stylesheet" href="/public/main.css">
<title>NodeJS+MongoDB</title>
<style>
.chat-content {
height: 450px;
overflow-y: scroll;
padding: 10px;
}
.chat-content li {
margin-top: 10px;
list-style: none;
}
.text-small {
font-size: 12px;
color: gray;
margin-bottom: 0;
}
.chat-box {
background: #eee;
padding: 5px;
border-radius: 5px;
float: left;
}
.mine {
float: right;
}
</style>
</head>
<body>
<%- include('nav.html')%>
<div class="container p-4 detail">
<div class="row">
<div class="col-3">
<ul class="list-group chat-list">
<% for (let i = 0; i < data.length; i++ ) { %>
<li class="list-group-item" data-id="<%= data[i]._id %>">
<h6> <%= data[i].title %> </h6>
<p class="text-small"> <%= data[i].nickname[0] %>, <%= data[i].nickname[1] %>님의 채팅</p>
</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>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/js/bootstrap.bundle.min.js"
integrity="sha384-Fy6S3B9q64WdZWQUiU+q4/2Lc9npb8tCaSX9FK7E8HnRr0Jz8D6OP9dO5Vg3Q9ct" crossorigin="anonymous">
</script>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<!-- SSE -->
<script>
</script>
<script>
let clickData = ''
let eventSource
$('.list-group-item').click((e) => {
console.log(e.currentTarget);
clickData = e.currentTarget.dataset.id;
// SSE
// 해당 변수에 뭔가 있다면 즉 참가하고 있는 채팅방이 있다면 그 채팅을 닫아달라는 코드
if (eventSource != undefined) {
eventSource.close()
$('.chat-content').html('') // 내부 비워주기
}
// 유저의 데이터 수신은 아래와 같다.
// new EventSource('/경로') <--- 특별한 get 요청 방법
eventSource = new EventSource('/msg/' + clickData)
// eventSourece로 요청후 데이터를 받고자 한다면
// <------- 'test'는 아까 server.js 에서 작명헀던 것 -> event: 보낼데이터 이름 + \n
eventSource.addEventListener('test', (
e) => {
console.log(JSON.parse(e.data)); // <--- 서버에서 보낸 데이터
JSON.parse(e.data).map((chatData) => {
$('.chat-content').append("<li><span class='chat-box'>" + chatData.content + "</span></li>")
})
})
})
$('#send').click((e) => {
const sendData = {
parent: clickData,
content: $('#chat-input').val()
}
$.ajax({
method: 'post',
url: '/msg',
data: sendData
}).then((r) => {
console.log('전송 성공');
})
})
</script>
<script>
$(function () {
$("#file").on('change', function () {
readURL(this); // 선택한 파일을 읽고, 해당 파일의 데이터 URL을 생성 후 파일을 처리
});
});
function readURL(input) {
if (input.files && input.files[0]) {
var reader = new FileReader();
reader.onload = function (e) {
$('#preview').attr('src', e.target.result);
}
reader.readAsDataURL(input.files[0]);
}
}
</script>
</body>
</html>
'NodeJS+MongoDB' 카테고리의 다른 글
NodeJS+MongoDB Part3 - NodeJS+Express 서버 & React 연동 (0) | 2023.07.03 |
---|---|
NodeJS+MongoDB Part3 - 실시간 채팅 (Socket.io) (0) | 2023.07.03 |
NodeJS+MongoDB Part3 - 채팅 (0) | 2023.06.29 |
NodeJS+MongoDB Part3 - 이미지 업로드 & 이미지 서버 만들기 (0) | 2023.06.28 |
NodeJS+MongoDB Part3 - 게시판, API 관리 (0) | 2023.06.27 |