Spring_12_WebSocket (2.chatting 구현)

OngTK·2025년 9월 15일

Spring

목록 보기
12/25

🔗 WebSocket 연동 흐름 정리 (서버 ↔ 클라이언트)


✅ 1. 연결 시작 (Connection Established)

📌 클라이언트 (chatting.js)

// [2.1] 연결 성공 시 실행되는 이벤트 핸들러
client.onopen = (event) => {
    console.log("[Client] 연결 성공 / Successful Connection with Server");
    console.log(event);

    // [2.1.1] 방 번호와 닉네임 정보를 서버에 전송
    const msg = {
        type: "join",
        room: room,
        nickName: nickName
    };
    client.send(JSON.stringify(msg));
};

📌 서버 (ChatSocketHandler.java)

// [1] 클라이언트 소켓이 서버 소켓과 연결되었을 때 실행
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
    System.out.println("[Server] 연결 성공 / Successful connection with client socket");
}

✅ 2. 입장 처리 (type: "join")

📌 클라이언트

// [2.1.1] 연결 직후 서버에 입장 메시지 전송
const msg = {
    type: "join",
    room: room,
    nickName: nickName
};
client.send(JSON.stringify(msg));

📌 서버

// [3] 클라이언트가 서버에 메시지를 보냈을 때 실행
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
    System.out.println("[Server] 메시지 수신 / message from client socket");

    // [3.1] 메시지 내용 출력
    System.out.println(message.getPayload());

    // [3.2] JSON 문자열을 Map으로 변환
    Map<String, String> msg = objectMapper.readValue(
        message.getPayload(),
        new TypeReference<>() {}
    );

    // [3.3] 메시지 타입이 'join'일 경우
    if (msg.get("type").equals("join")) {
        String room = msg.get("room");
        String nickName = msg.get("nickName");

        // [3.4] 세션에 부가 정보 저장
        session.getAttributes().put("room", room);
        session.getAttributes().put("nickName", nickName);

        // [3.5] 접속 명단에 세션 등록
        if (connectingMap.containsKey(room)) {
            connectingMap.get(room).add(session);
        } else {
            List<WebSocketSession> list = new Vector<>();
            list.add(session);
            connectingMap.put(room, list);
        }

        // [3.6] 입장 알림 메시지 전송
        alarmMessage(room, nickName + "이 입장했습니다.");
    }

✅ 3. 메시지 전송 (type: "msg")

📌 클라이언트

// [3] 메시지 전송 함수
const onMsgSend = () => {
    const msginput = document.querySelector(".msginput");
    const message = msginput.value;

    if (message === '') return;

    const msg = {
        type: "msg",
        message: message,
        from: nickName,
        date: new Date().toLocaleString()
    };

    client.send(JSON.stringify(msg));
    msginput.value = '';
};

📌 서버

// [3.7] 메시지 타입이 'msg'일 경우
else if (msg.get("type").equals("msg")) {
    String room = (String) session.getAttributes().get("room");

    // [3.9] 같은 방에 있는 모든 세션에 메시지 전송
    for (WebSocketSession client : connectingMap.get(room)) {
        client.sendMessage(message);
    }
}

✅ 4. 서버 → 클라이언트 메시지 수신 처리

📌 클라이언트

client.onmessage = (event) => {
    console.log("[Client] 메시지 수신 / Incoming Message from Server");

    const message = JSON.parse(event.data);
    const msgbox = document.querySelector(".msgbox");
    let html = '';

    if (message.type === "alarm") {
        html += `${message.message}`;
    } else if (message.type === "msg") {
        if (message.from === nickName) {
            html += `${message.date} ${message.message}`;
        } else {
            html += `${message.from} ${message.message} ${message.date}`;
        }
    }

    msgbox.innerHTML += html;
    msgbox.scrollTop = msgbox.scrollHeight;
};

📌 서버

