1. 네이버 새로고침
•
새로고침을 하면 계속 HTTP 요청을 하는 것을 의미 - 실시간 X
2. 통신 방식
3. WebSocket이란
•
WebSocket 은 양방향 통신이다.
•
클라이언트와 서버가 지속적으로 통신이 가능하다.
•
WebSocket도 연결을 하기 위해선 최초에 한번은 HTTP 요청을 보낸다.
4. Spring에서 WebSocket을 사용하는 두 가지
Spring WebSocket
Spring WebSocket + STOMP
•
STOMP 프로토콜이 해주는 일
◦
메시지 목적지 헤더에 명시
◦
명령어로 구분
◦
헤더/바디 분리
◦
브로커가 각 채널 구독자에게만 자동으로 메시지 분배
•
왜 STOMP?
◦
생산성과 유지보수성
◦
코드가 간결하고 실수 가능성이 적음
◦
확장성/변경 용이
5. WebSocket vs STOMP over WebSocket
6. 실습
순수 WebSocket 채팅 서버 실습
•
ChatWebSocketHandler 생성
package org.example.backendproject.purewebsocket.handler;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import org.example.backendproject.purewebsocket.dto.ChatMessage;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
public class ChatWebSocketHandler extends TextWebSocketHandler {
// 동시성 문제를 해결 - 서버에 여러 클라이언트 접속 시에 발생할 수 있는 데이터 손실 고려
private final Set<WebSocketSession> sessions = Collections.synchronizedSet(new HashSet<>());
// json 문자열 -> 자바 객체로 변환
private final ObjectMapper objectMapper = new ObjectMapper();
// 클라이언트가 보낸 메세지를 서버가 받았을 때 호출
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message)
throws Exception {
super.handleTextMessage(session, message);
// json 문자열 -> 자바 객체
ChatMessage chatMessage = objectMapper.readValue(message.getPayload(), ChatMessage.class);
for (WebSocketSession s : sessions) {
if (s.isOpen()) {
// 자바 객체 -> json 문자열
s.sendMessage(new TextMessage(objectMapper.writeValueAsString(chatMessage)));
System.out.println("전송된 메세지 = " + chatMessage.getMessage());
}
}
}
// 클라이언트가 웹 소켓 서버에 접속했을 때 호출
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
super.afterConnectionEstablished(session);
sessions.add(session); // 연결된 클라이언트 저장
System.out.println("접속된 클라이언트 세션 ID = " + session.getId());
}
// 클라이언트가 연결이 끊어졌을 때 호출
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status)
throws Exception {
super.afterConnectionClosed(session, status);
sessions.remove(session);
}
}
Java
복사
•
ChatMessage 생성
package org.example.backendproject.purewebsocket.dto;
import lombok.Getter;
@Getter
public class ChatMessage {
private String message;
private String from;
}
Java
복사
•
WebSocketConfig 생성
package org.example.backendproject.purewebsocket.config;
import org.example.backendproject.purewebsocket.handler.ChatWebSocketHandler;
import org.springframework.context.annotation.Configuration;
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
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(new ChatWebSocketHandler(), "/ws-chat")
.setAllowedOriginPatterns("*");
// ws-chat 엔드포인트로 요청을 보낼 수 있는지 결정하는 보안 정책 설정
// * <- 모든 도메인에서 접근 가능
}
}
Java
복사
•
HtmlController 생성
package org.example.backendproject.purewebsocket;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class HtmlController {
@GetMapping("/")
public String index() {
return "redirect:/purechat1.html";
}
}
Java
복사
•
static 폴더에 purechat1.html 추가
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>Pure WebSocket Chat</title>
<style>
body { font-family: 'Segoe UI', sans-serif; background: #f7f8fa; }
.container {
width: 400px; margin: 60px auto; background: #fff; padding: 32px 30px;
border-radius: 14px; box-shadow: 0 2px 8px rgba(0,0,0,0.07);
}
h2 { text-align: center; color: #2c3e50; margin-bottom: 24px; }
#chatArea {
width: 100%; height: 250px; border: 1px solid #aaa;
border-radius: 8px; margin-bottom: 18px; overflow-y: auto;
background: #fafdff; padding: 10px 7px; font-size: 15px;
}
.row { display: flex; gap: 10px; align-items: center; margin-bottom: 13px; }
input[type="text"] {
box-sizing: border-box; border: 1px solid #ccc; border-radius: 6px;
font-size: 15px; padding: 9px; outline: none; background: #f9fafd;
transition: border 0.2s;
}
input[type="text"]:focus { border-color: #4078c0; background: #fff; }
#user { width: 110px; }
#msg { flex: 1; min-width: 0; }
button {
background: #4078c0; color: white; font-weight: bold;
border: none; border-radius: 6px; padding: 10px 20px;
font-size: 15px; cursor: pointer; transition: background 0.2s;
}
button:hover { background: #285690; }
.btn-disconnect {
background: #eee; color: #285690; font-weight: bold;
}
.btn-disconnect:hover { background: #e0e8f5; }
.sysmsg { color: #666; font-style: italic; margin: 7px 0 3px 0;}
.msgrow { margin-bottom: 3px;}
.from { font-weight: bold; color: #4078c0;}
.hidden { display: none; }
</style>
</head>
<body>
<div class="container">
<h2>Pure WebSocket Chat</h2>
<!-- 로그인 영역 -->
<div class="row" style="margin-bottom: 15px;">
<input type="text" id="user" placeholder="닉네임">
<button onclick="connect()">Connect</button>
<button class="btn-disconnect" onclick="disconnect()">Disconnect</button>
</div>
<!-- 채팅 영역 (처음엔 숨김) -->
<div id="chatWrapper" class="hidden">
<div id="chatArea"></div>
<div class="row">
<input type="text" id="msg" placeholder="메시지" onkeydown="if(event.key==='Enter'){sendMessage();}">
<button onclick="sendMessage()">Send</button>
</div>
</div>
</div>
<script>
let ws = null;
function connect() {
const user = document.getElementById("user").value;
if (!user) {
alert("닉네임을 입력하세요!");
return;
}
ws = new WebSocket("/ws-chat");
ws.onopen = function () {
showSysMsg('Connected!');
document.getElementById("chatWrapper").classList.remove("hidden");
};
ws.onmessage = function (event) {
const msg = JSON.parse(event.data);
showMessage(msg.from, msg.message);
};
ws.onclose = function () {
showSysMsg('Disconnected');
document.getElementById("chatWrapper").classList.add("hidden");
};
}
function disconnect() {
if (ws) {
ws.close();
ws = null;
}
}
function sendMessage() {
const user = document.getElementById("user").value;
const msg = document.getElementById("msg").value;
if (!user || !msg) {
alert("닉네임과 메시지를 모두 입력하세요!");
return;
}
ws.send(JSON.stringify({ from: user, message: msg }));
document.getElementById("msg").value = "";
}
function showMessage(from, message) {
const chatArea = document.getElementById("chatArea");
chatArea.innerHTML += `<div class="msgrow"><span class="from">${from}:</span> ${message}</div>`;
chatArea.scrollTop = chatArea.scrollHeight;
}
function showSysMsg(msg) {
const chatArea = document.getElementById("chatArea");
chatArea.innerHTML += `<div class="sysmsg">${msg}</div>`;
chatArea.scrollTop = chatArea.scrollHeight;
}
</script>
</body>
</html>
JavaScript
복사
•
◦
닉네임 입력 후 Connect 클릭
•
Postman에서 다음과 같이 입력 후 연결 시도
•
연결 확인
•
다음과 같이 입력 후 Send 버튼 클릭
메세지 내용 확인 + 헤더 내용 확인
entity → repository → service → controller 순서로 room개발 진행
순수 WebSocket 채팅 서버 실습 - room 생성
•
entity 생성 (room > entity)
package org.example.backendproject.purewebsocket.room.entity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@Entity
@Table(name = "chat_room")
public class ChatRoom {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true)
private String roomId;
}
Java
복사
•
repository 생성
package org.example.backendproject.purewebsocket.room.repository;
import java.util.Optional;
import org.example.backendproject.purewebsocket.room.entity.ChatRoom;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface RoomRepository extends JpaRepository<ChatRoom, Long> {
Optional<ChatRoom> findByRoomId(String roomId);
}
Java
복사
•
service 생성
package org.example.backendproject.purewebsocket.room.service;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.example.backendproject.purewebsocket.room.entity.ChatRoom;
import org.example.backendproject.purewebsocket.room.repository.RoomRepository;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class RoomService {
private final RoomRepository roomRepository;
public ChatRoom createRoom(String roomId) {
return roomRepository.findByRoomId(roomId)
.orElseGet(() -> {
ChatRoom chatRoom = new ChatRoom();
chatRoom.setRoomId(roomId);
return roomRepository.save(chatRoom);
});
}
public List<ChatRoom> findAllRooms() {
return roomRepository.findAll();
}
}
Java
복사
•
controller 생성
package org.example.backendproject.purewebsocket.room.controller;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.example.backendproject.purewebsocket.room.entity.ChatRoom;
import org.example.backendproject.purewebsocket.room.service.RoomService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/rooms")
public class RoomController {
private final RoomService roomService;
@GetMapping
public List<ChatRoom> getAllRoom() {
return roomService.findAllRooms();
}
@PostMapping("/{roomId}")
public ChatRoom createRoom(@PathVariable String roomId) {
return roomService.createRoom(roomId);
}
}
Java
복사
•
HtmlController 코드 변경
package org.example.backendproject.purewebsocket;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class HtmlController {
@GetMapping("/")
public String index() {
return "redirect:/purechat2.html";
}
}
Java
복사
•
html 파일 생성
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>Room-Based WebSocket Chat</title>
<style>
body { font-family: 'Segoe UI', sans-serif; background: #f7f8fa; }
.container {
width: 540px;
margin: 60px auto;
background: #fff;
padding: 32px 30px;
border-radius: 14px;
box-shadow: 0 2px 8px rgba(0,0,0,0.07);
}
h2 { text-align: center; color: #2c3e50; margin-bottom: 24px; }
.row { display: flex; gap: 10px; align-items: center; margin-bottom: 13px; }
input[type="text"] {
box-sizing: border-box;
border: 1px solid #ccc;
border-radius: 6px;
font-size: 15px;
padding: 9px;
outline: none;
background: #f9fafd;
transition: border 0.2s;
width: 100%;
max-width: 130px;
}
input[type="text"]:focus {
border-color: #4078c0;
background: #fff;
}
button {
background: #4078c0; color: white; font-weight: bold;
border: none; border-radius: 6px; padding: 10px 20px;
font-size: 15px; cursor: pointer; transition: background 0.2s;
}
button:hover { background: #285690; }
.btn-disconnect {
background: #eee; color: #285690; font-weight: bold;
}
.btn-disconnect:hover { background: #e0e8f5; }
#chatArea {
width: 100%; height: 250px; border: 1px solid #aaa;
border-radius: 8px; margin-bottom: 18px; overflow-y: auto;
background: #fafdff; padding: 10px 7px; font-size: 15px;
}
.sysmsg { color: #666; font-style: italic; margin: 7px 0 3px 0; }
.msgrow { margin-bottom: 3px; }
.from { font-weight: bold; color: #4078c0; }
.hidden { display: none; }
#roomList { margin-bottom: 20px; }
.room-item {
padding: 6px 10px;
border: 1px solid #ccc;
border-radius: 6px;
margin-bottom: 5px;
cursor: pointer;
}
.room-item:hover {
background: #e8f0ff;
}
</style>
</head>
<body>
<div class="container">
<h2>Pure WebSocket Chat Room ADD</h2>
<!-- 방 목록 표시 -->
<div id="roomList">
<strong>방 목록:</strong>
<div id="rooms"></div>
</div>
<!-- 닉네임 입력 및 disconnect -->
<div class="row" id="nicknameRow">
<!-- <input type="text" id="user" placeholder="닉네임">-->
<input type="text" id="room" placeholder="방 번호">
<button onclick="manualConnect()">Connect</button>
</div>
<!-- 채팅 영역 (처음엔 숨김) -->
<div id="chatWrapper" class="hidden">
<div id="chatArea"></div>
<div class="row">
<input type="text" id="msg" placeholder="메시지" onkeydown="if(event.key==='Enter'){sendMessage();}">
<button onclick="sendMessage()">Send</button>
<button class="btn-disconnect" onclick="disconnect()">Disconnect</button>
</div>
</div>
</div>
<script>
let ws = null;
let roomId = "";
let nickname = "";
window.onload = function () {
loadRoomList();
};
function loadRoomList() {
fetch("/api/rooms")
.then(response => response.json())
.then(data => {
const roomsDiv = document.getElementById("rooms");
const roomListSection = document.getElementById("roomList");
roomsDiv.innerHTML = "";
if (data.length === 0) {
roomListSection.classList.add("hidden");
} else {
roomListSection.classList.remove("hidden");
data.forEach(room => {
const div = document.createElement("div");
div.className = "room-item";
div.textContent = `방 번호: ${room.roomId}`;
div.onclick = () => enterRoom(room.roomId);
roomsDiv.appendChild(div);
});
}
});
}
function enterRoom(id, inputNick = null) {
if (!inputNick) {
const nick = prompt("닉네임을 입력하세요:");
if (!nick) {
alert("닉네임이 필요합니다.");
return;
}
nickname = nick;
} else {
nickname = inputNick;
}
roomId = id;
//방목록 데이터베이스에 저장하는 곳
//방목록 데이터베이스에 저장하는 곳
//방목록 데이터베이스에 저장하는 곳
fetch(`/api/rooms/${roomId}`, { method: "POST" }) // 주석 해제
ws = new WebSocket("/ws-chat");
ws.onopen = function () {
showSysMsg(`[${nickname}]님이 방 [${roomId}]에 입장했습니다.`);
document.getElementById("chatWrapper").classList.remove("hidden");
document.getElementById("roomList").classList.add("hidden");
document.getElementById("nicknameRow").classList.add("hidden");
};
ws.onmessage = function (event) {
const msg = JSON.parse(event.data);
showMessage(msg.from, msg.message, msg.roomId);
};
ws.onclose = function () {
showSysMsg("Disconnected");
document.getElementById("chatWrapper").classList.add("hidden");
document.getElementById("roomList").classList.remove("hidden");
document.getElementById("nicknameRow").classList.remove("hidden");
loadRoomList();
};
}
function manualConnect() {
const inputRoom = document.getElementById("room").value;
if (!inputRoom) {
alert("방 번호를 입력하세요.");
return;
}
const inputNick = prompt("닉네임을 입력하세요:");
if (!inputNick) {
alert("닉네임이 필요합니다.");
return;
}
nickname = inputNick;
enterRoom(inputRoom, inputNick);
}
function disconnect() {
if (ws) {
ws.close();
ws = null;
}
}
function sendMessage() {
const msg = document.getElementById("msg").value;
if (!nickname || !msg || !roomId) {
alert("모든 정보를 입력해주세요.");
return;
}
ws.send(JSON.stringify({
from: nickname,
message: msg,
roomId: roomId
}));
document.getElementById("msg").value = "";
}
function showMessage(from, message, room) {
const chatArea = document.getElementById("chatArea");
chatArea.innerHTML += `<div class="msgrow"><span class="from">[${room}] ${from}:</span> ${message}</div>`;
chatArea.scrollTop = chatArea.scrollHeight;
}
function showSysMsg(msg) {
const chatArea = document.getElementById("chatArea");
chatArea.innerHTML += `<div class="sysmsg">${msg}</div>`;
chatArea.scrollTop = chatArea.scrollHeight;
}
</script>
</body>
</html>
HTML
복사
•
결과 확인
Spring WebSocket + STOMP 채팅 서버 (1:1), 단체
•
ChatMessage 생성
package org.example.backendproject.stompwebsocket.dto;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class ChatMessage {
private String message;
private String from;
private String to; // 귓속말을 받을 사람
private String roomId; // 방 id
}
Java
복사
•
WebSocketConfig 생성
package org.example.backendproject.stompwebsocket.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/topic", "/queue"); // 구독용 경로 서버 -> 클라이언트
registry.setApplicationDestinationPrefixes("/app"); // 전송용 경로 클라이언트 -> 서버
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws-chat")
.setAllowedOriginPatterns("*");
}
}
Java
복사
•
ChatController 생성
package org.example.backendproject.stompwebsocket.controller;
import org.example.backendproject.stompwebsocket.dto.ChatMessage;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.stereotype.Controller;
@Controller
public class ChatController {
@MessageMapping("/chat.sendMessage")
@SendTo("/topic/public")
public ChatMessage sendMessage(ChatMessage message) {
return message;
}
}
Java
복사
•
HtmlController 변경 - html 파일 추가
package org.example.backendproject.purewebsocket;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class HtmlController {
@GetMapping("/")
public String index() {
return "redirect:/stompchat2.html";
}
}
Java
복사
•
stormchat2.html
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>WebSocket STOMP Chat</title>
<style>
body { font-family: 'Segoe UI', sans-serif; background: #f7f8fa; }
.container {
width: 400px; margin: 60px auto; background: #fff; padding: 32px 30px;
border-radius: 14px; box-shadow: 0 2px 8px rgba(0,0,0,0.07);
}
h2 { text-align: center; color: #2c3e50; margin-bottom: 20px;}
#chatArea {
width: 100%; height: 250px; border: 1px solid #aaa;
margin-bottom: 18px; overflow-y: auto; padding: 10px 7px; border-radius: 8px;
background: #fafdff; font-size: 15px;
}
.row { display: flex; gap: 10px; align-items: center; margin-bottom: 12px; }
input[type="text"] {
box-sizing: border-box; border: 1px solid #ccc; border-radius: 6px;
font-size: 15px; padding: 9px; outline: none; background: #f9fafd;
transition: border 0.2s;
}
input[type="text"]:focus { border-color: #4078c0; background: #fff; }
#user, #room { width: 110px; }
#msg { flex: 1; min-width: 0; }
button {
background: #4078c0; color: white; font-weight: bold;
border: none; border-radius: 6px; padding: 10px 20px;
font-size: 15px; cursor: pointer; transition: background 0.2s;
}
button:hover { background: #285690; }
.btn-disconnect {
background: #eee; color: #285690; font-weight: bold;
}
.btn-disconnect:hover { background: #e0e8f5; }
.sysmsg { color: #666; font-style: italic; margin: 7px 0 3px 0;}
.msgrow { margin-bottom: 3px;}
.from { font-weight: bold; color: #4078c0;}
.hidden { display: none; }
#roomList { margin-bottom: 20px; }
.room-item {
padding: 6px 10px; border: 1px solid #ccc; border-radius: 6px; margin-bottom: 5px;
cursor: pointer;
}
.room-item:hover { background: #e8f0ff; }
</style>
</head>
<body>
<div class="container">
<h2>Spring WebSocket + STOMP Chat</h2>
<!-- 방 목록 -->
<div id="roomList">
<strong>방 목록:</strong>
<div id="rooms"></div>
</div>
<!-- 입장 영역 -->
<div class="row" id="enterRow">
<input type="text" id="user" placeholder="닉네임">
<input type="text" id="room" placeholder="방 번호">
<button onclick="connect()">Connect</button>
</div>
<!-- 귓속말 대상 -->
<div class="row hidden" id="whisperRow">
<input type="text" id="whisperTo" placeholder="귓속말 대상 (닉네임)">
</div>
<!-- 채팅 영역 (입장 후에만 표시) -->
<div id="chatWrapper" class="hidden">
<div id="chatArea"></div>
<div class="row">
<input type="text" id="msg" placeholder="메시지">
<button onclick="sendMessage()">Send</button>
<button class="btn-disconnect" onclick="disconnect()">Disconnect</button>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/stompjs@2.3.3/lib/stomp.min.js"></script>
<script>
let stompClient = null;
let nickname = "";
let roomId = "";
window.onload = function () {
loadRoomList();
};
function loadRoomList() {
fetch("/api/rooms")
.then(response => response.json())
.then(data => {
const roomsDiv = document.getElementById("rooms");
const roomListDiv = document.getElementById("roomList");
roomsDiv.innerHTML = "";
if (data.length === 0) {
roomListDiv.classList.add("hidden");
} else {
roomListDiv.classList.remove("hidden");
data.forEach(room => {
const div = document.createElement("div");
div.className = "room-item";
div.textContent = `방 번호: ${room.roomId}`;
div.onclick = () => {
const nick = prompt("닉네임을 입력하세요:");
if (!nick) return;
document.getElementById("user").value = nick;
document.getElementById("room").value = room.roomId;
connect();
};
roomsDiv.appendChild(div);
});
}
});
}
function connect() {
nickname = document.getElementById("user").value;
roomId = document.getElementById("room").value;
if (!nickname || !roomId) {
alert("닉네임과 방 번호를 입력하세요!");
return;
}
//방생성기능
//방생성기능
fetch(`/api/rooms/${roomId}`, { method: "POST" })
// const socket = new WebSocket('/ws-chat');
const socket = new WebSocket('/ws-chat?nickname=' + nickname);
stompClient = Stomp.over(socket);
stompClient.connect({}, function (frame) {
showSysMsg(`[${nickname}]님이 방 [${roomId}]에 입장했습니다.`);
document.getElementById("chatWrapper").classList.remove("hidden");
document.getElementById("enterRow").classList.add("hidden");
document.getElementById("roomList").classList.add("hidden");
// document.getElementById("whisperRow").classList.remove("hidden"); // ✅ 여기! 닉네임 작성
stompClient.subscribe(`/topic/${roomId}`, function (chat) {
const message = JSON.parse(chat.body);
showMessage(message.from, message.message);
});
//귓속말
stompClient.subscribe('/user/queue/private', function (message) {
const msg = JSON.parse(message.body);
alert("💬 귓속말: " + msg.from + " - " + msg.message);
});
});
}
function disconnect() {
if (stompClient !== null) {
stompClient.disconnect();
}
showSysMsg('Disconnected');
document.getElementById("chatWrapper").classList.add("hidden");
document.getElementById("enterRow").classList.remove("hidden");
document.getElementById("roomList").classList.remove("hidden");
document.getElementById("whisperRow").classList.add("hidden"); // ✅ 추가
loadRoomList();
}
function sendMessage() {
const msg = document.getElementById("msg").value;
const toUser = document.getElementById("whisperTo").value.trim();
if (!nickname || !msg || !roomId) {
alert("모든 정보를 입력해주세요!");
return;
}
const payload = {
from: nickname,
message: msg,
roomId: roomId
};
if (toUser) {
payload.to = toUser; // 귓속말 대상이 있으면 to 추가
}
stompClient.send("/app/chat.sendMessage", {}, JSON.stringify(payload));
document.getElementById("msg").value = "";
}
function showMessage(from, message) {
const chatArea = document.getElementById("chatArea");
chatArea.innerHTML += `<div class="msgrow"><span class="from">${from}:</span> ${message}</div>`;
chatArea.scrollTop = chatArea.scrollHeight;
}
function showSysMsg(msg) {
const chatArea = document.getElementById("chatArea");
chatArea.innerHTML += `<div class="sysmsg">${msg}</div>`;
chatArea.scrollTop = chatArea.scrollHeight;
}
document.getElementById('msg').addEventListener('keydown', function(e) {
if (e.key === 'Enter') sendMessage();
});
</script>
</body>
</html>
JavaScript
복사
•
결과 확인
⇒ 기존에 WebSocket에서 만들었던 방들도 같이 확인 가능
•
네임, 방 번호 입력 후 Connect 버튼 클릭
•
메시지 입력 후 Send 버튼 클릭 - 메시지 내용 확인
귓속말 기능 적용
•
ChatController 수정
package org.example.backendproject.stompwebsocket.controller;
import lombok.RequiredArgsConstructor;
import org.example.backendproject.stompwebsocket.dto.ChatMessage;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Controller;
@Controller
@RequiredArgsConstructor
public class ChatController {
// 단일 브로드캐스트 (동적으로 방 생성이 안 됨)
// @MessageMapping("/chat.sendMessage")
// @SendTo("/topic/public")
// public ChatMessage sendMessage(ChatMessage message) {
// return message;
// }
@Value("${PROJECT_NAME:web Server}")
private String instanceName;
// 서버가 클라이언트에게 수동으로 메세지를 보낼 수 있도록 하는 클래스
private final SimpMessagingTemplate template;
// 동적으로 방 생성 가능
@MessageMapping("/chat.sendMessage")
public void sendMessage(ChatMessage message) {
message.setMessage(instanceName + " " + message.getMessage());
if (message.getTo() != null && !message.getTo().isEmpty()) {
// 귓속말
// 내 아이디로 귓속말 경로를 활성화 함
template.convertAndSendToUser(message.getTo(), "/queue/private", message);
} else {
// 일반 메시지
// message에서 roomId를 추출해서 해당 roomId를 구독하고 있는 클라이언트에게 메세지를 전달
template.convertAndSend("/topic/" + message.getRoomId(), message);
}
}
}
Java
복사
•
WebSocketConfig 수정
package org.example.backendproject.stompwebsocket.config;
import org.example.backendproject.stompwebsocket.handler.CustomHandshakeHandler;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
/**
* 구독중 Prefix
* /topic 일반 채팅을 받을 접두어
* /queue 귓속말을 받을 접두어
* 서버가 보내는 메시지를 클라이언트가 구독할 때 사용하는 경로
*/
registry.enableSimpleBroker("/topic", "/queue"); // 구독용 경로
/** 전송용 Prefix **/
// 클라이언트가 서버에 메시지를 보낼 때 사용하는 경로 접두어
registry.setApplicationDestinationPrefixes("/app"); // 클라이언트 -> 서버
// 서버가 특정 사용자에게 메시지를 보낼 때, 클라이언트가 구독할 경로 접두어
registry.setUserDestinationPrefix("/user"); // 서버 -> 특정 사용자
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws-chat")
.setHandshakeHandler(new CustomHandshakeHandler())
.setAllowedOriginPatterns("*");
}
}
Java
복사
•
handler 패키지 생성 후 StompPrincipal 생성
package org.example.backendproject.stompwebsocket.handler;
import java.security.Principal;
public class StompPrincipal implements Principal {
private final String name;
public StompPrincipal(String name) {
this.name = name;
}
@Override
public String getName() {
return name;
}
}
Java
복사
•
CustomHandshakeHandler 생성
package org.example.backendproject.stompwebsocket.handler;
import java.security.Principal;
import java.util.Map;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.support.DefaultHandshakeHandler;
public class CustomHandshakeHandler extends DefaultHandshakeHandler {
@Override
protected Principal determineUser(ServerHttpRequest request,
WebSocketHandler wsHandler,
Map<String, Object> attributes) {
String nickname = getNickName(request.getURI().getQuery());
return new StompPrincipal(nickname);
}
private String getNickName(String query) {
if (query == null || !query.contains("nickname=")) {
return "닉네임없음";
} else {
return query.split("nicknames=")[1];
}
}
}
Java
복사
•
서로 다른 브라우저로 통신 여부 확인
Docker 이미지 생성 + 컨테이너 생성 및 시작
•
이미지 생성 (Dockerfile 위치한 경로에서 실행)
docker build -t backend .
Shell
복사
•
컨테이너 생성 및 시작 (docker-compose.yaml 위치한 경로에서 실행)
docker compose up -d
Shell
복사
7. 취업 특강
•
지원하고 싶은 회사의 채용 페이지 확인
•
인재상, 조직문화가 핵심
•
내가 어떤 개발을 했을 때 가장 만족을 느끼는지에 대해 고민을 할 필요가 있음
•
산업군, 직무의 선택이 중요!! (자기소개서 작성)
◦
어떤 산업군으로 가야 할지 고민을 할 필요가 있음 - 왜 그 산업군인가?
◦
왜 그 회사인가? 왜 그 경쟁사인가?
◦
왜 그 진로를 선택했는가? (왜 그 직무?)
◦
왜 다른 지원자가 아니라 나를 선택해야 하는 이유에 대해 분명하게 말할 수 있어야 한다.
▪
나만의 강점!!
▪
ex) 게임 중의 특정 카테고리… - 캐시 카우 RPG 게임
•
자기소개서에 나는 xxx한 사람입니다 라는 문장을 쓰는 건 자충수를 두는 것이다.
•
그 회사에 들어가기 위한 목적의식이 있어야 한다.
•
검색했을 때 정보가 많이 안 나오는 회사이면 면접 찾기가 쉽지 않다.
•
단점 설명
◦
전 호기심이 너무 많은 사람이다.