기본 흐름
1. 사용자가 메시지 작성 후 전송
2. 서버가 메시지를 받아서 같은 채팅방의 다른 사용자들에게 전달
3. 모든 사용자가 실시간으로 메시지 확인
Room 개념 활용
클라이언트에서 서버에 Room 입장 요청
<!-- chatDetail.ejs -->
<script>
const socket = io()
socket.emit('ask-join', '<%= result._id %>')
</script>
_id)를 Room 이름으로 사용서버에서 Room 조인 처리
// server.js
socket.on('ask-join', async (data) => {
socket.join(data)
})
보안 강화 (선택사항)
socket.on('ask-join', async (data) => {
// 현재 로그인된 사용자 정보 확인
let currentUser = socket.request.session.passport.user.id
// DB에서 해당 채팅방에 권한이 있는지 확인
let chatRoom = await db.collection('chatroom').findOne({
_id: new ObjectId(data),
member: new ObjectId(currentUser)
})
if (chatRoom) {
socket.join(data)
}
})
클라이언트에서 메시지 전송
<script>
document.querySelector('.chat-button').addEventListener('click', function () {
let 작성한거 = document.querySelector('.chat-input').value
socket.emit('message-send', {
room: '<%= result._id %>',
msg: 작성한거
})
})
</script>
서버에서 메시지 분배
// server.js
socket.on('message-send', async (data) => {
console.log('유저가 보낸거 : ', data) // { room: ~~, msg: ~~~ }
io.to(data.room).emit('message-broadcast', data.msg)
})
io.to(room이름): 특정 Room의 모든 사용자에게 메시지 전송클라이언트에서 메시지 수신 처리
<!-- chatDetail.ejs -->
<script>
socket.on('message-broadcast', (data) => {
document.querySelector('.chat-screen')
.insertAdjacentHTML('beforeend', `<div class="chat-box"><span>${data}</span></div>`)
})
</script>
insertAdjacentHTML(): 기존 HTML에 새로운 요소 추가beforeend: 지정된 요소의 마지막 자식으로 추가문제점: 새로고침 시 채팅 내용이 모두 사라짐
해결책: 채팅 메시지를 DB에 저장하고 페이지 로드 시 불러오기
메시지 DB 저장
socket.on('message-send', async (data) => {
// DB에 채팅 메시지 저장
await db.collection('chatMessage').insertOne({
parentRoom: new ObjectId(data.room),
content: data.msg,
who: new ObjectId(socket.request.session.passport.user.id),
timestamp: new Date()
})
// 실시간으로 다른 사용자들에게 전송
io.to(data.room).emit('message-broadcast', data.msg)
})
페이지 로드 시 기존 채팅 불러오기
app.get('/chat/detail/:id', async (요청, 응답) => {
let chatRoom = await db.collection('chatroom').findOne({
_id: new ObjectId(요청.params.id)
})
let messages = await db.collection('chatMessage').find({
parentRoom: new ObjectId(요청.params.id)
}).toArray()
응답.render('chatDetail.ejs', {
result: chatRoom,
messages: messages
})
})
DB Adapter 사용
// MongoDB Adapter 설정 예시
const { MongoStore } = require('@socket.io/mongo-adapter');
io.adapter(new MongoStore({
uri: 'mongodb://localhost:27017/myapp'
}));
app.get('/stream/list', (요청, 응답) => {
응답.writeHead(200, {
"Connection": "keep-alive",
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
});
응답.write('event: msg\n');
응답.write('data: 바보\n\n');
});
주요 헤더 설명
Connection: keep-alive: 연결 지속 유지Content-Type: text/event-stream: SSE 형식임을 명시Cache-Control: no-cache: 캐싱 방지<script>
let eventSource = new EventSource('/stream/list')
eventSource.addEventListener('msg', function (e){
console.log(e.data);
});
</script>
let 찾을문서 = [
{ $match: { operationType: 'insert' } }
]
const changeStream = db.collection('post').watch(찾을문서)
changeStream.on('change', (result) => {
console.log(result)
console.log('새 글:', result.fullDocument)
});
주요 기능
operationType: insert, update, delete 등 작업 유형 필터링result.fullDocument: 변경된 문서의 전체 내용app.get('/stream/post', (요청, 응답) => {
응답.writeHead(200, {
"Connection": "keep-alive",
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
})
const 찾을문서 = [
{ $match: { operationType: 'insert' } }
]
let changeStream = db.collection('post').watch(찾을문서)
changeStream.on('change', (result) => {
응답.write('event: msg\n')
응답.write(`data: ${JSON.stringify(result.fullDocument)}\n\n`)
})
});
<script>
let eventSource = new EventSource('/stream/post')
eventSource.addEventListener('msg', function (e){
let 가져온거 = JSON.parse(e.data)
document.querySelector('.white-bg')
.insertAdjacentHTML('afterbegin',
`<div class="list-box"><h4>${가져온거.title}</h4></div>`
)
})
</script>
let changeStream
connectDB.then((client) => {
db = client.db('forum')
// Change Stream을 한 번만 생성하여 성능 향상
changeStream = db.collection('post').watch([
{ $match: { operationType: 'insert' } }
])
server.listen(process.env.PORT, () => {
console.log('서버 실행중')
})
})
1. 채팅 메시지 DB 저장 및 불러오기
chatMessage 컬렉션에 저장2. 발신자별 메시지 스타일 구분
// 내가 보낸 메시지 우측 정렬
if (message.who === currentUserId) {
messageHTML = `<div class="chat-box mine"><span>${message.content}</span></div>`
} else {
messageHTML = `<div class="chat-box"><span>${message.content}</span></div>`
}
3. 채팅방 접근 권한 검증
4. 실시간 타이핑 표시
// 타이핑 중임을 다른 사용자에게 알림
socket.emit('typing', { room: roomId, user: username })
socket.broadcast.to(roomId).emit('user-typing', username)
이러한 기능들을 통해 완전한 실시간 채팅 시스템을 구축할 수 있습니다.