API

[spring webSocket - 스프링 웹소켓] : 간이 채팅방 구현

letsDoDev 2024. 9. 16. 19:17

구조 

  1. 채팅방 입장 시 웹소켓 connect~
  2. 웹소켓 연결 성공 시, 종료 시 프론트 웹소켓 이벤트 핸들러로 채팅방 입·퇴장 메시지 해당 채팅방 유저들에게 message send
  3. 채팅방 최초 입장 시 REST API 로 해당 채팅방 대화기록 모두 불러와서 화면에 뿌려줌
  4. 채팅 참여자들이 메시지 보내면 [프론트 message send -> 백 webSocketHandler 데이터 처리]로 DB에 데이터 적재 및 해당 사용자들에세 session message send 

 

 

#1 스프링 이니셜라이저로 프로젝트 생성

- 채팅방 같이 DB 적재가 자주 일어나야 하는 API 의 경우 NOSQL DB를 사용해야 하지만 

- 나는 WebSocket 을 사용하며 개발을 진행한다는 것에 중점을 두어 그냥 MYSQL 을 사용하기로 하였다.

 

#2 application.yml

spring:
  application:
    name: websocketchat


  # mysql db 설정
  datasource:
    url: "jdbc:mysql://localhost/websocketchat"
    username: "root"
    password: ${MYSQL_ROOT_PASSWORD} # 시스템 환경 변수에 저장한 'MYSQL_ROOT_PASSWORD' 사용
    driver-class-name : com.mysql.cj.jdbc.Driver

  jpa:
    hibernate:
      ddl-auto: create  # 'create', 'update', 'validate', 'none'
    properties:
      hibernate:
        show_sql: trueㄴ
        format_sql: true
        dialect: org.hibernate.dialect.MySQL8Dialect  # MySQL 8.x

 

#3 MYSQL SCHEMA 또는 DATABASE 생성 

 - 이거는 어차피 JPA ddl 로 테이블 생성해줄 거기 때문에 미리 생성해주지 않아도 큰 상관은 없음

CREATE DATABASE websocketchat;

USE websocketchat;

CREATE TABLE CHAT(
	ID BIGINT AUTO_INCREMENT,
	NAME VARCHAR(20) NOT NULL,
    DETAIL VARCHAR(255),
    ADD_DATE TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    CHAT_ROOM_ID VARCHAR(20) NOT NULL,
	PRIMARY KEY(id)    
)

- 테이블 체크

 

#4 채팅방 어플리케이션 구현에 필요한 domain, controller, dto, repository 구현

- Chat.java

package com.study.websocketchat.domain.chat;

import com.study.websocketchat.domain.chat.dto.ChatDto;
import jakarta.persistence.*;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;

import java.time.LocalDateTime;

@ToString
@Entity
@Getter
@NoArgsConstructor
public class Chat {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "NAME", nullable = false, length = 20)
    private String name;

    @Column(name = "DETAIL", length = 255)
    private String detail;

    @Column(name = "ADD_DATE", columnDefinition = "TIMESTAMP DEFAULT CURRENT_TIMESTAMP", updatable = false, insertable = false)
    private LocalDateTime addDate;

    @Column(name = "CHAT_ROOM_ID", nullable = false, length = 20)
    private String chatRoomId;

    @Builder
    public Chat(Long id, String name, String detail, LocalDateTime addDate, String chatRoomId) {
        this.id = id;
        this.name = name;
        this.detail = detail;
        this.addDate = addDate;
        this.chatRoomId = chatRoomId;

    }

    public static Chat dtoToChat(ChatDto chatDto) {
        return Chat.builder()
                .name(chatDto.getName())
                .detail(chatDto.getDetail())
                .chatRoomId(chatDto.getChatRoomId())
                .build();
    }

}

 

- ChatDto.java

package com.study.websocketchat.domain.chat.dto;

import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;

import java.time.LocalDateTime;

/**
 * 단순 테스트 dto 이므로 request 와 response를 구분하지 않겠다.
 */
@ToString
@Getter
@NoArgsConstructor
public class ChatDto {

