반응형
Heli, 헬리
시행착오를 줄이는 방법 - 진태양
Heli, 헬리
  • 분류 전체보기 (82)
    • General (28)
      • Essay (22)
      • Craftsmanship (2)
      • IT Meet & Hack (4)
    • Finance (1)
      • Portfolio (1)
      • Activity (0)
    • Infrastructure (1)
      • Kubernetes (0)
      • AWS (1)
    • Development (45)
      • News (4)
      • Architecture (4)
      • Web (1)
      • Spring Framework (7)
      • JVM (12)
      • MongoDB (0)
      • Git (2)
      • Algorithm (14)
      • Python (1)
    • Computer Science (1)
      • Network (1)
    • Civic Hacking (3)
      • Code for Korea (3)
    • Know-how (2)
      • IT Service (1)
      • Career (1)
    • English (1)
      • Translation (1)

인기 글

  • 서버 개발자, 커뮤니티 빌더의 이야기가 궁금하신분!
    2023.03.28
    서버 개발자, 커뮤니티 빌더의 이야기가 궁금하신분!
  • Why DDD, Clean Architecture and ⋯
    2022.03.10
    Why DDD, Clean Architecture and ⋯
  • [번역] 개발자가 잠자는 동안 돈을버는 5가지 방법 | 사⋯
    2022.04.17
    [번역] 개발자가 잠자는 동안 돈을버는 5가지 방법 | 사⋯
  • M1 칩에서 pyqt5 설치하기 - qmake 패스 설정
    2022.07.30
  • [Java & Kotlin] enum class가 완벽한 ⋯
    2021.12.13
    [Java & Kotlin] enum class가 완벽한 ⋯

블로그 메뉴

  • 홈
  • 관리
  • 방명록
hELLO · Designed By 정상우.
Heli, 헬리

시행착오를 줄이는 방법 - 진태양

SpringBoot에서 STOMP로 채팅 애플리케이션 만들기 (3)
Development/Spring Framework

SpringBoot에서 STOMP로 채팅 애플리케이션 만들기 (3)

2021. 12. 14. 15:56
반응형

목차

  • SpringBoot에서 STOMP로 채팅 애플리케이션 만들기 (1)
  • SpringBoot에서 STOMP로 채팅 애플리케이션 만들기 (2)
  • SpringBoot에서 STOMP로 채팅 애플리케이션 만들기 (3) - 현재 게시글

 

이제 만들어둔 서버와 통신한 클라이언트를 만들어볼 차례다.

개략적으론 SocketJS와 STOMP Client를 이용해 통신하고, vue/bootstrap/freemarker로 화면을 그릴 예정이다.

1. 의존성 추가

dependencies {
  implementation("org.webjars.bower:bootstrap:4.3.1")
  implementation("org.webjars.bower:vue:2.5.16")
  implementation("org.webjars.bower:axios:0.17.1")
  implementation("org.webjars:sockjs-client:1.1.2")
  implementation("org.webjars:stomp-websocket:2.3.3-1")
}

2. freemarker 옵션 설정

// application.yml
spring:
  freemarker:
    cache: false
    template-loader-path: classpath:/templates
    suffix: .ftlh

3. template 파일 작성

더보기
// room.ftlh
<!doctype html>
<html lang="en">
<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" @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.id"
            v-on:click="enterRoom(item.id)">
            {{item.id}}
        </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 src="/webjars/bootstrap/4.3.1/dist/js/bootstrap.min.js"></script>
