WebSocket - 실시간채팅 구현 미니 프로젝트

이동명·2023년 7월 17일
1

# 1. WebSocket 이란 ?


WebSocket 이란 ?

  • 기존의 단방향 HTTP 프로토콜과 호환되어 양방향 통신을 제공하기 위해 개발된 프로토콜.

  • 일반 Socket통신과 달리 HTTP 80 Port를 사용하므로 방화벽에 제약이 없으며 통상 WebSocket으로 불린다.

  • 접속까지는 HTTP 프로토콜을 이용하고, 그 이후 통신은 자체적인 WebSocket 프로토콜로 통신하게 된다.

    소켓은 HTTP(Hyper Text Transfer Protocol)를 사용하는 네트워크 데이터 통신의 단점을 보완하는데 그 목적이 있다. HTTP를 다뤄본 적이 있다면, HTTP는 HTML이라는 문서를 운반하기 위한 프로토콜로 다음과 같이 동작한다.

모든 HTTP를 사용한 통신은 클라이언트가 먼저 요청을 보내고, 그 요청에 따라 웹 서버가 응답하는 형태이며 웹 서버는 응답을 보낸 후 웹 브라우저와의 연결을 끊는다. 양쪽이 데이터를 동시에 보내는 것이 아니기 때문에 이러한 통신 방식을 반이중 통신(Half Duplex)라고 한다.

사실 HTTP만으로도 원하는 정보를 송수신할 수 있었지만, 인간의 욕심은 끝이 없기에 인터넷이 발전함에 따라 원하는 것이 더욱 다양해졌다. 예를 들어 클라이언트가 먼저 요청하지 않아도 서버가 먼저 데이터를 보내거나, 표준 TCP/IP 통신을 사용해 특정 서버와 통신을 하는 등 원하는 것이 늘어가자 그것을 이루고자 많은 플러그인 및 웹 기술이 개발되었다.

WebSocket이 존재하기 전에는 ?

* polling

클라이언트가 평범한 HTTP Request를 서버로 계속 요청해 이벤트 내용을 전달받는 방식. 가장 쉬운 방법이지만 클라이언트가 지속적으로 Request를 요청하기 때문에 클라이언트의 수가 많아지면 서버의 부담이 급증한다. HTTP Request Connection을 맺고 끊는 것 자체가 부담이 많은 방식이고, 클라이언트에서 실시간 정도의 빠른 응답을 기대하기 어렵다.

* Long polling

클라이언트에서 서버로 일단 HTTP Request를 요청한다. 이 상태로 계속 기다리다가 서버에서 해당 클라이언트로 전달할 이벤트가 있다면 그 순간 Response 메세지를 전달하며 연결이 종료된다. 곧이어 클라이언트가 다시 HTTP Request를 요청해 서버의 다음 이벤트를 기다리는 방식. polling보다 서버의 부담이 줄겠으나, 클라이언트로 보내는 이벤트들의 시간간격이 좁다면 polling과 별 차이 없게 되며, 다수의 클라이언트에게 동시에 이벤트가 발생될 경우 서버의 부담이 급증한다.

* Streaming

Long Polling과 마찬가지로 클라이언트 -> 서버로 HTTP Request를 요청한다. 서버 -> 클라이언트로 이벤트를 전달할 때 해당 요청을 해제하지 않고 필요한 메세지만 보내기(Flush)를 반복하는 방식. Long Polling과 비교하여 서버에 메세지를 보내지 않고도 다시 HTTP Request 연결을 하지 않아도 되어 부담이 경감된다고 한다.


WebSocket