    private String id;
    private String name;
    private String detail;
    private LocalDateTime addDate;
    private String chatRoomId;

    @Builder
    public ChatDto(String id, String name, String detail, LocalDateTime addDate, String chatRoomId) {
        this.id = id;
        this.name = name;
        this.detail = detail;
        this.addDate = addDate;
        this.chatRoomId = chatRoomId;
    }
}

 

- ChatRepository.java

package com.study.websocketchat.domain.chat.repository;

import com.study.websocketchat.domain.chat.Chat;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.util.List;

public interface ChatRepository extends JpaRepository<Chat, Long> {
    
    // 채팅 리스트 전체 출력
    List<Chat> findAll();

    @Query("SELECT c.chatRoomId FROM Chat c GROUP BY c.chatRoomId")
    List<String> findChatRoomIdList();

    // 특정 채팅방의 대화 출력
    @Query("SELECT c FROM Chat c WHERE c.chatRoomId = :chatRoomId ORDER BY c.addDate ASC")
    List<Chat> findChatDetails(@Param("chatRoomId") String chatRoomId);
}

 

- ChatController

package com.study.websocketchat.domain.chat;

import com.study.websocketchat.domain.chat.dto.ChatDto;
import com.study.websocketchat.domain.chat.service.ChatService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;

import java.util.ArrayList;
import java.util.List;

@Slf4j
@RequiredArgsConstructor
@RestController
@RequestMapping("/chat")
public class ChatController {

    private final ChatService chatService;

    // 채팅방 최초 입장 시
    @PostMapping("/join/chatRoom")
    public List<ChatDto> joinChatRoomInit(@RequestBody ChatDto chatDto) {
        log.info("ChatController is matchaing : joinChatRoomInit");
        log.info(chatService.getDetails(chatDto.getChatRoomId()).toString());
        if(chatService.getDetails(chatDto.getChatRoomId()) == null || chatService.getDetails(chatDto.getChatRoomId()).isEmpty()) {
            return new ArrayList<>();
        } else {
            return chatService.getDetails(chatDto.getChatRoomId());
        }
    }
}

 

- ChatService.java (인터페이스)

package com.study.websocketchat.domain.chat.service;

import com.study.websocketchat.domain.chat.dto.ChatDto;

import java.util.List;

public interface ChatService {

    public List<ChatDto> getDetails(String chatRoomId);

    public boolean addChat(ChatDto chatDto);

    public List<String> getChatRoomList(); // 안 쓰는 메소드
}

 

- ChatServiceImpl.java (구현클래스)

package com.study.websocketchat.domain.chat.service;

import com.study.websocketchat.domain.chat.Chat;
import com.study.websocketchat.domain.chat.dto.ChatDto;
import com.study.websocketchat.domain.chat.repository.ChatRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

@Slf4j
@Service
@RequiredArgsConstructor
public class ChatServiceImpl implements ChatService {

    private final ChatRepository chatRepository;

    /**
     * 채팅 기록 없는 경우는 빈 객체 넘겨줌
     * @return List<ChatDto>
     */
    @Override
    public List<ChatDto> getDetails(String chatRoomId) {

        try {
            // List<ChatDto> chatList = chatRepository.findAll().stream().map(chat -> {
            List<ChatDto> chatList = chatRepository.findChatDetails(chatRoomId).stream().map(chat -> {
                return ChatDto.builder()
                            .id(chat.getId().toString())
                            .name(chat.getName())
                            .detail(chat.getDetail())
                            .addDate(chat.getAddDate()).
                            build();
            }).toList();

            if(chatList.isEmpty()) {
                throw new RuntimeException("chat-record is empty");
            }

            return chatList;

        } catch (Exception e) {
            List<ChatDto> emptyChatList = new ArrayList<>();

            log.error(e.getLocalizedMessage());
            return emptyChatList;
        }
    }


