STOMP은 rabbitMQ, ActiveMQ, Apollo 과 같은 인기 있는 서버에서 구현되어 있る 简単하고 강력한 메시지 보내는 protocol입니다. WebSocket를 통해 STOMP을 사용하면 간단한 protocol을 사용하여 web browser에서 메시지를 보내는 것이 인기 있습니다. AMQP과 같은 protocol은 브라우저가 TCP 연결을 허용하지 않기 때문에 제한이 있습니다.

WebSocket를 통해 STOMP을 사용하려면 @stomp/stompjs를 사용할 수 있지만, 어려운 callback과 복잡한 API가 더 specialized use case에 대응되는 것입니다. 幸运的是, 더 적은 사용자들이 知られている @stompjs/rx-stomp가 있습니다. 이는 RxJS observable를 통해 좋은 interface를 제공합니다. Observable은 Angular에서만 사용되는 것이 아니며, React가 어떻게 작동하는지에 잘 맞는 것입니다. 다양한 메시지 источник로 복잡한 workflow와 pipeline를 구성하는 것은 간단합니다.

튜orial은 Angular의 초기 버전과 somewhat similar path를 따라가ます, 하지만 component structure과 code style은 React의 functional style에 맞춰져 있습니다.

Note: 이 튜orial은 strict TypeScript로 쓰여졌지만, JavaScript code는 5개의 type declaration만 있으므로 거의 동일합니다. JS 버전은 type import과 definition을 건너뛰即可합니다.

목차

목표

여기서는 RxStomp의 다양한 方面을 다양한 컴포넌트에 따라 简単한 채팅방 응용을 만듭니다. 전체적으로는 다음과 같은 목표가 있습니다:

  • React FRONTEND이 RxStomp로 STOMP 서버에 연결되어 있음.

  • STOMP 서버에 대한 我们的 연결 상태 표시.

  • 任意 topic에 대한 Pub/Sub 로직.

  • RxStomp 로직을 다양한 컴포넌트로 분할하여 로직과 责任을 어떻게 분리하는지 보여줍니다.

  • RxStomp 연결/구독 라이프사이cles을 React 컴포넌트 라이프사이cles과 ALIGNING하여 누수나 닫혀지지 않은 watchers가 없도록 Ensure.

Prerequisites

  • React 응용 프로그램이 그 에이전트에 연결할 수 있도록 STOMP 서버가 실행되어 있어야 합니다. 이곳에서는 rabbitmq_web_stomp 확장 기능을 사용하여 RabbitMQ를 사용할 것입니다.

  • React 最新 버전. 이 튜토리얼에서는 v18를 사용하ますが, 老旧 버전도 대부분 동일하게 동작할 것입니다.

  • observable에 대한 一些 친iliarity이 도움이 됩니다.

Starter STOMP Server with RabbitMQ

RabbitMQ도 사용하고자 하시면 (하지 않아도 괜찮습니다), 다양한 운영체제에 대한 설치 안내를 보실 수 있습니다. 확장 기능을 추가하려면 다음과 같이 실행해야 합니다:

$ rabbitmq-plugins enable rabbitmq_web_stomp

Docker를 사용하실 수 있다면, 이 튜토리얼에서 필요한 모든 것을 설정하기 위한 Docker 파일이 이렇게 되어 있을 것입니다:

FROM rabbitmq:3.8.8-alpine

run rabbitmq-plugins enable --offline rabbitmq_web_stomp

EXPOSE 15674

시작 React emplate

이 튜토리얼에서는 Vitereact-ts emplate을 사용할 것입니다. 우리의 응용 프로그램의 중앙 부분은 App 컴포넌트에 있고, 다른 specific STOMP 기능을 위한 子 컴포넌트를 생성할 것입니다.

RxStomp을 설치하는 方法

이 튜토리얼에서는 @stomp/rx-stomp npm 패키지를 사용할 것입니다:

$ npm i @stomp/rx-stomp rxjs

이렇게 하면 2.0.0 버전을 설치합니다.

주의: 이 튜토리얼은 rxjs를 명시적으로 지정하지 않아도 기본적으로 작동합니다. 그러나 그것을 명시적으로 하는 것은 좋은 惯例입니다.