이처럼 HTTP 통신의 특징인 (연결 -> 연결 해제) 때문에 효율이 많이 떨어지게 되고, 웹 브라우저 말고 외부 플러그인이 항상 필요하게 되었다. 그래서 이런 상황을 극복하고자 2014년 HTML5에 웹 소켓을 포함하게 되었다. 웹소켓은 클라이언트가 접속 요청을 하고 웹 서버가 응답한 후 연결을 끊는 것이 아닌 Connection을 그대로 유지하고 클라이언트의 요청 없이도 데이터를 전송할 수 있는 프로토콜이다. 프로토콜의 요청은 [ws://~]로 시작한다.

웹소켓은 HTTP환경에서 전이중 통신(Full Duplex, 2-way communication)을 지원하기 위한 프로토콜이며 RFC6455에 정의되어 있다. HTTP 프로토콜에서 HandShaking을 완료한 후, HTTP로 동작하지만, HTTP와는 다른 방식으로 통신을 한다.

WebSocket이 기존의 TCP Socket과 다른 점은 최초 접속이 일반 HTTP Request를 통해 HandShaking 과정을 통해 이뤄진다는 점이다.

HTTP Request를 그대로 사용하기 때문에 기존의 80, 443 포트로 접속을 하므로 추가 방화벽을 열지 않고도 양방향 통신이 가능하고, HTTP 규격인 CORS 적용이나 인증 등 과정을 기존과 동일하게 가져갈 수 있는 것이 장점이다.

웹소켓은 서비스를 동적으로 만들어 주지만, Ajax, Streaming, Long polling 기술이 더 효과적일 경우도 있다. 예를 들어, 변경 사항의 빈도가 자주 일어나지 않고, 데이터의 크기가 작은 경우 Ajax, Streaming, Long polling 기술이 더 효과적일 수 있다. 즉, 실시간성을 보장해야 하고, 변경 사항의 빈도가 잦다면, 또는 짧은 대기 시간, 고주파수, 대용량의 조합인 경우 WebSocket이 좋은 해결책이 될 수 있다.

뉴스나 메일, SNS 피드는 동적으로 업데이트 하는 것은 맞지만 몇 분마다 업데이트 하는 것이 좋다. 반면 협업, 게임, 금융 앱은 훨씬 더 실시간에 근접해야한다.

# 2. 접속 과정

웹소켓을 이용하여 서버와 클라이언트가 통신을 하려면 먼저 웹소켓 접속 과정을 거쳐야 한다. 웹소켓 접속 과정은 TCP/IP 접속 그리고 웹소켓 열기 HandShake 과정으로 나눌 수 있다. 웹소켓도 TCP/IP위에서 동작하므로, 서버와 클라이언트는 웹소켓을 사용하기 전에 서로 TCP/IP 접속이 되어있어야 한다. TCP/IP 접속이 완료된 후 서버와 클라이언트는 웹소켓 열기 HandShake 과정을 시작한다.

WebSocket 구현 테스트

1. 라이브러리 추가

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-websocket'
    compileOnly 'org.projectlombok:lombok'
    developmentOnly 'org.springframework.boot:spring-boot-devtools'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

의존성은 위와 같은 항목만 추가해주었다.

2. WebSocket Handler

소켓통신은 서버와 클라이언트가 1:N의 관계를 맺는다. 즉, 하나의 서버에 다수 클라이언트가 접속할 수 있다. 따라서 서버는 다수의 클라이언트가 보낸 메세지를 처리할 핸들러가 필요하다. 텍스트 기반의 채팅을 구현해볼 것 이므로 'TextWebSocketHandler'를 상속받아서 작성한다. Client로부터 받은 메세지를 Log출력하고 클라이언트에게 환영하는 메세지를 보내는 역할을 한다.

@Component
@Slf4j
public class ChatHandler extends TextWebSocketHandler {

    private static List<WebSocketSession> list = new ArrayList<>();

    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        String payload = message.getPayload();
        log.info("payload : " + payload);

        for(WebSocketSession sess: list) {
            sess.sendMessage(message);
        }
    }

    /* Client가 접속 시 호출되는 메서드 */
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {

        list.add(session);

        log.info(session + " 클라이언트 접속");
    }

    /* Client가 접속 해제 시 호출되는 메서드드 */

    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {

        log.info(session + " 클라이언트 접속 해제");
        list.remove(session);
    }
}

