-
WebSoket(stompJS + React)프론트엔드 2023. 1. 18. 00:54
웹소켓이란?
클라이언트에서 Request를 날리면 서버에서 Response를 한다.
즉, 클라이언트와 서버의 관계는 stateless하다.
하지만, 웹소켓은 statefull protocol로 요청을 매번 보낼 필요 없이 connection을 유지해서 양방향 통신을 가능하도록 만든 기술이다. 즉, connection을 하고 데이터를 주고받다가 connection을 끊기만 하면 된다.
웹소켓으로 최초접속을 할 경우, 클라이언트에서 랜덤하게 생성한 키값을 서버에 전송하고, 서버는 이 키값을 바탕으로 토큰을 생성하여 클라이언트에 Response를 보내어 클라이언트와 서버간의 handshacking을 해야한다.
웹소켓 프로토콜?
- soket.io
인터넷 익스플로어 구버전의 사용자는 웹소켓으로 작성된 웹페이지를 볼 수 없었기에 이를 해결하기 위해 나왔다.
soket.io는 웹페이지가 열리는 브라우저가 웹소켓을 지원하면 일반 웹소켓방식으로 동작하며, 지원하지 않는다면 http를 이용해 웹소켓을 흉내내는 방식으로 통신을 지원한다. soket.io는 Node.js에 종속적 이다.
=> 인프런에서 해당강의를 들었으나 현재 참가하는 프로젝트에서는 spring을 사용하기에 stomp를 사용할 예정이다.
- soket.js
스프링에서 위와 같은 브라우저 문제를 해결하기 위한 방법으로 soketJS를 솔루션으로 제공한다. 서버 개발시 일반 websoket으로 통신할지 SoketJS 호환으로 통신할지 결정할 수 있다. 그리고 클라이언트는 SoketJS client를 통해 서버랑 통신한다.
- stomp
tomp는 단순 (또는 스트리밍) 텍스트 지향 메시징 프로토콜입니다. spring에 종속적이며, 구독방식으로 사용하고 있습니다. 가벼워서 보통 많이들 사용합니다.보통 Node.js 를 이용할땐 soket.io 를 주로 사용하고,
Spring을 사용할땐 soket.js, stomp 를 주로 사용합니다.
Stomp?
1) 서버와 연결할 클라이언트 connection
2) 메세지 전송 전 subscriber와 publisher를 지정
3) subscribe를 하면 해당 URL로 나에게 메시지를 보낼 수 있는 경로가 생긴다.
4) publisher를 하면 publish한 URL로 메시지가 이동한다.
id: JW
topic/publish/MG
MG한테 메세지 보내줘! 안녕?
컨트롤러
/subscibe/MG
MG야, JW가 안녕?이라고 보냈어.id: MG
cf. topic이 방 넘버라 생각하면 된다.
– app: WebSocket으로의 앱으로 접속을 위한 포인트가 되며 메시지를 실제로 보낼 때 사용된다
– topic: 일 대 다수의 커넥션에서 메시지를 전송한다
– queue: 일 대 일의 커넥션에서 메시지를 전송한다
– user: 메시지를 보내기 위한 사용자를 특정한다
우선 사전에 서버 개발자 분께 소켓 연결 endpoint를 받았다.
웹소켓 클라이언트 구축 전 soketjs-client와 stompjs를 install해준다.
해당 프로젝트에서 타입스크립트로 코드를 짜주었기에, .d.ts.file을 추가로 install해주었다.
npm i soketjs-client, @stomp/stompjs --save npm i @types/stompjs, @types/soketjs-client --save
import는 다음과 같이 했다.
import SockJS from 'sockjs-client'; import StompJs from '@stomp/stompjs';
프록시 설정은 다음과 같이 했다.
before:
devServer: { historyApiFallback: true, port: 3090, devMiddleware: { publicPath: '/dist/' }, static: { directory: path.resolve(__dirname) }, proxy: { '/api/': { target: 'http://localhost:3095', changeOrigin: true, ws: true, }, }, },
after:
const { createProxyMiddleware } = require("http-proxy-middleware"); module.exports = (app) => { app.use( "/api", createProxyMiddleware({ target: "http://localhost:8080", changeOrigin: true, }) ); app.use( "/ws-stomp", createProxyMiddleware({ target: "http://localhost:8080", ws: true }) ); };
App.js
import React, { useEffect, useRef, useState } from "react"; import * as StompJs from "@stomp/stompjs"; import * as SockJS from "sockjs-client"; const ROOM_SEQ = 1; const App = () => { const client = useRef({}); const [chatMessages, setChatMessages] = useState([]); const [message, setMessage] = useState(""); useEffect(() => { connect(); return () => disconnect(); }, []); //sock = new SockJS("/ws/chat"); //ws = Stomp.over(sock); //connet(); const connect = () => { client.current = new StompJs.Client({ // brokerURL: "ws://localhost:8080/ws-stomp/websocket", // 웹소켓 서버로 직접 접속 webSocketFactory: () => new SockJS("/ws-stomp"), // proxy를 통한 접속 connectHeaders: { "auth-token": "spring-chat-auth-token", }, debug: function (str) { console.log(str); }, reconnectDelay: 5000, heartbeatIncoming: 4000, heartbeatOutgoing: 4000, onConnect: () => { subscribe(); }, onStompError: (frame) => { console.error(frame); }, }); client.current.activate(); }; const disconnect = () => { client.current.deactivate(); }; const subscribe = () => { client.current.subscribe(`/sub/chat/${ROOM_SEQ}`, ({ body }) => { setChatMessages((_chatMessages) => [..._chatMessages, JSON.parse(body)]); }); }; const publish = (message) => { if (!client.current.connected) { return; } client.current.publish({ destination: "/pub/message", body: JSON.stringify({ roomSeq: ROOM_SEQ, message }), }); setMessage(""); }; return ( <div> {chatMessages && chatMessages.length > 0 && ( <ul> {chatMessages.map((_chatMessage, index) => ( <li key={index}>{_chatMessage.message}</li> ))} </ul> )} <div> <input type={"text"} placeholder={"message"} value={message} onChange={(e) => setMessage(e.target.value)} onKeyPress={(e) => e.which === 13 && publish(message)} /> <button onClick={() => publish(message)}>send</button> </div> </div> ); }; export default App;
/topic/channel/{chatrooId} : 단체톡방 중 id가 chatroomId인 톡방의 메시지를 받음
/topic/direct/{chatrooId} : 디엠톡방 중 id가 chatroomId인 톡방의 메시지를 받음
/chatroom/{chatroomId}/message : 특정 채팅룸 과거 메세지를 전체 반환 => 이부분은 n개씩 쪼개서 받는게 더 좋을 듯하다.
개념 참고) https://stomp-js.github.io/guide/stompjs/rx-stomp/using-stomp-with-sockjs.html
'프론트엔드' 카테고리의 다른 글
컴포넌트 성능 최적화(함수형 업데이트, 리덕스) (0) 2022.08.18 케이커 웹/앱 - 5 (제안서) (0) 2022.08.15 케이커 웹/앱 - 4 (파일 업로드, 드래그 앤 드롭) (0) 2022.08.14 케이커 웹/앱 - 3 (사이드바) (0) 2022.08.14 케이커 웹/앱 - 2 (마이페이지) (1) 2022.08.14