    /**
     * 채팅 입력 성공 시 true 실패 시 false 반환
     * @param chatDto
     * @return boolean
     */
    @Override
    public boolean addChat(ChatDto chatDto) {
        try {
            log.info("chatDto -> {}", chatDto);

            Chat chat = chatRepository.save(Chat.dtoToChat(chatDto));
            log.info("chat -> {}", chat);

            if(chat == null) {
                throw new RuntimeException("add chat data is failed");
            }

            return true;

        } catch (Exception e) {

            log.error(e.getLocalizedMessage());
            return false;
        }
    }

    @Override
    public List<String> getChatRoomList() { // 안 쓰는 메소드
        try {

            List<String> chatRoomList = chatRepository.findChatRoomIdList();

            if(chatRoomList.isEmpty()) {
                throw new RuntimeException("chatRoomList is null");
            }

            return chatRoomList;

        } catch (Exception e) {

            List<String> emptyRoomList = new ArrayList<>();

            log.error(e.getLocalizedMessage());
            return emptyRoomList;
        }
    }
}

 

#5 웹소켓 사용을 위한 WebSocketConfig, WebSocketHandlerImpl, runner 클래스에 어노테이션 처리

- WebSocketConfig.java

package com.study.websocketchat.config;

import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;

@Configuration
@EnableWebSocket
@RequiredArgsConstructor
public class WebSocketConfig implements WebSocketConfigurer {


    private final WebSocketHandler webSocketHandler;

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(webSocketHandler, "/ws/chat").setAllowedOrigins("http://localhost:8080");
    }
}

 

- WebSocketHandlerImpl.java

package com.study.websocketchat.handler;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.study.websocketchat.domain.chat.dto.ChatDto;
import com.study.websocketchat.domain.chat.service.ChatService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.*;
import org.springframework.web.socket.handler.TextWebSocketHandler;

import java.util.*;

@Component
@Slf4j
@RequiredArgsConstructor
public class WebSocketHandlerImpl extends TextWebSocketHandler {

    private final ObjectMapper objectMapper;

    // 채팅 서비스 빈 주입
    private final ChatService chatService;
    
    private final Set<WebSocketSession> sessions = new HashSet<>(); // 각 사용자마다 하나의 고유 세션이 그 세션들을 전부 가지고 있는 객체