payload ?

페이로드란 전송되는 데이터를 의미한다.. 데이터를 전송할 때, Header와 META 데이터, 에러 체크 비트 등과 같은 다양한 요소들을 함께 보내 데이터 전송 효율과 안정성을 높히게 된다. 이때, 보내고자 하는 데이터 자체를 의미하는 것이 페이로드이다. 예를 들어 택배 배송을 보내고 받을 때 택배 물건이 페이로드고 송장이나 박스 등은 부가적은 것이기 때문에 페이로드가 아니다.

다음 JSON에서 페이로드는 "data"이다. 나머지는 통신을 하는데 있어 용이하게 해주는 부가적 정보들이다.

{
"status":
"from":"localhost",
"to":"http://melonicedlatte.com/chatroom/1",
"method":"GET",
"data":{"message":"There is a cutty dog!"}
}

3. WebSocket Config

핸들러를 이용해 WebSocket을 활성화하기 위한 Config를 작성할 것이다.

@EnableWebSocket 어노테이션을 사용해 WebSocket을 활성화 하도록 한다.

WebSocket에 접속하기 위한 Endpoint는 /chat으로 설정하고,

도메인이 다른 서버에서도 접속 가능하도록 CORS : setAllowedOrigins("*"); 를 추가해준다. 이제 클라이언트가 ws://localhost:8080/chat으로 커넥션을 연결하고 메세지 통신을 할 수 있는 준비를 마쳤다.

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

    private final ChatHandler chatHandler;

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
    
        registry.addHandler(chatHandler, "ws/chat").setAllowedOrigins("*");
    }
}

CORS

교차 출처 리소스 공유(Cross-Origin Resource Sharing, CORS)는 추가 HTTP 헤더를 사용해 한 출처에서 실행 중인 웹 어플리케이션이 다른 출처의 선택한 자원에 접근할 수 있는 권한을 부여하도록 브라우저에 알려주는 체제이다. 웹 어플리케이션은 리소스가 자신의 출처( domain, protocol, port )와 다를 때 교차 출처 HTTP 요청을 실행한다. 이에 대한 응답으로 서버는 Access-Control-Allow-Origin 헤더를 다시 보낸다.

4. ChatController

@Controller
@Slf4j
public class ChatController {
    
    @GetMapping("/chat")
    public String chatGET(){

        log.info("@ChatController, chat GET()");
        
        return "chat";
    }
}

5. chat.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <script src="https://code.jquery.com/jquery-3.6.0.min.js" charset="UTF-8"></script>
</head>

<body>
<div class="container">
    <div class="col-6">
        <label><b>채팅방</b></label>
    </div>
    <div>
        <div id="msgArea" class="col"></div>
        <div class="col-6">
            <div class="input-group mb-3">
                <input type="text" id="msg" class="form-control" aria-label="Recipient's username" aria-describedby="button-addon2">
                <div class="input-group-append">
                    <button class="btn btn-outline-secondary" type="button" id="button-send">전송</button>
                </div>
            </div>
        </div>
    </div>
</div>

