Search
🚀

websocket stomp 테스터 만들기 with sock.js

태그
vue.js
sock.js
stomp
분류
Frontend

웹소켓 테스트를 어떻게 할까?

일반적으로 웹소켓 Stomp 를 활용해 코드를 작성 후, 아래 세군데 정도에서 테스트 할 수 있습니다.
하지만 apic 의 경우 아래의 단점이 있었습니다.
크롬이 막혀있다.
mac 에서는 실행이 안된다.
그래서 이전에는 WebSocket Debug Tool 을 사용했습니다. 헤더에 토큰을 넣을 수도 있고, 여러모로 사용이 편했지만 이번에 버전 3 로 넘어가면서 조금 더 나은 테스트 툴이 없을까 하는 생각을 하게 되었습니다.

인증 방식의 변화

Stomp 를 사용하여 실시간 서비스 개발하기 를 보면 알다시피 여타 블로그에 있는 방식과 약간 다릅니다. 대부분 연결할 때, 그리고 발행할 때 전부 헤더에 토큰을 넣고 인증하는 방식을 사용했습니다. 처음에 구현할 때에는 이를 차용하여 같은 로직으로 구현했지만, 토큰의 유출 빈도가 높은 것 같아 웹소켓 연결 처음에만 인증 절차를 거치는 것으로 변경했습니다. 지속적으로 토큰 인증을 하고 싶은 경우, 웹소켓용 토큰을 만들어야 하지 않나 하는 생각이 들긴 합니다.
어 쨌 든 ! 바뀐 인증 방식과 원하는 기능이 있는 웹소켓 테스터를 찾았으나 찾지 못해서 결국 직접 만들었습니다.

WebSocket Tester GitHub

만든 코드를 올린 repository 입니다. 급하게 만드느라 app.vue 에 다 박아 둔게 맘에 걸립니다만,,
websocketTester
sieunnnn
자유롭게 변형 / 공유해도 괜찮습니다. 각자의 프로젝트에 맞게 사용해주세요.
위의 README.md 에서 아래와 같은 내용을 확인할 수 있습니다.
사용 방법을 담은 영상
관련된 글의 링크 리스트
위의 테스터를 사용하면 아래와 같은 장점이 있습니다.
하나의 페이지에서 구독 / 발행 전부 가능하다
따라서 창을 여러 개 띄울 필요가 없습니다.
발행 주소를 마구 바꿀 수 있다
구독 주소로 오는 메세지가 예쁘게 출력 된다

어떻게 구현했을까?

Vue3, TypeScript 와 Sock.js 를 사용하여 구현하였습니다.

Sock.js 연결하기

package.json

아래와 같이 stomp 와 sock.js 연결을 위해 필요한 dependencies 를 넣어주세요.
"dependencies": { "@stomp/stompjs": "^7.0.0", "sockjs-client": "^1.6.1", ... }
JSON
복사

vite.config.ts

그냥 실행 시, global 오류가 발생하므로, 아래와 같이 넣어주세요.
import { defineConfig } from 'vite'; import vue from '@vitejs/plugin-vue'; export default defineConfig({ plugins: [vue()], define: { global: 'globalThis', }, });
TypeScript
복사

Client 연결

App.vue

