241113 에이콘 아카데미 수업을 기반하여 작성되었음을 알립니다.
WebSocket
WebSocket은 기존의 단방향 HTTP 프로토콜과 호환되어 양방향 통신을 제공하기 위해 개발된 프로토콜.
Http 통신은 무상태성, 비연결성, 단방향 통신, 실시간 통신인 채팅 기능을 운영할 수 없다.
처음부터 WebSocket을 사용하지는 못하고 Http로 접속, 타겟팅을 한다음에 WebSocket 사용하여 통신할 수 있다.
WebSocket 통신은 양방향 통신이 가능 HTTP 통신과 다르게 연결을 맺고 바로 끊어버리는 게 아니라 계속 유지를 하기 때문에 실시간 통신에 적합하다.
먼저 STOMP 프로토콜에 대해서 이해를 해야 한다.
STOMP (Simple Text Oriented Messaging Protocol, 스트리밍 텍스트 지향 메시지 프로토콜)
간단한 메시지를 전송하기 위한 프로토콜로 메시지 브로커와 publisher - subscriber 방식을 사용
메시지의 발행자와 구독자가 존재하고 메시지를 보내는 사람과 받는 사람이 구분되어 있다.
웹소켓(WebSocket) 엔드포인트는 클라이언트와 서버 간에 웹소켓 연결을 시작하는 URL이다.
웹소켓 엔드포인트는 일반적으로 다음과 같은 형식을 따른다.
- ws:// 또는 wss://로 시작 (HTTP와 HTTPS와 유사하게, ws는 일반 웹소켓, wss는 보안 웹소켓을 의미)
웹 소켓 설정 : Websocket을 활성화하기 위한 Config 파일을 작성한다.
@EnableWebSocket을 선언하여 WebSocket을 활성화 하고, WebSocket을 접속하기 위한 endpoint 주소를 작성한다. 도메인이 다른 서버에서도 접속 가능 하도록 CORS : setAllowedOrigins("*") 설정을 추가.
클라이언트가 서버에 요청하면 스레드가 만들어짐, 클라이언트도, 서버도 소켓을 각자 만들어 통신을 주고 받는다!
1:1 통신 또는 1:N 통신 가능 이렇게 지속적인, 양방향 통신을 가능하게 하는 것이 소켓이다!
스프링에서는 websocket을 지원한다. 텍스트, 이미지 등등을 주고 받을 수 있다.
수신자는 /topic 경로를 구독하고 있고 발행자는 /app 혹은 /topic으로 메시지를 보내는 모습을 확인할 수 있다. 만약 발행자가 /topic 경로로 메시지를 보내면 바로 수신자에게 도착하고 /app 경로로 메시지를 보내면 가공을 한 다음 보내게 된다.
(채팅같은 경우 가공이 필요없어 바로 토픽으로 뺀다!)
WebSocket 실습 (메세지 주고받기)
dependencies
index
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
<!-- SockJS와 STOMP 라이브러리 읽기용 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.5.1/sockjs.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script>
<script type="text/javascript">
let stompClient = null;
function connect(){
// 웹소켓 어플리케이션 서버는 WebScoket의 endpoint를 만들고, 클라이언트는 endpoint의 URI를 통해 서버와 접속
let socket = new SockJS("/ws"); // "/ws" : 암호화되지 않은 웹소켓 연결
stompClient = Stomp.over(socket); // Stomp객체 생성 : 브로커 역할
// connect()는 Sockjs와 stomp client를 이용하여 Spring Boot에서 구성한 / ws 엔드 포인트에 연결합니다.
stompClient.connect({}, function(frame){
// frame 객체는 서버와의 연결 상태, 메시지, 또는 오류 정보를 담고 있는 데이터 구조로 이해할 수 있다.
console.log('connect : ' + frame);
stompClient.subscribe('/topic/messages', function(message){
showMessage(message.body);
});
});
}
function sendMessage(message){ // 서버로 메세지 전송
// 사용자가 입력한 메세지를 "/app/message"로 전송
let msgContent = document.querySelector("#message").value;
stompClient.send("/app/message",{},msgContent); // send(메세지 목적지,헤더,메세지:payload)
}
function showMessage(message){
// 수신된 메세지를 페이지에 표시
let msgElement = document.createElement("li");
msgElement.textContent = message;
document.querySelector("#msgArea").appendChild(msgElement);
}
window.onload = function(){
connect();
}
</script>
</head>
<body>
<div>
<input type="text" id="message" placeholder="메세지 입력"
onkeydown="if(event.keyCode == 13) sendMessage()" />
<button onclick="sendMessage()">전송</button>
</div>
<ul id="msgArea"></ul>
</body>
</html>
WebSocketConfig
package pack;
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 {
// WebSocketMessageBrokerConfigurer : 제공하는 추상메소드를 바디에 내용이 없는 일반메소드로 만들어놓음, 해당 메소드를 커스터마이징해서 써먹음
@Override // WebSocketMessageBroker의 동작을 재정의
public void configureMessageBroker(MessageBrokerRegistry registry) {
// MessageBrokerRegistry : MessageBroker의 대상(profile)을 설정, 외부 메세지 브로커로 라우팅(요청을 핸들러와 연결, 매핑하는 것이 라우팅이다)
// /topic으로 시작하는 메세지는 MessageBroker로 라우팅
registry.enableSimpleBroker("/topic");
// 클라이언트는 접두어 "/app"으로 시작하는 메세지를 서버로 보냄
registry.setApplicationDestinationPrefixes("/app");
}
@Override // Endpoint를 등록
public void registerStompEndpoints(StompEndpointRegistry registry) {
// 클라이언트가 WebSocket을 통해 연결할 수 있는 URL을 정의, SocketJS와 같은 콜백옵션을 설정 가능
registry.addEndpoint("/ws").withSockJS();
// 처음만 HTTP로 타겟팅, 이후 프로토콜을 WebSocket으로 바꾼다는 뜻
// "/ws" : 이 경로로 접속해 STOMP 메세지를 주고 받을 수 있다.
}
}
Controller
package pack;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.stereotype.Controller;
@Controller
public class MessageController {
// 클라이언트가 "/app/message"로 전송한 메세지를 처리
@MessageMapping("/message")
@SendTo("/topic/messages") // 처리된 메세지를 모두에게 전파시킴
public String sendMessage(String message) {
return message;
// 반환하는 메세지는 자동으로 "/topic/messages"를 구독하는 모든 클라이언트에게 전송
}
}
브라우저 바꿔가면서 전송 확인
남의 IP로 접속하여 전송 확인
CORS(Cross-Origin Resource Sharing) 처리
서로 다른 도메인 (웹 서버), 만약 내가 다음 사이트에서 아작스 요청을 했으면 다음 사이트에서 데이터를 가져와야 함, 네이버 사이트에서 가져올 수 없음, 다 막아놓음
https://naver.com과 http://naver.com은 프로토콜이 다르므로 다른 출처이다.
CORS(Cross-Origin Resource Sharing)은 서버측에서 헤더를 통해서 다른 출처(웹)에서 자원에 접근할 수 있는 권한을 부여하도록 브라우저에게 알려주는 정책이다.
웹에서 서버로 요청
브라우저에서 요청 헤더에 Origin을 추가로 담아서 보낸다.
서버에서는 보내는 응답 헤더 안에 Access-Control-Allow-Origin 넣어서 보낸다.
브라우저는 요청시 보낸 Origin과 응답 헤더 안에 담긴 Access-Control-Allow-Origin의 값을 비교한다
만약 Access-Control-Allow-Origin안에 Origin이 포함되지 않는다면 브라우저가 해당 응답을 버리고 CORS Policy를 위반했다는 에러를 console에 뿌려준다.
https://velog.io/@rokwon_k/Web-CORS
WebConfig
package pack;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override // WebMvcConfigurer의 추상 메소드를 재정의
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**") // 모든 URL에 대해 "/ws"(WebSocket Endpoint) 연결을 허용
.allowedOrigins("*")
.allowedMethods("GET", "POST", "PUT", "DELETE") // 허용 메소드 지정
.allowedHeaders("*"); // 모든 헤더 허용
}
}
WebSocket 실습 2 (멀티 채팅)
dependencies
chat.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
<link rel="stylesheet" href="/css/chat.css">
<!-- SockJS와 STOMP 라이브러리 읽기용 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.5.1/sockjs.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script>
<script type="text/javascript">
let stompClient = null;
function connect(){
// 웹소켓 어플리케이션 서버는 WebScoket의 endpoint를 만들고, 클라이언트는 endpoint의 URI를 통해 서버와 접속
let socket = new SockJS("/ws"); // "/ws" : 암호화되지 않은 웹소켓 연결
stompClient = Stomp.over(socket); // Stomp객체 생성 : 브로커 역할
// connect()는 Sockjs와 stomp client를 이용하여 Spring Boot에서 구성한 / ws 엔드 포인트에 연결합니다.
stompClient.connect({}, function(frame){
// frame 객체는 서버와의 연결 상태, 메시지, 또는 오류 정보를 담고 있는 데이터 구조로 이해할 수 있다.
console.log('connect : ' + frame);
stompClient.subscribe('/topic/public', function(message){
showMessage(JSON.parse(message.body)); // JSON 형태의 문자열을 객체로 바꾸어 주고 showMessgae()에 던져줌
});
});
}
function sendMessage(){ // 서버로 메세지 전송
// 사용자가 입력한 메세지를 "/app/message"로 전송
let nameInput = document.querySelector("#name");
let messageContent = document.querySelector("#message").value;
// 이름 입력란에서 이름을 서버로 전송하고 비활성화
if(!nameInput.disabled){
stompClient.send("/app/chat.addUser",{},
JSON.stringify({sender:nameInput.value, type:'JOIN'}));
nameInput.disabled = true;
}
// 메세지가 있고 stompClient가 연결상태라면 메세지를 서버로 전송
if(messageContent && stompClient) {
let chatMessage = {
sender:nameInput.value,
content:messageContent,
type:'CHAT'
};
stompClient.send("/app/chat.sendMessage",{},JSON.stringify(chatMessage));
document.querySelector("#message").value = "";
}
}
function leaveChat(){
if(stompClient){ // stompClient 연결상태 확인
stompClient.disconnect();
}
document.querySelector("#nameInput").disabled = false; // 채팅 퇴장 후 이름 입력란을 다시 활성화
alert("다신 오지마~");
}
function showMessage(message){
// 수신된 메세지를 페이지에 표시
let messageElement = document.createElement("li");
// 메세지의 타입에 따라 다른 메세지가 화면에 출력
if(message.type == "JOIN"){
messageElement.classList.add('event-message');
message.content = message.sender + "님 입장";
} else if(message.type == "LEAVE") {
messageElement.classList.add('event-message');
message.content = message.sender + "님 퇴장";
} else {
messageElement.classList.add('chat-message');
let usernameElement = document.createElement("strong");
usernameElement.classList.add('nickname');
let usernameText = document.createTextNode(message.sender + " : ");
usernameElement.appendChild(usernameText);
messageElement.appendChild(usernameElement);
}
let textElement = document.createElement("span");
let messageText = document.createTextNode(message.content);
textElement.appendChild(messageText);
messageElement.appendChild(textElement);
document.querySelector("#messageArea").appendChild(messageElement);
// 자동 스크롤 기능 추가
let messageArea = document.querySelector("#messageArea");
messageArea.scrollTop.scrollHeight;
}
window.onload = function(){
connect();
}
window.onbeforeunload = function(){ // 브라우저 닫히기 전 WebSocket 연결 종료
if(stompClient){
stompClient.disconnect();
}
}
</script>
</head>
<body>
<div>
<h2 style="color: #004d40">🏡 채팅</h2>
<ul id="messageArea">
</ul>
<!-- 입력창과 버튼을 감싸는 컨테이너 추가 -->
<div id="inputContainer">
<input type="text" id="name" placeholder="이름 입력"> <input
type="text" id="message" size="100" placeholder="메세지 입력"
onkeydown="if (event.keyCode == 13) sendMessage()">
<button onclick="sendMessage()">전송</button>
<button onclick="leaveChat()">퇴장</button>
</div>
</div>
</body>
</html>
WebSocketConfig
package pack.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 {
// WebSocketMessageBrokerConfigurer : 제공하는 추상메소드를 바디에 내용이 없는 일반메소드로 만들어놓음, 해당 메소드를 커스터마이징해서 써먹음
@Override // WebSocketMessageBroker의 동작을 재정의
public void configureMessageBroker(MessageBrokerRegistry registry) {
// MessageBrokerRegistry : MessageBroker의 대상(profile)을 설정, 외부 메세지 브로커로 라우팅(요청을 핸들러와 연결, 매핑하는 것이 라우팅이다)
// /topic으로 시작하는 메세지는 MessageBroker로 라우팅
registry.enableSimpleBroker("/topic");
// 클라이언트는 접두어 "/app"으로 시작하는 메세지를 서버로 보냄
registry.setApplicationDestinationPrefixes("/app");
}
@Override // Endpoint를 등록
public void registerStompEndpoints(StompEndpointRegistry registry) {
// 클라이언트가 WebSocket을 통해 연결할 수 있는 URL을 정의, SocketJS와 같은 콜백옵션을 설정 가능
registry.addEndpoint("/ws").setAllowedOriginPatterns("*").withSockJS(); // 다른 도메인의 접근도 허용, 이게 기본값임
// 처음만 HTTP로 타겟팅, 이후 프로토콜을 WebSocket으로 바꾼다는 뜻
// "/ws" : 이 경로로 접속해 STOMP 메세지를 주고 받을 수 있다.
}
}
WebConfig
package pack.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override // WebMvcConfigurer의 추상 메소드를 재정의
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**") // 모든 URL에 대해 "/ws"(WebSocket Endpoint) 연결을 허용
.allowedOrigins("*")
.allowedMethods("GET", "POST", "PUT", "DELETE") // 허용 메소드 지정
.allowedHeaders("*"); // 모든 헤더 허용
}
}
ChatMessage
package pack.model;
import lombok.Data;
// 메세지 객체를 정의 : 메세지 타입(입장, 채팅, 퇴장), 발신자, 내용 등의 구조를 정의
@Data
public class ChatMessage {
private String sender; // 메세지 발신자
private String content; // 메세지 본문
private MessageType type; // 메세지 유형
public enum MessageType { // 열거형으로 상수값을 재정의, 가독성 상승
JOIN, // 입장 메세지
CHAT, // 채팅 메세지
LEAVE // 퇴장 메세지
}
}
Controller
package pack.controller;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.stereotype.Controller;
import pack.model.ChatMessage;
@Controller
public class ChatController {
@MessageMapping("/chat.sendMessage")
@SendTo("/topic/public")
public ChatMessage sendMessage(ChatMessage chatMessage) {
// 클라이언트로부터 채팅 메세지를 수신 후 해당 메세지를 반환해 브로드캐스팅함
return chatMessage;
}
@MessageMapping("/chat.addUser")
@SendTo("/topic/public")
public ChatMessage addUser(ChatMessage chatMessage,
SimpMessageHeaderAccessor headerAccessor) {
// WebSocket 세션에 사용자 이름을 저장 - 세션은 클라이언트와 서버 간의 연결을 추적 가능
headerAccessor.getSessionAttributes().put("username", chatMessage.getSender());
return chatMessage; // 새로운 채팅명을 브로드캐스팅
}
}
결과
WebSocket 실습 3 (alert)
dependencies
WebConfig
package pack.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override // WebMvcConfigurer의 추상 메소드를 재정의
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**") // 모든 URL에 대해 "/ws"(WebSocket Endpoint) 연결을 허용
.allowedOrigins("*")
.allowedMethods("GET", "POST", "PUT", "DELETE") // 허용 메소드 지정
.allowedHeaders("*"); // 모든 헤더 허용
}
}
WebSocketConfig
package pack.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 {
// WebSocketMessageBrokerConfigurer : 제공하는 추상메소드를 바디에 내용이 없는 일반메소드로 만들어놓음, 해당 메소드를 커스터마이징해서 써먹음
@Override // WebSocketMessageBroker의 동작을 재정의
public void configureMessageBroker(MessageBrokerRegistry registry) {
// MessageBrokerRegistry : MessageBroker의 대상(profile)을 설정, 외부 메세지 브로커로 라우팅(요청을 핸들러와 연결, 매핑하는 것이 라우팅이다)
// /topic으로 시작하는 메세지는 MessageBroker로 라우팅
registry.enableSimpleBroker("/topic");
// 클라이언트는 접두어 "/app"으로 시작하는 메세지를 서버로 보냄
registry.setApplicationDestinationPrefixes("/app");
}
@Override // Endpoint를 등록
public void registerStompEndpoints(StompEndpointRegistry registry) {
// 클라이언트가 WebSocket을 통해 연결할 수 있는 URL을 정의, SocketJS와 같은 콜백옵션을 설정 가능
registry.addEndpoint("/ws").withSockJS();
// 처음만 HTTP로 타겟팅, 이후 프로토콜을 WebSocket으로 바꾼다는 뜻
// "/ws" : 이 경로로 접속해 STOMP 메세지를 주고 받을 수 있다.
// cors
//registry.addEndpoint("ws").setAllowedOriginPatterns("*"); // 모든 도메인 허용
//registry.addEndpoint("ws").setAllowedOriginPatterns("http://localhost:8080", "http://korea.com"); // 해당 도메인만 접근할 수 있음
}
}
index
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.5.1/sockjs.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script>
<script type="text/javascript">
let stompClient = null;
function connect(){
// 웹소켓 어플리케이션 서버는 WebScoket의 endpoint를 만들고, 클라이언트는 endpoint의 URI를 통해 서버와 접속
let socket = new SockJS("/ws"); // "/ws" : 암호화되지 않은 웹소켓 연결
stompClient = Stomp.over(socket); // Stomp객체 생성 : 브로커 역할
// connect()는 Sockjs와 stomp client를 이용하여 Spring Boot에서 구성한 / ws 엔드 포인트에 연결합니다.
stompClient.connect({}, function(frame){
// frame 객체는 서버와의 연결 상태, 메시지, 또는 오류 정보를 담고 있는 데이터 구조로 이해할 수 있다.
console.log('connect : ' + frame);
stompClient.subscribe('/topic/notifications', function(noti){
// 도착한 메세지(noti)를 JSON으로 파싱
let parseNoti = JSON.parse(noti.body);
showNotiFunc(parseNoti.type, parseNoti.message);
});
});
}
function sendRequest(){
let fromUser = document.getElementById("fromUser").value;
if(fromUser.trim() === ""){
alert("사용자 이름 입력 1");
return;
}
stompClient.send("/app/friend-request",{},fromUser);
}
function sendComment(){
let fromUser = document.getElementById("fromUser").value;
if(fromUser.trim() === ""){
alert("사용자 이름 입력 2");
return;
}
stompClient.send("/app/comment",{},fromUser);
}
function sendLike(){
let fromUser = document.getElementById("fromUser").value;
if(fromUser.trim() === ""){
alert("사용자 이름 입력 3");
return;
}
stompClient.send("/app/like",{},fromUser);
}
function showNotiFunc(type, message){
let notiShow = document.getElementById("notiShow");
let li = document.createElement("li");
if(type === "좋아요") {
li.innerHTML = `${type}:${message} ❤️`;
} else {
li.appendChild(document.createTextNode(`${type}:${message}`));
}
notiShow.appendChild(li);
}
window.onload = connect;
</script>
</head>
<body>
<h2>실시간 소셜 미디어 알림(친구 요청, 댓글, 좋아요)</h2>
<div>
사용자 이름 : <input type="text" id="fromUser" placeholder="이름 입력">
</div>
<br/>
<div>
이벤트 발생<br/>
<button onclick="sendRequest()">친구 요청</button>
<button onclick="sendComment()">댓글 달기</button>
<button onclick="sendLike()">좋아요 클릭</button>
</div>
<br/>
<div>
<b>알림 : </b>
<ul id="notiShow"></ul>
</div>
</body>
</html>
MyNotification
package pack;
public class MyNotification {
private String type;
private String message;
public MyNotification() {
// TODO Auto-generated constructor stub
}
public MyNotification(String type, String message) {
this.type = type;
this.message = message;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
Controller
package pack;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.stereotype.Controller;
@Controller
public class NotiController {
@MessageMapping("/friend-request")
@SendTo("/topic/notifications")
public MyNotification friendRequest(String fromUser) {
if(fromUser == null || fromUser.trim().isEmpty()) {
fromUser = "알 수 없는 사용자";
}
return new MyNotification("친구요청", fromUser + "님이 친구 요청을 보냈습니다.");
}
@MessageMapping("/comment")
@SendTo("/topic/notifications")
public MyNotification comment(String fromUser) {
if(fromUser == null || fromUser.trim().isEmpty()) {
fromUser = "익명 사용자";
}
return new MyNotification("댓글", fromUser + "님이 게시판에 댓글을 달았습니다.");
}
@MessageMapping("/like")
@SendTo("/topic/notifications")
public MyNotification like(String fromUser) {
if(fromUser == null || fromUser.trim().isEmpty()) {
fromUser = "모르는 사용자";
}
return new MyNotification("좋아요", fromUser + "님이 좋아요를 눌렀습니다.");
}
}
결과
에러
🚨 클릭시 undefinded 뜸
컨트롤러를 통해 도착한 메세지를 JSON 형식으로 파싱하지 않아 생긴 에러 : JSON.parse로 파싱 후 에러 해결!
WebSocket 실습 4 (실시간 주가)
dependencies
StockPriceHandler
package pack;
import java.util.Random;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
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 StockPriceHandler extends TextWebSocketHandler {
private final Random random = new Random(); // 주가를 랜덤하게 생성하기 위함
@Override // WebSocket 연결이 되면 자동 호출
// 세션을 초기화하거나 연결이 성립될 때 수행할 작업을 정의
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
// 새로운 스케줄러 생성
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);
// 주기적으로 주가 전송
executorService.scheduleAtFixedRate(() -> {
if(session.isOpen()) {
try {
// 주가 생성
double price = 100 + random.nextDouble() * 10;
session.sendMessage(new TextMessage("현재 가격: " + String.format("%.2f", price)));
} catch (Exception e) {
e.printStackTrace();
}
}
}, 0, 2, TimeUnit.SECONDS); // scheduleAtFixedRate(수행할 작업, 지연시간, 주기, 시간 단위)
session.getAttributes().put("executorService", executorService);
}
@Override // WebSocket 연결이 종료되면 자동 호출
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
// 세션에 저장된 스케즐러를 가져와서 종료
ScheduledExecutorService executorService =
(ScheduledExecutorService)session.getAttributes().get("executorService");
if(executorService != null && executorService.isShutdown()) {
executorService.shutdown();
}
}
}
WebSocketConfig
package pack;
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 // WebSocket 처리를 위한 등록
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
// WebSocket의 엔드포인트와 핸들러를 설정
registry.addHandler(new StockPriceHandler(), "/stock-price").setAllowedOrigins("*"); // .setAllowedOrigins("*") : 실습용이라 그럼 실무에서는 이렇게 하면 안됨~
}
}
index
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<h1>메인</h1>
메뉴1 메뉴2 <a href="show.html">주식자료 보기</a>
</body>
</html>
show
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
<script type="text/javascript">
const socket = new WebSocket("ws://localhost:8080/stock-price");
socket.onopen = function(){
document.getElementById("price").innerText = "연결됨";
}
socket.onmessage = function(event){
document.getElementById("price").innerText = event.data;
}
socket.onclose = function(){
document.getElementById("price").innerText = "연결중지";
}
</script>
</head>
<body>
<h2>에이콘 아카데미 최신 주식 가격</h2>
<div id="price">연결 중 ...</div>
</body>
</html>
리액트 설치
자바스크립트의 개발 속도 브라우저의 버전업 속도보다 빨라 브라우저가 자바스크립트를 해석하지 못함 이러한 문제를 해결하는 것이 Babel
HomePage : https://ko.react.dev/
Babel : https://babeljs.io/docs/en/
legacy: https://ko.reactjs.org/
Repository : github.com/facebook/react
nodejs : 브라우저 없이 해석할 수 있음!
1. node-v22.11.0-x64 설치
2. cmd로 설치 확인! ( npx가 없을 경우 3번으로)
3. npx가 없을 경우에 npm으로 'React 개발환경 설치를 위한 create-react-app' 을 먼저 실행한다.
> npm install -g create-react-app
: 폴더에 상관없이 어디서든 create-react-app 명령을 사용할 수 있도록 함.
> create-react-app -V
: 설치를 확인. V는 대문자.
create-react-app : 의존성을 전부 찾아서 자동으로 다 설치해줌!
4. 현재는 npx를 사용하여 create-react-app을 실행하는 것이 권장되는 방법이다.
npx는 npm 패키지를 실행하는 데 사용되며, 전역 설치 없이 필요한 패키지를 다운로드하여 실행한다.
따라서 npx create-react-app (어플리케이션이름my-app)와 같은 명령을 사용하여 새로운 React 애플리케이션을 생성할 수 있다. 이렇게 하면 전역 설치를 하지 않아도 되며, 항상 최신 버전의 create-react-app을 사용할 수 있다.
c:\work\reactex> npx create-react-app my-app
c:\work\reactex> cd myreact-app
c:\work\reactex\myreact-app> dir
5. 작업 폴더 reactsou 폴더 생성 후 이동 (C:\work\reactsou)
6. npx create-react-app my-app1 입력!
에러나면 커맨드창을 관리자모드로 실행하거나 안되면 파워셀사용!!
스크립트 보안 오류시 해당 블로그 참조하여 에러 해결!
7. 어플리케이션 폴더 생성 확인
만약 폴더를 미리 만들었다면 해당 폴더 경로 뒤에 점하나 찍어줌! ( npx create-react-app my-app1 .)
폴더 안에 뭐 많이 만들어줬다!
8. 어플리케이션으로 이동 후 npm start하면 실행 확인
9. VSCode사용하는게 편함! VSCode 확장프로그램 설치!
10. VSCode가 지원하는 터미널 사용
11. npm start하여 정상적으로 실행되는지 확인!
'Study > Acorn' 카테고리의 다른 글
241115 리액트 (useState / 문제) (0) | 2024.11.15 |
---|---|
241114 리액트 (개요 / 컴포넌트 생성 / 배포 / 데이터 전달 (Props and State) / CSS 및 이미지 적용) (14) | 2024.11.14 |
241112 파일 업로드 (DB) / Security (0) | 2024.11.12 |
241108/241111 파일 업로드/다운로드 (1) | 2024.11.08 |
241107 RESTful / AOP (3) | 2024.11.07 |