<script>
  $(document).ready(function () {

    const getRandomUsername = () => {
      const adjectives = ["Happy", "Brave", "Clever", "Energetic", "Friendly"];
      const nouns = ["Cat", "Dog", "Tiger", "Lion", "Elephant"];

      const randomAdjectiveIndex = Math.floor(Math.random() * adjectives.length);
      const randomNounIndex = Math.floor(Math.random() * nouns.length);

      const randomAdjective = adjectives[randomAdjectiveIndex];
      const randomNoun = nouns[randomNounIndex];

      return randomAdjective + randomNoun;
    };

    const username = getRandomUsername();

    console.log(username)

    $("#button-send").on("click", function () {
      send();
    });

    const websocket = new WebSocket("ws://localhost:8080/ws/chat");

    websocket.onmessage = onMessage;
    websocket.onopen = onOpen;
    websocket.onclose = onClose;

    function send() {
      let msg = document.getElementById("msg");
      console.log(username + ":" + msg.value);
      websocket.send(username + ":" + msg.value);
      msg.value = "";
    }

    function onClose() {
      var str = username + ": 님이 방을 나가셨습니다.";
      websocket.send(str);
    }

    function onOpen() {
      var str = username + ": 님이 입장하셨습니다.";
      websocket.send(str);
    }

    function onMessage(msg) {
      var data = msg.data;
      var sessionId = null;
      var message = null;
      var arr = data.split(":");

      for (var i = 0; i < arr.length; i++) {
        console.log("arr[" + i + "]: " + arr[i]);
      }

      var cur_session = username;
      console.log("cur_session : " + cur_session);
      sessionId = arr[0];
      message = arr[1];

      console.log("sessionID : " + sessionId);
      console.log("cur_session : " + cur_session);

      if (sessionId == cur_session) {
        var str = "<div class='col-6'>";
        str += "<div class='alert alert-secondary'>";
        str += "<b>" + sessionId + " : " + message + "</b>";
        str += "</div></div>";
        $("#msgArea").append(str);
      } else {
        var str = "<div class='col-6'>";
        str += "<div class='alert alert-warning'>";
        str += "<b>" + sessionId + " : " + message + "</b>";
        str += "</div></div>";
        $("#msgArea").append(str);
      }
    }
  });
</script>
</body>

</html>

여기 까지는 Spring WebSocket만을 통해서 간단한 채팅기능만 구현하였다. 하지만 현재 채팅방도 하나이고, WebSocket을 지원하지 않는 브라우저 에선 서비스가 불가능하기 때문에 조금 더 고도화가 필요하다.


Stomp

  • WebSocket 프로토콜은 두 가지 유형의 메세지를 정의하고 있지만 그 메세지의 내용까지는 정의하고 있지 않는다.

  • STOMP (Simple Text Oriented Messaging Protocol)은 메세징 전송을 효율적으로 하기 위해 탄생한 프로토콜이고, 기본적으로 pub / sub 구조로 되어있어 메세지를 전송하고 메세지를 받아 처리하는 부분이 확실히 정해져 있기 때문에 개발자 입장에서 명확하게 인지하고 개발할 수 있는 이점이 있다. 한 줄로 정의하자면, STOMP 프로토콜은 WebSocket 위에서 동작하는 프로토콜로써 클라이언트와 서버가 전송할 메세지의 유형, 형식, 내용들을 정의하는 매커니즘이다.

  • 또한 STOMP를 이용하면 메세지의 헤더에 값을 줄 수 있어 헤더 값을 기반으로 통신 시 인증 처리를 구현하는 것도 가능하며 STOMP 스펙에 정의한 규칙만 잘 지키면 여러 언어 및 플랫폼 간 메세지를 상호 운영할 수 있다.

그래서 STOMP 란 ?

STOMP는 TCP 또는 WebSocket 같은 양방향 네트워크 프로토콜 기반으로 동작한다.

이름에서 알 수 있듯, STOMP는 Text 지향 프로토콜이나, Message Payload에는 Text or Binary 데이터를 포함 할 수 있다.

위에서 언급한 pub / sub란 메세지를 공급하는 주체와 소비하는 주체를 분리해 제공하는 메세징 방법이다. 기본적인 컨셉을 예로 들자면 우체통(Topic)이 있다면 집배원(Publisher)이 신문을 우체통에 배달하는 행위가 있고, 우체통에 신문이 배달되는 것을 기다렸다가 빼서 보는 구독자(Subscriber)의 행위가 있다. 이때 구독자는 다수가 될 수 있다. pub / sub 컨셉을 채팅방에 빗대면 다음과 같다.

  • 채팅방 생성 : pub / sub 구현을 위한 Topic이 생성됨

  • 채팅방 입장 : Topic 구독

  • 채팅방에서 메세지를 송수신 : 해당 Topic으로 메세지를 송신(pub), 메세지를 수신(sub)