// [4] 알림 메시지 전송 함수
public void alarmMessage(String room, String message) throws Exception {
    Map<String, String> msg = new HashMap<>();
    msg.put("type", "alarm");
    msg.put("message", message);

    String sendMsg = objectMapper.writeValueAsString(msg);

    for (WebSocketSession session : connectingMap.get(room)) {
        session.sendMessage(new TextMessage(sendMsg));
    }
}

✅ 5. 연결 종료 처리

📌 클라이언트

client.onclose = (event) => {
    console.log("[Client] 연결 종료 / Shutting down the connection with Server");
    console.log(event);
};

📌 서버

@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
    System.out.println("[Server] 연결 종료 / Shutting down the connection with the client socket");

    String room = (String) session.getAttributes().get("room");
    String nickName = (String) session.getAttributes().get("nickName");

    if (room != null && nickName != null) {
        List<WebSocketSession> list = connectingMap.get(room);
        list.remove(session);

        alarmMessage(room, nickName + "이 퇴장했습니다.");
    }
}

✅ ChatSocketHandler


@Component      // Spring container Bean 등록
public class ChatSocketHandler extends TextWebSocketHandler {

    // [0] 접속된 ClientSocket List
    private static final Map<String, List<WebSocketSession>> connectingMap = new Hashtable<>();
    /**
     * <p>ex) ConnectingMap</p>
     * <p>Key : String : ChatRoomNo</p>
     * <p>value : List<WebSocketSession> : Clients</p>
     * <p>{ 0 : [ A , B ] }, { 1 : [ C , D ] }</p>
     */

    // ※ ObjectMapper : JSON <-> Map 변환 라이브러리
    private final ObjectMapper objectMapper = new ObjectMapper();

    /// 주요 메소드
    /// - objectMapper.readValue(JSON 문자열, 변환할 클래스명.class) : Json 객체 > 클래스 타입으로 변환
    /// - objectMapper.writeValueAsString( Map객체 ) : Map 객체 > JSON 으로 변환

    // [1] 클라이언트 소켓 - 서버 소켓 연동 시작 이벤트
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
//        System.out.println("ChatSocketHandler.afterConnectionEstablished");
//        System.out.println("session = " + session);
        System.out.println("[Server] 연결 성공 / Successful connection with client socket");

    } // func end

    // [2] 클라이언트 소켓 - 서버 소켓 연동 종료 이벤트
    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
//        System.out.println("ChatSocketHandler.afterConnectionClosed");
//        System.out.println("session = " + session + ", status = " + status);
        System.out.println("[Server] 연결 종료 / Shutting down the connection with the client socket");

        // [2.1] 접속 종료된 session 정보 확인
        String room = (String) session.getAttributes().get("room");         // 기본타입 Object
        String nickName = (String) session.getAttributes().get("nickName");

        // [2.2] room 과 nickName이 일치하는 데이터가 접속명단에 존재하면 >> session 제거
        if (room != null && nickName != null) {
            List<WebSocketSession> list = connectingMap.get(room);  // 해당 방번호의 접속명단 꺼내기
            list.remove(session);                                   // 명단에서 session 제거

            // [2.3] 퇴장한 nickName으로 알림메시지 보내기 (4번 func) - 입장 알림
            alarmMessage(room, nickName + "이 퇴장했습니다.");
        }
    } // func end

    // [3] 클라이언트 소켓 - 서버 소켓 메시지 인입 이벤트
    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
