[Node.js] 실시간 채팅

Comely·2025년 6월 9일

Node.js

목록 보기
14/14

채팅 기능 동작 원리

기본 흐름
1. 사용자가 메시지 작성 후 전송
2. 서버가 메시지를 받아서 같은 채팅방의 다른 사용자들에게 전달
3. 모든 사용자가 실시간으로 메시지 확인

Room 개념 활용

  • 전체 사용자가 아닌 특정 채팅방(Room) 사용자들에게만 메시지 전송
  • 채팅방별로 독립적인 대화 공간 구성

채팅 기능 구현 단계

1. 채팅방 입장 시 Room 조인

클라이언트에서 서버에 Room 입장 요청

<!-- chatDetail.ejs -->
<script>
  const socket = io()
  socket.emit('ask-join', '<%= result._id %>')
</script>
  • 채팅방 상세페이지 접속 시 자동으로 해당 채팅방 Room에 입장 요청
  • 채팅방의 고유 ID(_id)를 Room 이름으로 사용

서버에서 Room 조인 처리

// server.js
socket.on('ask-join', async (data) => {
  socket.join(data)
})
  • 클라이언트 요청을 받아 해당 Room에 사용자 추가
  • 이제 같은 Room의 사용자들끼리만 메시지 송수신 가능

보안 강화 (선택사항)

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)
  }
})

2. 메시지 전송 기능

클라이언트에서 메시지 전송

<script>
  document.querySelector('.chat-button').addEventListener('click', function () {
    let 작성한거 = document.querySelector('.chat-input').value
    socket.emit('message-send', { 
      room: '<%= result._id %>', 
      msg: 작성한거 
    })
  })
</script>
  • 전송 버튼 클릭 시 메시지 내용과 채팅방 ID를 함께 서버로 전송
  • Object 형태로 여러 데이터를 한번에 전달

서버에서 메시지 분배

// 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의 모든 사용자에게 메시지 전송
  • 메시지를 보낸 사용자를 포함한 모든 Room 멤버가 수신

3. 메시지 수신 및 화면 표시

클라이언트에서 메시지 수신 처리

<!-- 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
  })
})

Socket.io 고급 설정

DB Adapter 사용

  • 웹소켓 연결 정보가 메모리에만 저장되어 서버 재시작 시 모든 연결이 끊어짐
  • MongoDB Adapter를 사용하면 연결 정보를 DB에 안전하게 저장
// MongoDB Adapter 설정 예시
const { MongoStore } = require('@socket.io/mongo-adapter');

io.adapter(new MongoStore({
  uri: 'mongodb://localhost:27017/myapp'
}));

Server Sent Events (SSE) 실시간 업데이트

SSE 기본 개념

  • 기존 HTTP: 1회 요청 → 1회 응답 → 연결 종료
  • SSE: 연결을 유지하며 서버에서 클라이언트로 지속적인 데이터 전송

SSE 서버 설정

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: 캐싱 방지

클라이언트 SSE 연결

<script>
  let eventSource = new EventSource('/stream/list')
  eventSource.addEventListener('msg', function (e){
    console.log(e.data);
  });
</script>

MongoDB Change Stream 활용

Change Stream 기본 설정

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: 변경된 문서의 전체 내용
  • 실시간으로 DB 변화 감지 및 처리

실시간 게시물 업데이트

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. 채팅방 접근 권한 검증

  • 로그인하지 않은 사용자 접근 차단
  • 채팅방 멤버가 아닌 사용자 접근 차단
  • Socket 연결 시와 페이지 접속 시 모두 검증

4. 실시간 타이핑 표시

// 타이핑 중임을 다른 사용자에게 알림
socket.emit('typing', { room: roomId, user: username })
socket.broadcast.to(roomId).emit('user-typing', username)

이러한 기능들을 통해 완전한 실시간 채팅 시스템을 구축할 수 있습니다.

profile
App, Web Developer

0개의 댓글