클라이언트는 메세지를 전송하기 위해 SEND, SUBSCRIBE COMMAND를 사용할 수 있다. 또한, SEND, SUBSCRIBE COMMAND 요청 Frame에는 메세지가 무엇이고, 누가 받아서 처리할지에 대한 Header 정보가 포함되어 있다.

이런 명령어들은 "destination" 헤더를 요구하는데 이것이 어디에 전송할지, 혹은 어디에서 메세지를 구독할 것 인지를 나타낸다.

위와 같은 과정을 통해 STOMP는 Publish-Subscribe 매커니즘을 제공한다. 즉 Broker를 통해 타 사용자들에게 메세지를 보내거나 서버가 특정 작업을 수행하도록 메세지를 보낼 수 있게 된다.

만약 Spring에서 지원하는 STOMP를 사용하면 Spring WebSocket 어플리케이션은 STOMP Broker로 동작하게 된다.

Spring에서 지원하는 STOMP는 많은 기능을 하는데 예를 들어 Simple In-Memory Broker를 이용해 SUBSCRIBE 중인 다른 클라이언트들에게 메세지를 보내준다. Simple In Memory Broker는 클라이언트의 SUBSCRIBE 정보를 자체적으로 메모리에 유지한다.

또한 RabbitMQ, ActiveMQ같은 외부 메세징 시스템을 STOMP Broker로 사용할 수 있도록 지원한다.

구조적인 면을 보자면, 스프링은 메세지를 외부 Broker에게 전달하고, Broker는 WebSocket으로 연결된 클라이언트에게 메세지를 전달하는 구조가 되겠다. 이와 같은 구조 덕분에 HTTP 기반의 보안 설정과 공통된 검증 등을 적용할 수 있게 된다.


STOMP 설정으로 채팅방 구현

ChatConfig

@Configuration
@EnableWebSocketMessageBroker
public class ChatConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/ws/chat").setAllowedOriginPatterns("*").withSockJS();
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {

        registry.enableSimpleBroker("/queue", "/topic");

        registry.setApplicationDestinationPrefixes("/app");
    }
}

ChatRoomController

@Controller
@RequiredArgsConstructor
@RequestMapping("/chat")
public class ChatRoomController {
    private final ChatService chatService;

    // 채팅 리스트 화면
    @GetMapping("/room")
    public String rooms(Model model) {

        return "/chat/room";
    }

    // 모든 채팅방 목록 반환
    @GetMapping("/rooms")
    @ResponseBody
    public List<ChatRoom> room() {
        return chatService.findAllRoom();
    }

    // 채팅방 생성
    @PostMapping("/room")
    @ResponseBody
    public ChatRoom createRoom(@RequestParam String name) {
        return chatService.createRoom(name);
    }
    // 채팅방 입장 화면

    @GetMapping("/room/enter/{roomId}")
    public String roomDetail(Model model, @PathVariable String roomId) {
        model.addAttribute("roomId", roomId);
        return "/chat/roomdetail";
    }
    // 특정 채팅방 조회
    @GetMapping("/room/{roomId}")
    @ResponseBody
    public ChatRoom roomInfo(@PathVariable String roomId) {
        return chatService.findById(roomId);
    }
}

MessageController

@RestController
@RequiredArgsConstructor
public class MessageController {

    private final SimpMessageSendingOperations sendingOperations;

    @MessageMapping("/chat/message")
    public void enter(ChatMessage message) {
        if (ChatMessage.MessageType.ENTER.equals(message.getType())) {
            message.setMessage(message.getSender()+"님이 입장하였습니다.");
        }
        sendingOperations.convertAndSend("/topic/chat/room/"+message.getRoomId(),message);
    }
}