    // chatRoomId: {session1, session2}  --> 사용자들의 session 세션이 들어가게 됨 예시는 두 명의 사용자 가정
    private final Map<String, Set<WebSocketSession>> chatRoomSessionMap = new HashMap<>();
    
    
    // 웹소켓 연결 성공 시
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        // super.afterConnectionEstablished(session);
        log.info("session connect is success by session_id : " + session.getId());
        if (!sessions.contains(session)) {
            sessions.add(session); // 모든 사용자들의 세션을 모아놓는 sesstions 객체에 신규 사용자 세션 추가
        }
    }

    // # 현재 프로젝트 에서는 다루지 않음
    //    @Override
    //    public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
    //        super.handleMessage(session, message);
    //    }
    
    // 웹소켓 통신시 메시지 다루는 부분
    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        // super.handleTextMessage(session, message);
        String payLoad = message.getPayload();
        log.info("payLoad : " + payLoad);

        // 페이로드(입력받은 메시지) -> chatMessageDto로 변환
        ChatDto chatDto = objectMapper.readValue(payLoad, ChatDto.class);
        log.info("message from session -> {}", chatDto);

        String chatRoomId = chatDto.getChatRoomId();


        if(!chatRoomSessionMap.containsKey(chatRoomId)) { // 해당 채팅이 채팅세션맵에 존재하지 않는다면
            chatRoomSessionMap.put(chatRoomId, new HashSet<>());
        }

        if(!chatRoomSessionMap.get(chatRoomId).contains(session)) { // 해상 사용자아 해당 채팅방세션맵의 채팅방세션정보에 없다면
            chatRoomSessionMap.get(chatRoomId).add(session);
        }

        Set<WebSocketSession> chatRoomSessions = chatRoomSessionMap.get(chatRoomId);

        if (chatRoomSessions != null) {
            // 채팅 메시지를 session 에 반환하면서 db 에 적재하는 커스텀 메소드
            customSendMessage(chatRoomSessions, chatDto, message);
        } else {
            log.error("there is no chat Info by chatRoomId : " + chatRoomId);
            throw new RuntimeException("채팅방 정보를 찾을 수 없습니다.");
        }
    }
    
    // # pong 타입 메시지는 다루지 않음 기본 타입인 ping 타입 데이터를 다룸  
    //    @Override
    //    protected void handlePongMessage(WebSocketSession session, PongMessage message) throws Exception {
    //        super.handlePongMessage(session, message);
    //    }
    
    // # 현재 프로젝트에서 사용하지 않으
    //    @Override
    //    public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
    //        // super.handleTransportError(session, exception);
    //    }

    // 웹소켓 연결 종료 시 처리
    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {


        // 1. 클라이언트가 채팅방을 떠났을 때 처리해야 할 로직
        // 예를 들어, 채팅방 목록에서 사용자를 제거하고 퇴장 메시지 브로드캐스트
        // 2. 다른 사용자들에게 사용자가 나갔음을 알릴 수 있음
        // 예시: 모든 사용자에게 "사용자가 채팅방을 떠났습니다." 메시지 전송

        String targetChatRoomId = null;
        Set<WebSocketSession> targetSessions = null;

        TextMessage userExitMessage = new TextMessage("{" +
                "id:'0'" +
                "user:'관리자'" +
                "detail:" + "'" + session.getId() + " 님께서 퇴장하였습니다." + "'" +
                "user:'알림'" +
                "chatRoomId:" + "'" + targetChatRoomId + "'" +
                "addTime:" + "'" + new Date().toString() + "'" +
                "}");

        try {

            log.info(session.getId() + "-session is closed");

            for (String chatRoomId : chatRoomSessionMap.keySet()) {

                if (chatRoomSessionMap.get(chatRoomId).contains(session)) {

                    targetChatRoomId = chatRoomId;
                    targetSessions = chatRoomSessionMap.get(chatRoomId);

                    chatRoomSessionMap.get(chatRoomId).remove(session);
                    log.info(session.getId() + "-session is deleted in chatRoomId : " + chatRoomId);
                }

                if(targetSessions == null || targetSessions.isEmpty()) {
                    chatRoomSessionMap.remove(chatRoomId);
                    log.info( chatRoomId + "-chatRoom is deleted by this chatRoom user is all out or not exist");
                }
            }

            // 전체 사용자 세션들에서 해당 사용자의 세션 삭제
            if (sessions.removeIf(sessionItem -> sessionItem.getId().equals(session.getId()))) {
                log.info("user session-id(" +  session.getId() + ") id deleted successfully");
            }

            super.afterConnectionClosed(session, status);

        } catch(Exception e) { // 예외 발생 시 웹소켓 서버 강제 종료 되길래 방지하고자 throw 대신 예외 메시지 리턴
            // throw new RuntimeException("WebSocket session closing is failed because :" + e.getLocalizedMessage());
            customSendError(session, e);
        }


    }
    
    // 사용하지 않음
    //    @Override
    //    public boolean supportsPartialMessages() {
    //        return super.supportsPartialMessages();
    //    }


    // 채팅 관련 메소드 커스텀 메소드 ======
    // 채팅 메시지를 session 에 반환하면서 db 에 적재하는 커스텀 메소드
    public void customSendMessage(Set<WebSocketSession> chatRoomSessions, ChatDto chatDto, TextMessage message) {
        try{
            if(chatDto != null) {
                chatService.addChat(chatDto); // db 적재
            }

            for(WebSocketSession session : chatRoomSessions) {
                session.sendMessage(message); // 사용자에게 세션으로 반환
            }
        } catch (Exception e) {
            log.error("send message to chatRoom is faisle because : ");
            log.error(e.getLocalizedMessage());
        }
    }

    public void customSendError(WebSocketSession session, Exception e) {
        TextMessage errorTextMessage = new TextMessage("{" +
                "id:'-1'" +
                "user:'관리자'" +
                "detail: 메시지 전송에 실패하였습니다. 사유 : " + e.getLocalizedMessage() +
                "user:'알림'" +
                "addTime:" + "'" + new Date().toString() + "'" +
                "}");
        log.error(e.getLocalizedMessage());
        chatRoomSessionMap.keySet().forEach(key -> {
            if (chatRoomSessionMap.get(key).contains(session)) {
                customSendMessage(chatRoomSessionMap.get(key), null ,errorTextMessage);
            };
        });
    }
}

 