STOMP 서버와 연결 및 해제 관리하는 方法

이제 App.tsx을 엽니다. 우리의 RxStomp клиент을 초기화하겠습니다. 클라이언트는 렌더링이 변할 일이 없으므로 그것을 useRef 훅으로 감쌈 것입니다.

// src/App.tsx
import { useRef } from 'react'
import { RxStomp } from '@stomp/rx-stomp'

import './App.css'

function App() {
  const rxStompRef = useRef(new RxStomp())
  const rxStomp = rxStompRef.current

  return (
    <>
      <h1>Hello RxStomp!</h1>
    </>
  )
}

export default App

기본적인 포트와 인증 세부 정보를 仮定하고, 다음에 우리의 연결에 대한 一些 설정을 정의할 것입니다.

// src/App.tsx

import { RxStomp } from '@stomp/rx-stomp'
import type { RxStompConfig } from '@stomp/rx-stomp'
...
const rxStompConfig: RxStompConfig = {
  brokerURL: 'ws://localhost:15674/ws',
  connectHeaders: {
    login: 'guest',
    passcode: 'guest',
  },
  debug: (msg) => {
    console.log(new Date(), msg)
  },
  heartbeatIncoming: 0,
  heartbeatOutgoing: 20000,
  reconnectDelay: 200,
}