ChatMessage

@Data
@NoArgsConstructor
@AllArgsConstructor
public class ChatMessage {
    public enum MessageType {
        ENTER, TALK
    }

    private MessageType type;
    //채팅방 ID
    private String roomId;
    //보내는 사람
    private String sender;
    //내용
    private String message;
}

ChatRoom

@Data
@NoArgsConstructor
public class ChatRoom {

    private String roomId;
    private String roomName;


    public static ChatRoom create(String name) {
        ChatRoom room = new ChatRoom();
        room.roomId = UUID.randomUUID().toString();
        room.roomName = name;
        return room;
    }
}

ChatService

@Service
@Slf4j
@RequiredArgsConstructor
public class ChatService {

    private Map<String, ChatRoom> chatRooms;

    @PostConstruct
    //의존관게 주입완료되면 실행되는 코드
    private void init() {
        chatRooms = new LinkedHashMap<>();
    }

    //채팅방 불러오기
    public List<ChatRoom> findAllRoom() {
        //채팅방 최근 생성 순으로 반환
        List<ChatRoom> result = new ArrayList<>(chatRooms.values());
        Collections.reverse(result);

        return result;
    }

    //채팅방 하나 불러오기
    public ChatRoom findById(String roomId) {
        return chatRooms.get(roomId);
    }

    //채팅방 생성
    public ChatRoom createRoom(String name) {
        ChatRoom chatRoom = ChatRoom.create(name);
        chatRooms.put(chatRoom.getRoomId(), chatRoom);
        return chatRoom;
    }
}

application.properties

spring.devtools.livereload.enabled=true
spring.devtools.restart.enabled=false
spring.freemarker.cache=false
spring.jackson.serialization.fail-on-empty-beans=false
spring.thymeleaf.prefix=classpath:/templates/

room.html

<!doctype html>
<html lang="en" xmlns:v-on="http://www.w3.org/1999/xhtml" xmlns:v-bind="http://www.w3.org/1999/xhtml">
<head>
    <title>Websocket Chat</title>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
    <!-- CSS -->
    <link rel="stylesheet" href="/webjars/bootstrap/4.3.1/dist/css/bootstrap.min.css">
    <style>
        [v-cloak] {
            display: none;
        }
    </style>
</head>
<body>
<div class="container" id="app" v-cloak>
    <div class="row">
        <div class="col-md-12">
            <h3>채팅방 리스트</h3>
        </div>
    </div>
    <div class="input-group">
        <div class="input-group-prepend">
            <label class="input-group-text">방제목</label>
        </div>
        <input type="text" class="form-control" v-model="room_name" v-on:keyup.enter="createRoom">
        <div class="input-group-append">
            <button class="btn btn-primary" type="button" @click="createRoom">채팅방 개설</button>
        </div>
    </div>
    <ul class="list-group">
        <li class="list-group-item list-group-item-action" v-for="item in chatrooms" v-bind:key="item.roomId" v-on:click="enterRoom(item.roomId)">
            {{item.roomName}}
        </li>
    </ul>
</div>
<!-- JavaScript -->
<script src="/webjars/vue/2.5.16/dist/vue.min.js"></script>
<script src="/webjars/axios/0.17.1/dist/axios.min.js"></script>
<script>
  var vm = new Vue({
    el: '#app',
    data: {
      room_name : '',
      chatrooms: [
      ]
    },
    created() {
      this.findAllRoom();
    },
    methods: {
      findAllRoom: function() {
        axios.get('/chat/rooms').then(response => { this.chatrooms = response.data; });
      },
      createRoom: function() {
        if("" === this.room_name) {
          alert("방 제목을 입력해 주십시요.");
          return;
        } else {
          var params = new URLSearchParams();
          params.append("name",this.room_name);
          axios.post('/chat/room', params)
            .then(
              response => {
                alert(response.data.roomName+"방 개설에 성공하였습니다.")
                this.room_name = '';
                this.findAllRoom();
              }
            )
            .catch( response => { alert("채팅방 개설에 실패하였습니다."); } );
        }
      },
      enterRoom: function(roomId) {
        var sender = prompt('대화명을 입력해 주세요.');
        if(sender !== "") {
          localStorage.setItem('wschat.sender',sender);
          localStorage.setItem('wschat.roomId',roomId);
          location.href="/chat/room/enter/"+roomId;
        }
      }
    }
  });