#6 어플리케이션 실행을 위한 IndexController(백), index.html(프론트)

- IndexController.java

package com.study.websocketchat.handler;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

@Controller
public class indexController {

    @RequestMapping("/")
    public String index() {
        return "index"; // 타임리프는 .html 생략가능
    }
}

 

- Index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>채팅방입니다.</title>
</head>
<body>
<script>
    let webSocket;

    // WebSocket 연결 설정 (채팅방에 입장)
    function connectWebSocket() {

        let name = "";
        if(document.getElementById("name") != null && document.getElementById("name") != undefined) {
            name = document.getElementById("name").value.toString().trim();
        }

        // WebSocket 연결 생성
        webSocket = new WebSocket("ws://localhost:8080/ws/chat");

        // WebSocket 연결 성공 시
        webSocket.onopen = function () {
            sendNotification(`'${name}' 님이 채팅방에 입장했습니다.`);
        };

        // 서버로부터 메시지를 수신했을 때
        webSocket.onmessage = function (event) {
            const message = event.data;
            appendMessage(message);
        };

        // WebSocket 연결 종료 시
        webSocket.onclose = function () {

        };

        // WebSocket 에러 처리
        webSocket.onerror = function (event) {
            appendSingleString("에러가 발생했습니다.");
            appendMessage(event.data);
        };
    }

    function joinChatRoom () {

        const nameElement = document.getElementById("name");

        if(nameElement != null && nameElement != undefined) {
            if(nameElement.value == null || nameElement.value.toString().trim() === "") {
                alert("유저명을 먼저 입력해주세요.");
                return;
            }
        }

        connectWebSocket(); // 웹소켓 연결

        const joinChatBtn = document.getElementById("joinChatBtn");
        let chatRoomId;

        if(joinChatBtn) {
            chatRoomId = joinChatBtn.value;
            console.log("chatRoomId : " + chatRoomId);
        }

        const body = {
            "chatRoomId": chatRoomId
        }

        // 기존 채팅 기록 불러오기 (비동기 통신)
        fetch("chat/join/chatRoom", {
            method: "POST",
            credentials: "include",
            headers: {
                "Content-Type": "application/json"
            },
            body: JSON.stringify(body)
        })
            .then(response => response.json())
            .then(data => {
                appendListMessage(data);
                // data.forEach(message => {
                //     appendMessage(message);
                //  });
            })
            .catch(error => {
                appendSingleString("채팅 기록을 불러오는 중 오류 발생: " + error);
            });
    }

    function appendSingleString(messageStr) {
        // console.log(message);
        const chatDiv = document.getElementById("chatDiv");

        if(chatDiv == null || chatDiv == undefined) {
            return;
        }

        const br = document.createElement("br");
        const messageElement = document.createElement("span");
        messageElement.innerText = messageStr;
        chatDiv.appendChild(br);
        chatDiv.appendChild(messageElement);
        chatDiv.scrollTop = chatDiv.scrollHeight; // 스크롤 자동 이동
    }

    function appendListMessage(messageList) {

        const messages = JSON.parse(JSON.stringify(messageList));
        const chatDiv = document.getElementById("chatDiv");

        if(chatDiv == null || chatDiv == undefined) {
            return;
        }

        if(messages == null || messages.length === 0) {
            return;
        }

        messages.map((message) => {
            const targetDate = new Date(message.addDate);
            const addDate = targetDate.getFullYear() +"."+ targetDate.getMonth() +"."+ targetDate.getDate() +" "+ targetDate.getHours() +"시"+ targetDate.getMinutes() +"분";

            const br = document.createElement("br");
            const messageElement = document.createElement("span");
            messageElement.innerText = `(${addDate}) ${message.name}: ${message.detail}`;

            console.log(messageElement);
            chatDiv.appendChild(br);
            chatDiv.appendChild(messageElement);
            chatDiv.scrollTop = chatDiv.scrollHeight; // 스크롤 자동 이동
        })
    }


    // 메시지를 채팅창에 추가하는 함수
    function appendMessage (messageStr, division) {

        const message = JSON.parse(messageStr); // 웹소켓 자체에서 넘어온 event.data는 JSON.parse() 만 해줘도 됨
        const chatDiv = document.getElementById("chatDiv");

        const targetDate = new Date(message.addDate);
        const addDate = targetDate.getFullYear() +"."+ targetDate.getMonth() +"."+ targetDate.getDate() +" "+ targetDate.getHours() +"시"+ targetDate.getMinutes() +"분";


        if(chatDiv == null || chatDiv == undefined) {
            return;
        }

        const br = document.createElement("br");
        const messageElement = document.createElement("span");
        messageElement.innerText = `(${addDate}) ${message.name}: ${message.detail}`;

        chatDiv.appendChild(br);
        chatDiv.appendChild(messageElement);
        chatDiv.scrollTop = chatDiv.scrollHeight; // 스크롤 자동 이동
    }

    // 메시지 전송 함수
    function sendMessage () {
        const name = document.getElementById("name").value;
        const detail = document.getElementById("detail").value;

        const chatMessage = {
            "name": name,
            "detail": detail,
            "chatRoomId": document.getElementById("joinChatBtn").value, // 만약 채팅방이 여러개 있는 버전으로 수정한다면 하드코딩 하면 안 됨
            "addDate": new Date()
        }

        if(webSocket == null) {
            return;
        }

        webSocket.send(JSON.stringify(chatMessage)); // 서버로 메시지 전송

        // 입력창 초기화
        document.getElementById("detail").value = "";
    }

    function sendNotification (message) {
        const notification = {
            "name": "알림",
            "detail": message,
            "chatRoomId": document.getElementById("joinChatBtn").value, // 만약 채팅방이 여러개 있는 버전으로 수정한다면 하드코딩 하면 안 됨
            addDate: new Date()
        }

        if(webSocket == null) {
            return;
        }

        webSocket.send(JSON.stringify(notification)); // 서버로 메시지 전송
    }

    function exitChatBtn () {
        if(webSocket != null) {
            if (window.confirm("채팅방을 나가시겠습니까?")) {

                if(document.getElementById("name") != null && document.getElementById("name") != undefined) {
                   const name = document.getElementById("name").value;
                   sendNotification(`'${name}' 님이 채팅방에서 퇴장했습니다.`);
                }

                webSocket.close();
            }
        }

        const chatDiv = document.getElementById("chatDiv");
        if(chatDiv != null && chatDiv != undefined) {
            chatDiv.innerHTML = "";
        }
    }

</script>

<h1>채팅방 어플리케이션</h1>

<button id="joinChatBtn" value="1" onclick="joinChatRoom()">1번 채팅방 입장</button>

<div id="chatDiv" style="border: 1px solid black; overflow: auto; height: 300px; width: 400px;"></div>

<!-- 메시지 입력 필드 -->
<input type="text" id="name" placeholder="유저명 입력" style="width: 100px;">
<br>
<input type="text" id="detail" placeholder="메시지를 입력하세요" style="width: 400px;">
<button id="addChatBtn" onclick="sendMessage()">보내기</button>
<button id="exitChatBtn" onclick="exitChatBtn()">채팅방 나가기</button>
</body>
</html>

 

 

#최종 실행 결과

# 참조

- 백엔드 코드에서는 채팅방이 존재할 때 까지 고려하여 로직을 구현하였으나,

- 프론트 쪽을 채팅방이 여러 개 존재할 때는 고려하여 개발하지 않았다.

- 추후 여유가 되면 한 번 더 커스텀 해보자!~