//        System.out.println("ChatSocketHandler.handleMessage");
//        System.out.println("session = " + session + ", message = " + message);
        System.out.println("[Server] 메세지 / message from client socket");

        // [3.1] Client Message
        System.out.println(message.getPayload());

        // [3.2] Message의 JSON 형식을 Map 타입으로 변환
        // ※ RESTFul API 는 @ResponseBody를 통해 JSON <-> Map 변환을 제공
        // Socket 은 지원하지 않으니 직접 해야함

        Map<String, String> msg = objectMapper.readValue(
                message.getPayload(),
                new com.fasterxml.jackson.core.type.TypeReference<Map<String, String>>() {
                }
        );
        /// As-is : Map<String, String> msg = objectMapper.readValue(message.getPayload(), Map.class);
        /// 아래와 같은 경고 발생
        /// uses unchecked or unsafe operations.
        /// Note: Recompile with -Xlint:unchecked for details.
        /// 요약 : 제네릭 타입이 명시되지 않아 발생하는 오류
        /// ObjectMapper가 Map<String, String> 타입으로 안전하게 변환할 수 있는 코드로 변경

        // [3.3] if 메세지 타입이 join 이면
        if (msg.get("type").equals("join")) {
            String room = msg.get("room");          // 방번호
            String nickName = msg.get("nickName");  // 접속 닉네임

            // [3.4] Client Socket의 부가정보를 추가
            session.getAttributes().put("room", room);
            session.getAttributes().put("nickName", nickName);

            // [3.5] 접속명단 등록
            // IF 방이 존재하면 해당 방에 접속한 session을 저장
            if (connectingMap.containsKey(room)) {
                connectingMap.get(room).add(session); // 해당 방번호에 session 정보를 추가
            } else { // 방이 존재하지 않으면
                List<WebSocketSession> list = new Vector<>();
                list.add(session);
                connectingMap.put(room, list);
            }

            // [3.6] 접속한 nickName으로 알림메시지 보내기 (4번 func) - 입장 알림
            alarmMessage(room, nickName + "이 입장했습니다.");
        } // [3.3] for end
        // [3.7] If 메시지 Type이 msg이면
        else if (msg.get("type").equals("msg")) {
            // [3.8] 방 번호 확인
            String room = (String) session.getAttributes().get("room");
            // [3.9] 같은 방에 있는 모든 세션에 메시지 전송
            for (WebSocketSession client : connectingMap.get(room)){
                client.sendMessage(message);
            }
        } // [3.7] for end
        System.out.println(connectingMap);
    } // func end

    // [4] session IO 여부에 따른 메세지 전달

    /// @param room    : 방번호
    /// @param message : 메시지 내용
    public void alarmMessage(String room, String message) throws Exception {
        // [4.1] Message 정보를 Map 타입으로 구성
        Map<String, String> msg = new HashMap<>();
        msg.put("type", "alarm");
        msg.put("message", message);

        // [4.2] Map 타입을 Json 형식 문자열로 변환
        String sendMsg = objectMapper.writeValueAsString(msg);

        // [4.3] 현재 같은 방에 위치한 모든 세션에게 메시지·알람 전달
        for (WebSocketSession session : connectingMap.get(room)) {
            session.sendMessage(new TextMessage(sendMsg));
        }

    } // func end


} // class end

✅ WebSocketConfig

@Component
@EnableWebSocket    // webSocket 활성화
public class WebSocketConfig implements WebSocketConfigurer {

    @Autowired
    private ChatSocketHandler chatSocketHandler;    // 의존성 주입(Dependency Injection)

    // [1] SocketHandler, 매핑 주소를 등록(registry)
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(chatSocketHandler, "/chat");
    } // func end

} // class end

✅ chatting.js

console.log("chatting js exe")

// [0.1] concept : 익명/비회원제 채팅
// Math.floor(Math.random() * 끝 값) + 시작값
const randomId = Math.floor(Math.random() * 1000) + 1 // 3자리에 난수 발생
const nickName = `익명${randomId}` // 익명+난수


// [0.2] RoomNo : 상단 메뉴에서 선택
// 전체 채팅 : X / 1번방 room = 1 / 2번방 room = 2
const params = new URL(location.href).searchParams;
const room = params.get("room") || "0";         // QueryString 가져오기, room이 없으면 0


// [1] ClinetSocket - ServerSocket Connectrion ==================================
const clinet = new WebSocket("/chat")