</script>
</body>
</html>

roomdetail.html

<!doctype html>
<html lang="en" xmlns:v-on="http://www.w3.org/1999/xhtml">
<head>
  <title>Websocket ChatRoom</title>
  <!-- Required meta tags -->
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">

  <!-- Bootstrap CSS -->
  <link rel="stylesheet" href="/webjars/bootstrap/4.3.1/dist/css/bootstrap.min.css">
  <style>
    [v-cloak] {
      display: none;
    }
  </style>
</head>
<body>
<div class="container" id="app" v-cloak>
  <div>
    <h2>{{room.name}}</h2>
  </div>
  <div class="input-group">
    <div class="input-group-prepend">
      <label class="input-group-text">내용</label>
    </div>
    <input type="text" class="form-control" v-model="message" v-on:keypress.enter="sendMessage">
    <div class="input-group-append">
      <button class="btn btn-primary" type="button" @click="sendMessage">보내기</button>
    </div>
  </div>
  <ul class="list-group">
    <li class="list-group-item" v-for="message in messages">
      {{message.sender}} - {{message.message}}
    </li>
  </ul>
  <div></div>
</div>
<!-- JavaScript -->
<script src="/webjars/vue/2.5.16/dist/vue.min.js"></script>
<script src="/webjars/axios/0.17.1/dist/axios.min.js"></script>
<script src="/webjars/sockjs-client/1.1.2/sockjs.min.js"></script>
<script src="/webjars/stomp-websocket/2.3.3-1/stomp.min.js"></script>
<script>
  //alert(document.title);
  // websocket & stomp initialize
  var sock = new SockJS("/ws/chat");
  var ws = Stomp.over(sock);
  var reconnect = 0;
  // vue.js
  var vm = new Vue({
    el: '#app',
    data: {
      roomId: '',
      room: {},
      sender: '',
      message: '',
      messages: []
    },
    created() {
      this.roomId = localStorage.getItem('wschat.roomId');
      this.sender = localStorage.getItem('wschat.sender');
      this.findRoom();
    },
    methods: {
      findRoom: function() {
        axios.get('/chat/room/'+this.roomId).then(response => { this.room = response.data; });
      },
      sendMessage: function() {
        ws.send("/app/chat/message", {}, JSON.stringify({type:'TALK', roomId:this.roomId, sender:this.sender, message:this.message}));
        this.message = '';
      },
      recvMessage: function(recv) {
        this.messages.unshift({"type":recv.type,"sender":recv.type=='ENTER'?'[알림]':recv.sender,"message":recv.message})
      }
    }
  });

  function connect() {
    // pub/sub event
    ws.connect({}, function(frame) {
      ws.subscribe("/topic/chat/room/"+vm.$data.roomId, function(message) {
        var recv = JSON.parse(message.body);
        vm.recvMessage(recv);
      });
      ws.send("/app/chat/message", {}, JSON.stringify({type:'ENTER', roomId:vm.$data.roomId, sender:vm.$data.sender}));
    }, function(error) {
      if(reconnect++ <= 5) {
        setTimeout(function() {
          console.log("connection reconnect");
          sock = new SockJS("/ws/chat");
          ws = Stomp.over(sock);
          connect();
        },10*1000);
      }
    });
  }
  connect();
</script>
</body>
</html>
profile
Web Developer

0개의 댓글