<script src="/webjars/sockjs-client/1.1.2/sockjs.min.js"></script>
<script>
    var vm = new Vue({
        el: '#app',
        data: {
            room_name: '',
            chatrooms: []
        },
        created() {
            this.findAllRoom();
        },
        methods: {
            findAllRoom: function () {
                axios.get('/api/v1/chat/room').then(response => {
                    console.log(response.data)
                    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('/api/v1/chat/room', params)
                        .then(
                            response => {
                                alert(response.data.id + "방 개설에 성공하였습니다.")
                                this.room_name = '';
                                this.findAllRoom();
                            }
                        )
                        .catch(response => {
                            alert("채팅방 개설에 실패하였습니다.");
                        });
                }
            },
            enterRoom: function (roomId) {
                var sender = prompt('대화명을 입력해 주세요.');
                localStorage.setItem('wschat.sender', sender);
                localStorage.setItem('wschat.roomId', roomId);
                location.href = "/chat/room/" + roomId;
            }
        }
    });
</script>
</body>
</html>
더보기
// roomdetail.ftlh
<!doctype html>
<html lang="en">
<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.id}}</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}} - {{new Date(message.createdAt)}}</a>
        </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('/api/v1/chat/room/' + this.roomId).then(response => {
                    this.room = response.data;
                });
            },
            sendMessage: function () {
                ws.send("/pub/chat/room/" + this.roomId, {}, JSON.stringify({
                    type: 'COMMENT',
                    sender: this.sender,
                    message: this.message
                }));
                this.message = '';
            },
            recvMessage: function (recv) {
                if (recv.type === 'COMMENT') {
                    this.messages.unshift({
                        "type": recv.type,
                        "sender": recv.sender,
                        "message": recv.message,
                        "createdAt": recv.createdAt
                    })
                }
            }
        }
    });

    function connect() {
        // pub/sub event
        ws.connect({}, function (frame) {
            ws.subscribe("/sub/chat/room/" + vm.$data.roomId, function (message) {
                var recv = JSON.parse(message.body);
                vm.recvMessage(recv);
            });
            ws.send("/pub/chat/room/" + vm.$data.roomId, {}, JSON.stringify({
                type: 'ENTER',
                sender: vm.$data.sender,
                message: ''
            }));
        }, 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>
  • room.ftlh: 최초 접속 시 채팅방을 생성하거나 기존에 생성된 채팅방에 접속할 수 있는 화면
  • roomdetail.ftlh: 채팅방에 접속했을 때 보이는 화면, 채팅을 보내거나 기록을 볼 수 있음

4. Template과 Controller 연결

// ChatRoomController.kt
@Controller
class ChatRoomController {

    @GetMapping("/chat/room")
    fun room(model: Model): String {
        return "/chat/room"
    }

    @GetMapping("/chat/room/{roomId}")
    fun roomEnter(
        model: Model,
        @PathVariable roomId: String?
    ): String {
        model.addAttribute("roomId", roomId)
        return "/chat/roomdetail"
    }
}

여기까지 진행하면 채팅방 리스트를 볼 수 있으며, 실제 채팅을 주고받을 수 있게 된다.

현재는 채팅 메시지 타입이 'COMMENT'인 경우만 화면에 그려주고 있는데, 'ENTER'일 때도 그려주는 등 다양한 활용이 가능하다. 

전체 코드가 담긴 GitHub Repository

https://github.com/960813/spring-boot-stomp

 

GitHub - 960813/spring-boot-stomp

Contribute to 960813/spring-boot-stomp development by creating an account on GitHub.

github.com

 

반응형
저작자표시 비영리 변경금지

    ☕️ Networking

    기술 직군의 기술적인 교류, 커리어 이야기, 직군 무관 네트워킹 모두 환영합니다!

    위클리 아카데미 오픈 채팅방(비밀번호: 9323)

    kakaotalk: https://open.kakao.com/o/gyvuT5Yd

    'Development/Spring Framework' 카테고리의 다른 글
    • [토비의 스프링 3.1] 2장 테스트
    • [토비의 스프링 3.1] 1장 오브젝트와 의존관계
    • SpringBoot에서 STOMP로 채팅 애플리케이션 만들기 (2)
    • SpringBoot에서 STOMP로 채팅 애플리케이션 만들기 (1)
    FreeMarker, Kotlin, springboot, SSR, Stomp, VUE, 뷰 스프링, 소켓 채팅, 소켓 통신, 코프링
    Heli, 헬리
    Heli, 헬리
    Java/Kotlin, Spring 백엔드 관련 기술을 익히고 공유합니다.
    [토비의 스프링 3.1] 1장 오브젝트와 의존관계
    다음 글
    [토비의 스프링 3.1] 1장 오브젝트와 의존관계
    SpringBoot에서 STOMP로 채팅 애플리케이션 만들기 (2)
    이전 글
    SpringBoot에서 STOMP로 채팅 애플리케이션 만들기 (2)

    티스토리툴바