실시간 메시지 전송과 수신
Cola Chat 익명 채팅 개발기· 5 / 7
이전 문서에서 WebSocket 연결과 해제를 이용해 채팅방 입장/퇴장 기능을 구현했고, 이제 메세지 전송과 수신을 구현하여 실제 채팅 기능을 완성할 것이다.
아이디어: 웹소켓 통신 과정
웹소켓 연결을 완료했다면, 메세지 전송 및 수신 기능은 비교적 간단하게 구현할 수 있다. 서버와 클라이언트 양쪽 사이드 모두 Socket.IO 라이브러리의 **on**과 emit 메서드를 적절한 위치에 사용하면 된다.
메세지 전송부터 수신까지의 과정은 다음과 같이 이루어진다.

특정 사용자가 메세지를 서버로 전송하면, 서버는 내부 로직을 처리한 후 같은 채널 및 채팅방에 속한 모든 사용자에게 메세지를 전송한다.
기능 구현: 메세지 전송 및 수신
서버와 클라이언트의 웹 프레임워크는 다음과 같다.
- 서버: Nest.js
- 클라이언트: Next.js
서버
서버에서는 Gateway의 이벤트 리스너와 Service의 비즈니스 로직으로 메세지 전송 기능을 구현한다.
-
Gateway 웹소켓 이벤트 리스너
// src/app/chat/chat.gateway.ts @WebSocketGateway({ namespace: /\/*.+/ }) export class ChatGateway { ... // 메세지 수신 처리 @SubscribeMessage('sendMessage') async handleSendMessage( @MessageBody() data: { roomId: string; content: string }, @ConnectedSocket() socket: Socket, ): Promise<void> { await this.chatService.handleSendMessage(data, socket); } // 공지 수신 처리 @SubscribeMessage('sendAlert') async handleSendAlert( @MessageBody() data: { roomId: string; content: string }, @ConnectedSocket() socket: Socket, ): Promise<void> { await this.chatService.handleSendAlert(data, socket); } }메세지 전송 이벤트는 두 가지로 분류했다. 하나는 사용자가 직접 작성해 전송하는 메세지(
'sendMessage')이고, 다른 하나는 공지사항(알림) 성격의 시스템 메세지('sendAlert')이다.바디 데이터로 채팅방 식별자(
roomId)와 메세지(content)를 전달 받으며, 별도의 반환값은 없다. -
Service 로직
// src/app/chat/chat.service.ts @Injectable() export class ChatService { ... // 메세지 수신 처리 async handleSendMessage( data: { roomId: string; content: string }, socket: Socket, ): Promise<void> { const { roomId, content } = data; const namespace: Namespace = socket.nsp; const chatterId: string = socket.data.chatters?.[roomId]; // 채팅방 만료 확인 await this.checkRoomExpired(roomId); // 메세지 DB 저장 const message = await this.createMessage({ type: MessageType.MESSAGE, content, roomId, chatterId, }); // 웹소켓 이벤트 전송 this.emit<Message>(namespace, roomId, SocketEvent.MESSAGE, message); } // 공지 수신 처리 async handleSendAlert( data: { roomId: string; content: string }, socket: Socket, ): Promise<void> { const { roomId, content } = data; const namespace: Namespace = socket.nsp; const chatterId: string = socket.data.chatters?.[roomId]; // 채팅방 만료 확인 await this.checkRoomExpired(roomId); // 메세지 DB 저장 const message = await this.createMessage({ type: MessageType.ALERT, content, roomId, chatterId, }); // 웹소켓 이벤트 전송 this.emit<Message>(namespace, roomId, SocketEvent.ALERT, message); } // 웹소켓 이벤트 전송 처리 emit<T>( namespace: Namespace, roomId: string, event: SocketEvent, data: T, ): void { namespace.to(roomId).emit(event, data); } }메시지 전송-수신 기능은, 클라이언트로부터 전달받은 메세지 데이터를 데이터베이스에 저장하고, 조회된 값을 다시 클라이언트에 전달하는 방식으로 작동한다.
이때,
emit메서드를 사용하여 소켓 인스턴스의namespace와roomId로 연결된 모든 소켓 인스턴스에 결과값을 전송한다. 이를 통해 특정 사용자가 메세지를 전송했을 때, 해당 채팅방에 참여 중인 모든 사용자에게 실시간으로 메세지가 전달되는 효과를 낼 수 있다.
클라이언트
클라이언트에서는 이벤트를 전송하는 함수와 이를 수신하는 Hook으로 메세지 기능을 구현한다.
-
웹소켓 메세지 이벤트 트리거
// src/shared/webSockets/emit.ts interface WSResponse<T> { success: boolean; message: string; data: T; } // 메세지 이벤트 전송 export function emitMessage( socket: Socket, data: { roomId: string; content: string }, callback?: () => void ) { socket.emit(SocketEvent.MESSAGE, data, (response: WSResponse<null>) => { if (response.success) callback && callback(); else return notFound(); }); } // 공지 이벤트 전송 export function emitAlert( socket: Socket, data: { roomId: string; content: string }, callback?: () => void ) { socket.emit(SocketEvent.ALERT, data, (response: WSResponse<null>) => { if (response.success) callback && callback(); }); }소켓의 이벤트를 전송하는 함수는 별도의 파일로 분리하여 각 이벤트별로 함수를 선언해두었다.
이 함수들은 소켓의
emit메서드를 사용해 이벤트를 전송하며, ‘메세지 전송 버튼’의onClick이벤트에 연결되어 있다. -
웹소켓 메세지 이벤트 리스너
// src/app/hooks/useSocket.ts export default function useSocket(props: Props) { ... const { socket, setSocket } = useSocketStore(); const addMessage = useMessageStore((state) => state.addMessage); useEffect(() => { if (!namespace) return; const socketInstance = io(`${SOCKET_BASE_URL}/${namespace}`, { transports: ['websocket'], autoConnect: false, }); // 소켓 연결 로직 ... // 메세지 이벤트 리스너 socketInstance.on(SocketEvent.MESSAGE, (data) => { // 메세지 데이터 상태 관리에 추가 addMessage(data); }); // 공지 이벤트 리스너 socketInstance.on(SocketEvent.ALERT, (data) => { // 메세지 데이터 상태 관리에 추가 addMessage(data); }); return () => { // 소켓 연결 해제 로직 ... }; }, [namespace, roomId]); }웹소켓 설정 Hook에서
on메서드를 이용해 메세지를 수신하는 이벤트 리스너를 등록했다. 이때,on메서드의 콜백 함수에서 전달된data를 메세지 상태 관리에 추가(addMessage)하여, 메세지를 보여주는 피드 컴포넌트가 리렌더링되도록 처리했다.