이곳에서는 입력받은 연결주소를 사용하여 웹소켓 연결을 수행합니다.
<div class="size"> <n-h3 prefix="bar" style="margin: 40px 0 5px 0"> <n-text type="primary">연결 하기</n-text> </n-h3> <div class="connect-container"> <n-input-group-label style="margin-right: 5px">🔑 연결 주소</n-input-group-label> <n-input v-model:value="connectionUrl" size="small" type="text" placeholder="웹소켓 연결을 위한 엔드포인트를 넣어주세요." :disabled="connected"/> <n-button :disabled="connected" n-button type="primary" @click="connect" style="margin-left: 10px"> 연결 하기 </n-button> </div> </div>
HTML
복사
import { defineComponent, ref, computed } from 'vue'; import SockJS from 'sockjs-client'; import { Client, Message } from '@stomp/stompjs'; import VueJsonPretty from 'vue-json-pretty'; import 'vue-json-pretty/lib/styles.css'; export default defineComponent({ components: { VueJsonPretty, }, name: 'App', setup() { const connected = ref(false); const client = ref<Client | null>(null); const subscriptionUrl = ref(''); const publishDestination = ref(''); const messages = ref<string[]>([]); const messageContent = ref(''); const connectionUrl = ref(''); const selectedFiles = ref<File[]>([]); const connect = async () => { if (connected.value) { alert('이미 연결된 상태입니다.'); return; } if (!connectionUrl.value) { alert('웹소켓 연결을 위한 엔드포인트가 필요해요.'); return; } const socketUrl = `${connectionUrl.value}`; try { const response = await fetch(socketUrl); if (!response.ok) { const error = await response.json(); console.log(error.errorCode); return; } } catch (error) { console.error('HTTP 요청에 실패했어요.:', error); return; } client.value = new Client({ webSocketFactory: () => { const sock = new SockJS(socketUrl); sock.onclose = (event) => { console.error('웹소켓 연결이 끊어졌어요.:', event); alert('웹소켓 연결이 끊어졌어요: ' + event.reason); }; return sock; }, debug: (str) => { console.log(str); }, reconnectDelay: 5000, heartbeatIncoming: 4000, heartbeatOutgoing: 4000, }); client.value.onConnect = () => { console.log('웹소켓 연결 완료!'); connected.value = true; }; client.value.onDisconnect = () => { connected.value = false; console.log('웹소켓 연결 해제'); }; client.value.onStompError = (frame) => { console.error('Broker 가 에러를 반환 했어요.: ' + frame.headers['message']); console.error('상세 에러문구: ' + frame.body); }; client.value.activate(); }; ... }
TypeScript
복사

Subscript 하기

이곳에서는 입력받은 구독주소를 사용하여 웹소켓 구독을 수행합니다.
<div class="size"> <n-h3 prefix="bar" style="margin: 20px 0 5px 0"> <n-text type="primary">구독 하기</n-text> </n-h3> <div>구독 주소를 입력해 주세요.</div> <div class="connect-container"> <n-input-group-label style="margin-right: 5px">🔑 구독 주소</n-input-group-label> <n-input v-model:value="subscriptionUrl" size="small" type="text" placeholder="구독 주소를 넣어주세요." /> <n-button type="primary" @click="subscribe" style="margin-left: 10px"> 구독 하기 </n-button> </div> ...
HTML
복사
const subscribe = () => { if (!client.value) { alert('먼저 연결을 해주세요.'); return; } if (!subscriptionUrl.value) { alert('구독 주소를 입력해주세요.'); return; } client.value.subscribe(subscriptionUrl.value, (message: Message) => { messages.value.push(message.body); }); };
TypeScript
복사

Publish 하기

이미지 제외 발행

먼저 발생주소를 받습니다.
그리고 알맞은 DTO를 JSON 형태로 받습니다.
<div> <n-h3 prefix="bar" style="margin: 20px 0 5px 0"> <n-text type="primary">발행 하기</n-text> </n-h3> <div class="connect-container"> <n-input-group-label style="margin-right: 5px">📨 발행 주소</n-input-group-label> <n-input v-model:value="publishDestination" size="small" type="text" placeholder="발행 주소를 넣어주세요."/> </div> ... <div class="connect-container" style="margin-top: 10px"> <n-input-group-label style="margin-right: 5px">📃 DTO(JSON)</n-input-group-label> <n-input v-model:value="messageContent" size="large" type="textarea" placeholder="JSON 형태의 DTO 를 넣어 주세요." style="height: 250px"/> </div> <div class="size" style="display: flex; flex-direction: row; justify-content: flex-end; width: 100%"> <n-button type="primary" @click="publish" style="width: 150px; margin-top: 10px"> 발행 하기 </n-button> </div> </div> ...
HTML
복사
const publish = async () => { if (!client.value || !publishDestination.value || !messageContent.value) { alert('발행 주소와 메시지 내용을 입력해주세요.'); return; } ... const messageObject = JSON.parse(messageContent.value); messageObject.images = encodedFiles.length > 0 ? encodedFiles : null; client.value.publish({ destination: publishDestination.value, body: JSON.stringify(messageObject), headers: { 'Content-Type': 'application/json' }, }); };
TypeScript
복사

이미지를 전송하고 싶다면?

<div class="connect-container" style="margin-top: 10px"> <n-input-group-label style="margin-right: 5px">📷 이미지 업로드</n-input-group-label> <input type="file" @change="handleFileChange" multiple /> </div>
HTML
복사
// 파일의 변화를 감지하는 메서드 추가 const handleFileChange = (event: Event) => { const input = event.target as HTMLInputElement; if (input && input.files) { selectedFiles.value = Array.from(input.files); console.log('선택된 파일:', selectedFiles.value); } else { console.error('파일을 찾을 수 없습ㄴ디ㅏ.'); } }; // 위에 있는 구독 메서드 const publish = async () => { if (!client.value || !publishDestination.value || !messageContent.value) { alert('발행 주소와 메시지 내용을 입력해주세요.'); return; } // 파일 관련 내용을 추가해줍니다. let encodedFiles: { name: string, content: string }[] = []; if (selectedFiles.value.length > 0) { encodedFiles = await Promise.all( selectedFiles.value.map((file) => { return new Promise<{ name: string; content: string }>((resolve, reject) => { const reader = new FileReader(); reader.onload = () => { resolve({ name: file.name, content: reader.result as string }); }; reader.onerror = (error) => { console.error('FileReader 오류:', error); reject(error); }; reader.readAsDataURL(file); }); }) ); }
TypeScript
복사

서버에 발행 메세지 보내기

서버에 값을 보낼때는 JSON.stringify() 를 사용하여 JavaScript 객체를 JSON 형식의 문자열로 변환해야 합니다.
client.value.publish({ destination: publishDestination.value, body: JSON.stringify(messageObject), headers: { 'Content-Type': 'application/json' }, }); };
TypeScript
복사

Print 하기

이곳에서는 요청값을 토대로 서버에서 받은 값을 출력합니다.
<div> <div style="font-size: 18px; font-weight: bold; margin: 30px 0 10px 0">📩 구독 주소로 아래와 같이 메세지가 도착 해요.</div> <div class="json-container"> <div v-for="(message, index) in formattedMessages" :key="index" style="margin: 30px"> <vue-json-pretty theme="light" :data="message" /> </div> </div> </div>
HTML
복사
서버에서 받은 메세지를 JavaScript 에서 사용하려면 JSON.parse() 를 사용하여 JSON 형식의 문자열을 parsing 하여 JavaScript 객체로 변환해야 합니다.
const formattedMessages = computed(() => { return messages.value.map((message) => { try { return JSON.parse(message); } catch (e) { return { error: 'JSON 형태가 아니에요. 다시 한번 확인 해주세요.' }; } }); });
TypeScript
복사
JSON.stringfy() 와 JSON.parse() 에 대해 더 알고 싶다면 아래 블로그를 참고해주세요!

만약 Print 를 이쁘게 하고 싶다면?

link iconnpmnpm: vue-json-pretty 를 사용해보세요!
package.json
"dependencies": { ... "vue-json-pretty": "^2.4.0" },
JSON
복사
App.vue
<div> <div style="font-size: 18px; font-weight: bold; margin: 30px 0 10px 0">📩 구독 주소로 아래와 같이 메세지가 도착 해요.</div> <div class="json-container"> <div v-for="(message, index) in formattedMessages" :key="index" style="margin: 30px"> <vue-json-pretty theme="light" :data="message" /> </div> </div> </div>
HTML
복사
import VueJsonPretty from 'vue-json-pretty'; import 'vue-json-pretty/lib/styles.css'; ...
TypeScript
복사