function App() {
  ...

개발자 경험을 더 좋게 만들기 위해 로컬 콘솔에 시간 스탬프가 들어가는 모든 메시지를 로그를 기록했습니다. 또한 쿼터 주기를 낮춤니다. 제 configurations will be quite different for your production application, so check out the RxStompConfig docs for all the options available.

다음으로 우리는 useEffect Hook 안에서 rxStomp에 구성을 전달할 것입니다. 이것은 구성요소 생명 주기와 함께 연결 활성화를 관리합니다.
...
function App() {
  const rxStompRef = useRef(new RxStomp())
  const rxStomp = rxStompRef.current

  useEffect(() => {
    rxStomp.configure(rxStompConfig)
    rxStomp.activate()

    return () => { 
      rxStomp.deactivate() 
    }
  })
  ...

Date ... >>> CONNECT
login:guest
passcode:guest
accept-version:1.2,1.1,1.0
heart-beat:20000,0

Date ... Received data 
Date ... <<< CONNECTED
version:1.2
heart-beat:0,20000
session:session-EJqaGQijDXqlfc0eZomOqQ
server:RabbitMQ/4.0.2
content-length:0

Date ... connected to server RabbitMQ/4.0.2 
Date ... send PING every 20000ms 
Date ... <<< PONG 
Date ... >>> PING

주의: 일반적으로 중복 로그를 볼 수 있다면 비활성화 또는 구독 해제 기능이 올바르게 구현되지 않았을 수 있습니다. React는 React.StrictMode를 통해 이러한 버그를 잡기 위해 개발 환경에서 각 구성요소를 두 번 렌더링합니다.

RxStompState enum은 브로커와의 연결 상태를 나타냅니다. 다음 목표는 UI에 연결 상태를 표시하는 것입니다.

rxStomp.connectionState$ 옵저버블을 사용하여 connectionStatus 문자열에 바인딩할 수 있습니다. useEffect를 사용한 것과 유사하게, 마운트 해제 작업에서 unsubscribe()를 사용할 것입니다.
import { useState } from 'react'

export default function Status() {
  const [connectionStatus, setConnectionStatus] = useState('')

  return (
    <>
      <h2>Connection Status: {connectionStatus}</h2>
    </>
  )
}

이를 보려면 앱에 포함시킵니다.
import { RxStompState } from '@stomp/rx-stomp'
import { useEffect, useState } from 'react'
import type { RxStomp } from '@stomp/rx-stomp'


export default function Status(props: { rxStomp: RxStomp }) {
  const [connectionStatus, setConnectionStatus] = useState('')

  useEffect(() => {
    const statusSubscription = props.rxStomp.connectionState$.subscribe((state) => {
      setConnectionStatus(RxStompState[state])
    })

    return () => {
      statusSubscription.unsubscribe()
    }
  }, [])

  return (
    <>
      <h2>Connection Status: {connectionStatus}</h2>
    </>
  )
}


import Status from './Status'
...
  return (
    <>
      <h1>Hello RxStomp!</h1>

      <Status rxStomp={rxStomp}/>
    </>
  )

이제 화면에 동작하는 시각적 인자가 있어야 합니다. STOMP 서버를 다시 시작하여 로그가 예상대로 동작하는지 시도하십시오.

消息发送方法

BROKER와 간단한 端到端 消息流程을 보여주기 위해 간단한 聊天室을 만듭니다.

이 functionality을 새 Chatroom 요소로 放下할 수 있습니다. まず, 입력에 バインドされた 사용자 이름과 메시지 필드를 갖추는 새 요소를 만들 수 있습니다.

// src/Chatroom.tsx
import { useState } from 'react'
import type { RxStomp } from '@stomp/rx-stomp'

export default function Chatroom(props: {rxStomp: RxStomp}) {
  const [message, setMessage] = useState('')
  const [userName, setUserName] = useState(`user${Math.floor(Math.random() * 1000)}`)

  return (
    <>
      <h2>Chatroom</h2>

      <label htmlFor='username'>Username: </label>
      <input
        type='text'
        name='username'
        value={userName}
        onChange={(e) => setUserName(e.target.value)}
      />

      <label htmlFor='message'>Message: </label>

      <input
        type='text'
        value={message}
        onChange={(e) => setMessage(e.target.value)}
        name='message'
      />
    </>
  )    
}

이를 我们的 App에서 채팅방에 가입하는 탭으로 포함할 것입니다:

// src/App.tsx
import { useEffect, useState, useRef } from 'react'
import Chatroom from './Chatroom'
...
function App() {
  const [joinedChatroom, setJoinedChatroom] = useState(false)
  ...
  return (
    <>
      <h1>Hello RxStomp!</h1>

      <Status rxStomp={rxStomp}/>

      {!joinedChatroom && (
        <button onClick={() => setJoinedChatroom(true)}>
          Join chatroom!
        </button>
      )}

      {joinedChatroom && (
        <>
          <button onClick={() => setJoinedChatroom(false)}>
            Leave chatroom!
          </button>

          <Chatroom rxStomp={rxStomp}/>
        </>
      )}

    </>
  )

실제 消息 보내는 시간이 되었습니다. STOMP은 텍스트 기반의 消息(바이너리 데이터도 가능)를 보내는 것이 가장 좋습니다. 보내고자 하는 이 데이터의 구조를 새 types 파일에 정의할 것입니다:

// types.ts
interface ChatMessage {
  userName: string,
  message: string
}

참고: TypeScript를 사용하지 않는다면 이 type definition을 추가하지 않을 수 있습니다.

다음으로, JSON을 사용하여 消息를 序列화하고 .publish를 사용하여 我们的 STOMP 서버에 주제와 JSON body를 사용하여 消息를 보내는 것을 시도할 것입니다.

// src/Chatroom.tsx
import type { ChatMessage } from './types'
...
const CHATROOM_NAME = '/topic/test'

export default function Chatroom(props: {rxStomp: RxStomp}) {
  ...
  function sendMessage(chatMessage: ChatMessage) {
    const body = JSON.stringify({ ...chatMessage })
    props.rxStomp.publish({ destination: CHATROOM_NAME, body })
    console.log(`Sent ${body}`)
    setMessage('')
  }

  return (
    <>
      <h2>Chatroom</h2>

      <label htmlFor="username">Username: </label>
      <input
        type="text"
        name="username"
        value={userName}
        onChange={(e) => setUserName(e.target.value)}
      />

      <label htmlFor="message">Message: </label>

      <input
        type="text"
        value={message}
        onChange={(e) => setMessage(e.target.value)}
        name="message"
      />

      <button onClick={() => sendMessage({userName, message})}>Send Message</button>
    </>
  )
}

시험하기 위해 Send Message 按钮을 몇 번 클릭하여 序列화가 제대로 동작하는지 확인하십시오. 아직 시각적인 변화를 볼 수 있지 않지만, 콘솔 로그는 그를 보여드릴 것입니다:

Date ... >>> SEND
destination:/topic/test
content-length:45

Sent {"userName":"user722","message":"1234567890"}

消息 수신 方法

모든 사용자의 메시지 목록을 표시하기 위해 새로운 컴포넌트를 만들겠습니다. 지금은 동일한 타입을 사용하고, 토픽 이름을 prop으로 전달하며, 모든 것을 목록으로 표시할 것입니다. 이 모든 것은 MessageList라는 새로운 컴포넌트에 들어갑니다.

// src/MessageDisplay.tsx
import { useEffect, useState } from 'react'
import type { RxStomp } from '@stomp/rx-stomp'
import type { ChatMessage } from './types'

export default function MessageDisplay(props: {rxStomp: RxStomp, topic: string}) {
  const [chatMessages, setChatMessages] = useState<ChatMessage[]>([
    {userName: 'admin', message: `Welcome to ${props.topic} room!`}
  ])

  return(
  <>
  <h2>Chat Messages</h2>
  <ul>
    {chatMessages.map((chatMessage, index) => 
      <li key={index}>
        <strong>{chatMessage.userName}</strong>: {chatMessage.message}
      </li>
    )}
  </ul>
  </>
  )
}

이제 모든 것을 하나로 모을 시간입니다!

Status 컴포넌트에서 구독을 관리하는 것과 유사하게, 마운트 시 구독을 설정하고 언마운트 시 구독을 해제합니다.

RxJS의 pipemap을 사용하여 JSON을 다시 ChatMessage로 역직렬화할 수 있습니다. 이 모듈식 설계를 통해 필요에 따라 RxJS 연산자를 사용하여 더 복잡한 파이프라인을 설정할 수 있습니다.

// src/MessageDisplay.tsx
...
import { map } from 'rxjs'

export default function MessageDisplay(props: {rxStomp: RxStomp, topic: string}) {
  const [chatMessages, setChatMessages] = useState<ChatMessage[]>([
    {userName: 'admin', message: `Welcome to ${props.topic} room!`}
  ])

  useEffect(() => {
    const subscription = props.rxStomp
      .watch(props.topic)
      .pipe(map((message) => JSON.parse(message.body)))
      .subscribe((message) => setChatMessages((chatMessages) => [...chatMessages, message]))

    return () => {
      subscription.unsubscribe()
    }
  }, [])

  ...

이 시점에서 채팅 GUI는 메시지를 올바르게 표시해야 하며, 여러 탭을 다른 사용자로 열어 실험해볼 수 있습니다.

여기서 시도해볼 수 있는 또 다른 것은 STOMP 서버를 끄고, 몇 개의 메시지를 보낸 다음, 다시 켜보는 것입니다. 메시지는 로컬에서 대기열에 들어가고 서버가 준비되면 발송되어야 합니다. 멋지죠!

요약

이 튜토리얼에서 우리는:

  • 좋은 개발 경험을 위해 @stomp/rx-stomp를 설치했습니다.

  • 연결 세부 정보, 디버거 로깅 및 타이머 설정으로 클라이언트를 구성하기 위해 RxStompConfig를 설정했습니다.

  • rxStomp.activaterxStomp.deactivate를 사용하여 클라이언트의 주요 라이프 周期을 관리했습니다.

  • rxStomp.connectionState$ 지시ables를 사용하여 구독 상태를 모니터링했습니다.

  • rxStomp.publish를 사용하여 구성 가능한 목적지와 消息体로 消息을 발행했습니다.

  • rxStomp.watch를 사용하여 주어진 주제에 대한 지시able를 생성했습니다.

  • 콘솔 로그와 React 컴포넌트를 사용하여 라이브러리의 동작을 보고 functionality과 fault tolerance를 확인했습니다.

最终的 코드는 Gitlab에서 찾을 수 있습니다: https://gitlab.com/harsh183/rxstomp-react-tutorial . 이를 시작 템플릿으로 사용하고 발생하는 모든 이슈를 보고하실 수 있습니다.