목차

    코드는 아래의 깃허브 주소에서 WebSocketAPIToy 폴더 속에 있습니다.

    https://github.com/Dev-Taehee/WebSocketToy

    설정

    https://start.spring.io/

    저는 Spring Initializr를 활용하여 위와 같이 설정한 후 시작했습니다.

     

    개요

    WebSocket API의 가장 중요한 두 가지는 WebSocketHandler와 WebSocketConfigurer 입니다.

    public class MyHandler extends TextWebSocketHandler {
    
    	@Override
    	public void handleTextMessage(WebSocketSession session, TextMessage message) {
    		// ...
    	}
    
    }

    WebSocketHandler는 말그대로 WebSocket을 다루는 친구입니다. WebSocket을 통해 오는 message들을 처리할 수도 있고 WebSocket이 연결된 후의 행동이나 WebSocket이 종료된 후의 행동을 관리할 수 있습니다.

    @Configuration
    @EnableWebSocket
    public class WebSocketConfig implements WebSocketConfigurer {
    
    	@Override
    	public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
    		registry.addHandler(myHandler(), "/myHandler");
    	}
    
    	@Bean
    	public WebSocketHandler myHandler() {
    		return new MyHandler();
    	}
    
    }

    WebSocketConfigurer는 WebSocket과 관련된 설정들을 관리할 수 있게 해줍니다. WebSocketHandler를 등록해주는 것과 등록된 WebSocketHandler에 접속할 수 있는 URL을 구체적으로 정해주는 등의 작업을 할 수 있습니다.

     

    만약 기본 설정인 Tomcat이 아닌 Jetty를 사용해주는 경우에는 아래와 같이 WebSocketServerFactory를 이용하여 사전 설정을 진행해주어야 한다고 합니다.

    @Configuration
    @EnableWebSocket
    public class WebSocketConfig implements WebSocketConfigurer {
    
    	@Override
    	public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
    		registry.addHandler(echoWebSocketHandler(),
    			"/echo").setHandshakeHandler(handshakeHandler());
    	}
    
    	@Bean
    	public DefaultHandshakeHandler handshakeHandler() {
    
    		WebSocketPolicy policy = new WebSocketPolicy(WebSocketBehavior.SERVER);
    		policy.setInputBufferSize(8192);
    		policy.setIdleTimeout(600000);
    
    		return new DefaultHandshakeHandler(
    				new JettyRequestUpgradeStrategy(new WebSocketServerFactory(policy)));
    	}
    
    }

    단순히 Handler와 Configurer 두 가지를 상속받은 후 구현하면 WebSocket 연결이 완료된다고 하니 굉장히 편리한 것 같습니다. 저는 두 클래스를 이용하여 채팅창을 구현해보는 실습을 통해 이해도를 높여보려고합니다.

     

    실습

    websocket 설정

    websocket 설정을 위하여 앞서 말씀드린 Handler와 Configurer 2가지를 생성합니다.

    우선 Handler입니다.

    @Log4j2
    public class MyHandler extends TextWebSocketHandler {
    
        private static List<WebSocketSession> list = new ArrayList<>();
    
        @Override
        protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
            log.info("HandleTextMessage 진입");
            log.info("session: " + session);
            log.info("message: " + message);
            for(WebSocketSession webSocketSession : list) {
                webSocketSession.sendMessage(message);
            }
        }
    
        @Override
        public void afterConnectionEstablished(WebSocketSession session) throws Exception {
            log.info(session + "클라이언트 접속");
            list.add(session);
        }
    }

    afterConnectionEstablished 메서드는 클라이언트가 접속하면 실행되는 메서드입니다.

    이를 통해 클라이언트가 채팅창에 들어오면 해당 클라이언트의 WebSocketSession을 등록해주는 작업을 진행하도록 하였습니다.

    handleTextMessage는 클라이언트가 텍스트 메시지를 보내면 해당 메시지를 채팅창에 참여한 사람들에게 보내주는 역할을 합니다.

     

    다음은 Configurer입니다.

    @Configuration
    @EnableWebSocket
    public class WebSocketConfig implements WebSocketConfigurer {
        @Override
        public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
            registry.addHandler(myHandler(), "ws/myHandler")
                    .setAllowedOrigins("*");
        }
    
        @Bean
        public WebSocketHandler myHandler() {
            return new MyHandler();
        }
    
    }

    Configurer는 앞선 예제 코드와 크게 변한 것이 없습니다.

    우선 Handler에 접속할 수 있는 url을 "ws/myHandler"로 사용했습니다.

    웹소켓을 이용하면 통신이 http 통신에서 Upgrade 헤더를 사용해 ws 통신으로 바뀌는 것을 앞선 시간에 확인할 수 있었습니다. 그런 이유로 "ws/myHandler"로 url을 정해봤습니다.

    setAllowedOrigins를 모두 허락해두었습니다.

     

    채팅창 접속 가능한 ChatController 생성

    채팅창에 접속 가능하도록 ChatController를 생성했습니다.

    @Controller
    public class ChatController {
    
        @GetMapping("/myHandler")
        public String getChat() {
            return "chat";
        }
    
    }

    endpoint를 "/myHandler"로 설정하고 접속시 chat.html을 열도록 해두었습니다.

     

    프론트엔드 구성

    프론트엔드 구성은 chatGPT로 작성한 후 제가 수정하는 방식으로 작업했습니다.

    우선 chat.html 입니다.

    <!DOCTYPE html>
    <html lang="en">
    
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>채팅 창</title>
        <link rel="stylesheet" th:href="@{/css/styles.css}">
    </head>
    
    <body>
        <div class="input-container">
            <label for="username">유저 이름: </label>
            <input type="text" id="username" class="username-input" placeholder="유저 이름을 입력하세요...">
            <button onclick="saveUsername()" class="save-button">저장</button>
        </div>
        <div class="chat-box">
            <div class="chat-container" id="chat-container">
                <!-- 메시지가 여기에 나타남 -->
            </div>
    
            <div class="input-container">
                <input type="text" id="message-input" class="message-input" placeholder="메시지를 입력하세요...">
                <button onclick="sendMessage()" class="send-button">전송</button>
            </div>
        </div>
    
        <script th:src="@{/js/script.js}"></script>
    </body>
    
    </html>

    다음은 styles.css 입니다.

    body {
        font-family: Arial, sans-serif;
        margin: 0;
        padding: 0;
        background-color: #f0f0f0;
    }
    
    .chat-box {
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        height: 100vh;
    }
    
    .chat-container {
        width: 80%;
        max-width: 400px;
        margin: 0 auto;
        background-color: #fff;
        border-radius: 10px;
        box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
        overflow-y: auto;
        height: 400px;
        padding: 20px;
    }
    
    .message {
        margin-bottom: 10px;
        padding: 10px;
        border-radius: 10px;
        max-width: 70%;
    }
    
    .sender-message {
        background-color: #f3d6c6;
        align-self: flex-end;
    }
    
    .user-message {
        background-color: #ccc;
    }
    
    .input-container {
        display: flex;
        margin-top: 20px;
    }
    
    .message-input {
        flex: 1;
        max-width: 400px;
        padding: 10px;
        border-radius: 5px;
        border: 1px solid #ccc;
    }
    
    .send-button {
        margin-left: 10px;
        padding: 10px 20px;
        border: none;
        border-radius: 5px;
        background-color: #4caf50;
        color: white;
        cursor: pointer;
    }

     

    위의 HTML과 CSS를 합쳐 아래의 화면이 나오도록 구성하였습니다.

     

    다음은 script.js 입니다.

    // WebSocket 서버 주소
    const websocketUrl = 'ws://localhost:8080/ws/myHandler';
    
    // WebSocket 연결
    const socket = new WebSocket(websocketUrl);
    
    // 연결이 열렸을 때 실행되는 이벤트 핸들러
    socket.addEventListener('open', (event) => {
        console.log('WebSocket 연결이 열렸습니다.');
    });
    
    // 메시지를 받았을 때 실행되는 이벤트 핸들러
    socket.addEventListener('message', (event) => {
        // 서버로부터 받은 메시지를 화면에 표시
        const receivedMessage = JSON.parse(event.data);
        displayMessage(receivedMessage.sender, receivedMessage.content, false);
    });
    
    // 연결이 닫혔을 때 실행되는 이벤트 핸들러
    socket.addEventListener('close', (event) => {
        console.log('WebSocket 연결이 닫혔습니다.');
    });
    
    function sendMessage() {
        var messageInput = document.getElementById('message-input');
        var message = messageInput.value.trim();
        if (message === '') return;
    
        var username = localStorage.getItem('username');
    
        const chatMessage = {
            sender: username,
            content: message
        }
    
        // 메시지를 WebSocket을 통해 서버로 전송
        socket.send(JSON.stringify(chatMessage));
    
        // // 입력한 메시지를 화면에 표시 (옵션)
        // displayMessage(username, message, true);
    
        // 입력 칸 비우기
        messageInput.value = '';
    }
    
    // 화면에 메시지를 표시하는 함수 (옵션)
    function displayMessage(sender, message, isSender) {
        var chatContainer = document.getElementById('chat-container');
        var messageElement = document.createElement('div');
        messageElement.textContent = sender + ': ' + message;
        messageElement.classList.add('message');
    
        var username = localStorage.getItem('username');
    
        if (username === sender) {
            messageElement.classList.add('sender-message');
        } else {
            messageElement.classList.add('user-message');
        }
    
        chatContainer.appendChild(messageElement);
    
        // 최하단으로 스크롤
        chatContainer.scrollTop = chatContainer.scrollHeight;
    }
    
        // 로컬 스토리지에서 유저 이름을 불러오고 입력 필드에 채우는 함수
    function loadUsername() {
        var savedUsername = localStorage.getItem('username');
        if (savedUsername) {
            document.getElementById('username').value = savedUsername;
        }
    }
    
        // 유저 이름을 로컬 스토리지에 저장하는 함수
    function saveUsername() {
        var usernameInput = document.getElementById('username');
        var username = usernameInput.value.trim();
        if (username === '') return;
    
        localStorage.setItem('username', username);
        alert('유저 이름이 저장되었습니다: ' + username);
    }
    
    // 페이지 로드 시 유저 이름을 불러옴
    loadUsername();
    
    // 아래에 메시지 전송 함수 및 기타 함수들을 추가하면 됩니다.

     

    js 파일에서 볼 수 있듯이 앞서 WebSocket Configurer에서 설정한 주소인 ws/Handler로 연결을 요청하도록 되어 있습니다. 또한 웹소켓 연결이므로 http://가 아닌 ws:// 로 시작된다는 점을 확인하실 수 있습니다.

     

    앞선 작업들을 통해 다음과 같이 채팅창 기능이 구현되었습니다.