// [2] Func =====================================================================
// [2.1] 연결 성공 / ClientSocket and ServerSocket Connection Successful
clinet.onopen = (event) => {
    console.log("[Client] 연결 성공 / Successful Connection with Server")
    console.log(event)

    // [2.1.1] 방번호에 특정한 닉네임을 등록하는 메시지 보내기 
    // JSON 형식
    let msg = { type: "join", room: room, nickName: nickName }
    clinet.send(JSON.stringify(msg));
} // func end


// [2.2] 연결 종료 / Shutting down the connection between ClientSocket and ServerSocket
clinet.onclose = (event) => {
    console.log("[Client] 연결 종료 / Shutting down the connection with Server")
    console.log(event)
} // func end


// [2.3] 메시지 / message from ServerSocket
clinet.onmessage = (event) => {
    console.log("[Client] 메세지 / Incomming Message from Server")

    // [2.3.1] 메시지 정보 확인
    console.log(event)
    // console.log(event.data)

    // [2.3.2] 받은 메세지를 JSON 타입으로 변환
    const message = JSON.parse(event.data);

    // [2.3.3] 받은 메세지의 타입을 확인 및 반영할 html 준비
    const msgbox = document.querySelector(".msgbox")
    let html = '';

    if (message.type == "alarm") {
        html += `<div class="alarm"> <span>${message.message}</span></div>`
    }
    // [2.3.5] type msg 수신 시
    else if (message.type == "msg") {
        // [2.3.6] msg 발송자가 자신이면
        if (message.from == nickName) {
            html += `<div class="secontent">
                        <div class="date"> ${message.date} </div>
                        <div class="content"> ${message.message} </div>
                    </div>`;
        }
        // [2.3.7] msg 발송자가 내가 아니라면
        else {
            html += `<div class="receiveBox">
                        <div class="profileImg">
                            <img  src="default.jpg"/>
                        </div>
                        <div>
                            <div class="recontent">
                                <div class="memberNic"> ${message.from} </div>
                                <div class="subcontent">
                                    <div class="content"> ${message.message} </div>
                                    <div class="date"> ${message.date} </div>
                                </div>
                            </div>
                        </div>
                    </div>`;
        }
    }
    // [2.3.4] 구성한 html 반영
    msgbox.innerHTML += html;

    // [2.3.8] div msgbox 가 고정 사이즈보다 커지면 자동 스크롤 내리기
    msgbox.scrollTop = msgbox.scrollHeight 
    // scrollTop : 현재 dom의 스크롤 상단 꼭지점 위치
    // scrollHeight : 스크롤 전체 길이
    // == 스크롤 상단 꼭지점위치를 위에서 부터 전체길이 만큼 내린 곳에 위치시켜라

} // func end


// [2.4] 에러 / error
clinet.onerror = (event) => {
    console.log("[Client] 에러 / Error")
    console.log(event)
} // func end


// [3] 메세지 전송하기
const onMsgSend = () => {

    // [3.1] input에서 값(msg txt) 가져오기
    const msginput = document.querySelector(".msginput")
    const message = msginput.value;
    // [3.2] 입력값이 없으면 종료
    if (message == '') return

    // [3.3] 메시지 구성
    const msg = { type: "msg", message: message, from: nickName, date : new Date().toLocaleString() }

    // [3.4] 메시지를 server socket 으로 송신
    clinet.send(JSON.stringify(msg))

    // [3.5] input 초기화
    msginput.value = '';

} // func end


// [4] 엔터키를 눌렀을 때, 메세지 발송
const enterKey = () => {
    // [4.1] input에서 enter을 눌렀다 땠을 때!(onkeyup) 정보를 가져옴
    // 참고 : JS의 Keycode(ACSII Code)
    // enter : 13
    if (window.event.keyCode == 13) {
        // [4.2] 메시지 전송 함수
        onMsgSend()
    }
} // func end
profile
2025.05.~K디지털_풀스택 수업 수강중

0개의 댓글