Chat App 과 같이 서로 통신하는 Web App을 만든다면 어떻게 해야할지 궁금하던 차에 여러 블로그를 통해 아래의 구조가 가능하다는 것을 알게 되었다. 그래서 간단한 Chat App을 만들어 보기로 했다.
front 는 React (+bootstrap), backend는 Gin으로, 그리고 front와 backend 는 websocket 통신을 하는 web app을 만들고, Gin server가 EMQX Broker를 통해 각자의 Topic 을 보면서 app A/B 와 통신하는 구조다. 사실 굳이 이렇게 websocket 과 emqx 를 둘다 사용해서 할 필요는 없는데, 잘 모르는 기술들을 다 한번씩 써보고 싶어서 ^^;; 이런 구조를 잡았다.
환경 설정
EMQX
Emqx Broker 를 사용하기 위해 local 에 Emqx Broker 를 설치 www.emqx.io/downloads#broker (mac)
> brew tap emqx/emqx
> brew install emqx
> emqx start
Source Tree 생성
Front 만들기 (React)
> mkdir chat
> npx create-react-app front
기본적인 React 앱이 생성된다. 브라우저에서 http://localhost:3000 오픈
> npm run start
참고: React 개발 툴을 chrome web store 에서 찾아서 설치하면 유용하다.
Backend 서버 만들기 (Gin + emqx)
> cd chat
> mkdir backend
> go mod init backend
Backend에서 front를 띄우도록 변경해야하니 아래와 같이 main.go 코드를 작성한다.
package main
import (
"github.com/gin-gonic/contrib/static"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
router.Use(static.Serve("/", static.LocalFile("../front/build", true)))
router.Run(":3030")
}
그리고 shell 파일 만들어서 한번에 실행하게 하면, http://localhost:3030 에서 오픈
#!/bin/bash
path=$( cd "$(dirname "$0")" ; pwd )
cd $path/front
npm install
npm run build
cd $path/backend
go run main.go
이제 기본 Chat 코드 준비가 완료 되었으니 다음을 진행하자
- front
- Chatting 메세지를 입력하고 보여주는 화면을 만들기
- websocket 으로 backend 와 연결해서 메세지 주고 받기
- backend
- websocket 으로 front 와 연결해서 메세지 주고 받기
- emqx library 로 emqx broker 와 연결해서 topic 을 통해 다른 앱과 통신하기
Step 1. Websocket 을 통한 backend 와 front 통신
Front 구현
UI 가 항상 많은 시간을 필요로 한다. 그러기에 최소한 채팅 개념만 보여주는 UI를 만들자.
이왕이면 저번에 공부한 bootstrap 도 사용해보고~
React 를 대강 이라도 이해하려면 reactjs.org/tutorial/tutorial.html 를 한번씩 따라해보면 된다.
불필요한 코드를 제거하고
websocket.js
- 자신의 서버 /ws 로 websocket url 을 설정
- websocket 으로 들어오는 메세지를 UI에 전달하기 위해 addListner 추가
- websocket 으로 메세지를 보내기 위해 sendMessage 추가
class websocketConn {
constructor() {
this.websocket = new WebSocket('ws://127.0.0.1:3030/ws')
this.connect()
}
connect() {
this.websocket.onopen = function () {
console.log('Connected');
};
this.websocket.onclose = function () {
console.log('Closed');
};
this.websocket.onmessage = function (message) {
let data = JSON.parse(message.data)
console.log('Message Received: ', data)
}
}
addListener(listner) {
console.log('addListener')
this.websocket.addEventListener("message", (message) => {
let data = JSON.parse(message.data)
listner(data)
})
}
sendMessage(message) {
console.log('sendMessage')
this.websocket.send(message)
}
}
export default websocketConn;
index.js
- websocket 초기화 작업 추가
import React from 'react';
import ReactDOM from 'react-dom';
import '../node_modules/bootstrap/dist/css/bootstrap.css'
import './index.css';
import App from './App';
import websocketConn from './websocket'
console.log('Create websocket connection ...')
const websocket = new websocketConn();
console.log('Create websocket connection Success!')
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
export default websocket;
App.js
- React Component 는 간단하게 아래 세가지로 구성
- Name : 채팅할 내 이름 UI
- Chat : 채팅 메세지 보여주는 UI
- Input : 나의 채팅 메세지 입력 UI
- websocket 사용하기 위해 import
- websocket 의 event 받기 위해 componentDidMount() 에서 websocket.addListener 호출
- message list를 업데이트해서 변경된 리스트를 Chat 에 전달
- message 받기: messageListner
- message list 에 들어온 메세지 추가
- message 보내기: handleMessage
- message list 에 보내는 메세지 추가
- 참고: form 을 사용해서 input을 받는데 이때 화면 전체가 refresh 되는 이슈가 있다. 그걸 방지하기 위해서 보통 onSubmit(e) 에서 e.preventDefault()를 추가한다.
import './App.css';
import React from 'react';
import Chat from './Chat';
import websocket from './index'
let nameInput = React.createRef();
let messageInput = React.createRef();
class Input extends React.Component {
...
}
class Name extends React.Component {
...
}
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
visible: false,
messageList: [],
}
}
componentDidMount() {
setTimeout(() => {
websocket.addListener(this.messageListener)
}, 250)
}
addMessage = (message) => {
let messageList = this.state.messageList.slice()
messageList.push(message)
this.setState({
messageList: messageList
})
}
messageListener = (message) => {
this.addMessage(message)
}
handleMessage = (e) => {
e.preventDefault();
let messageList = this.state.messageList.slice()
let message = {
payload: messageInput.current.value,
sender: nameInput.current.value
}
messageList.push(message)
this.setState({
messageList: messageList
})
websocket.sendMessage(JSON.stringify(message))
}
handleStart = (e) => {
e.preventDefault();
this.setState({
name: nameInput.current.value,
visible: true
})
}
render() {
return (
<div className="container-fluid" >
<div className="container-fluid text-white" style={{ backgroundColor: 'rgb(43, 38, 44)' }}>
<h1 style={{ color: '#fffdc2' }}> Let's Chat</h1>
<Name onSubmit={this.handleStart} />
{this.state.visible ? < Chat visible={this.state.visible} messageList={this.state.messageList} myName={nameInput.current.value} /> : null}
{this.state.visible ? <Input visible={this.state.visible} onSubmit={this.handleMessage} /> : null}
</div>
</div>
);
}
}
export default App;
chat.js
- Chat component는 아래 두 가지 component로 되어 있다.
- Receiver : message list의 message sender가 내 이름인 경우
- Sender: message list 의 message sender가 내 이름이 아닌 경우
- 참고: 자동 스크롤을 하기 위해서 보이지 않는 div 를 추가해서 scrollIntoView 함수로 구현한다.
import React from 'react';
class Receiver extends React.Component {
...
}
class Sender extends React.Component {
...
}
class Chat extends React.Component {
scrollToBottom = () => {
this.messagesEnd.scrollIntoView({ behavior: "smooth" });
}
componentDidMount() {
this.scrollToBottom();
}
componentDidUpdate() {
this.scrollToBottom();
}
render() {
let messageList = this.props.messageList
let myName = this.props.myName
const chatList = []
if (this.props.visible) {
var key
for (key in messageList) {
let message = messageList[key]
if (message.sender == myName) {
chatList.push(<Receiver name={message.sender} message={message.payload} />)
} else {
chatList.push(<Sender name={message.sender} message={message.payload} />)
}
}
}
return (
<div id="chat" className="p-3 container-fluid bg-white" style={{ height: '500px', overflow: 'auto', visibility: this.props.visible }}>
{chatList}
<div style={{ float: "left", clear: "both" }}
ref={(el) => { this.messagesEnd = el; }}>
</div>
</div>
);
}
}
export default Chat;
Server 구현
main.go
- melody 라이브러리를 이용해서 websocket 을 구현한다.
- 자신이 받은 메세지를 그대로 m.Broadcast(msg) 로 다시 전달 ( ^^; 일단 동작확인을 위해..)
package main
import (
...
"gopkg.in/olahol/melody.v1"
)
func main() {
...
m := melody.New()
router.GET("/ws", func(c *gin.Context) {
m.HandleRequest(c.Writer, c.Request)
})
m.HandleMessage(func(s *melody.Session, msg []byte) {
m.Broadcast(msg)
})
...
}
동작 확인
아래와 같이 자기 자신이 websocket 으로 보낸 메세지를 다시 전달받아서 중복해서 표시되는것을 확인할 수 있다.
Step 2. Emqx 를 통한 앱 간 통신
두 사용자 "kiki"와 "jiji" 에게 Topic 과 port 할당해서 앱을 실행해서 채팅을 해보자
name | port | topic |
kiki | 3030 | topic/client_kiki |
jiji | 3000 | topic/client_jiji |
Server 구현
iot.go
go 예제 www.emqx.io/blog/how-to-use-mqtt-in-golang 를 참고하여 작성
- 내 local 의 emqx broker 를 이용하기 위해 "127.0.0.1:1883" 에 연결
- 내 subscribe topic 으로 들어온 메세지를 websocket 으로 전달
- websocket 으로 들어온 내 메세지를 상대방 topic으로 publish
package iot
import (
"fmt"
"time"
mqtt "github.com/eclipse/paho.mqtt.golang"
"gopkg.in/olahol/melody.v1"
)
var connectHandler mqtt.OnConnectHandler = func(client mqtt.Client) {
fmt.Println("Connected")
}
var connectLostHandler mqtt.ConnectionLostHandler = func(client mqtt.Client, err error) {
fmt.Printf("Connect lost: %v", err)
}
type IotClient struct {
mqttClient mqtt.Client
publishTopic string
subscribeTopic string
websocket *melody.Melody
}
func IoTClient(clientId string, targetId string, websocket *melody.Melody) (*IotClient, error) {
var broker = "127.0.0.1"
var port = 1883
opts := mqtt.NewClientOptions()
opts.AddBroker(fmt.Sprintf("mqtt://%s:%d", broker, port))
opts.SetClientID(clientId)
opts.SetConnectTimeout(30 * time.Second)
opts.OnConnect = connectHandler
opts.OnConnectionLost = connectLostHandler
client := mqtt.NewClient(opts)
if token := client.Connect(); token.Wait() && token.Error() != nil {
panic(token.Error())
}
iotClient := &IotClient{
mqttClient: client,
publishTopic: "topic/" + targetId,
subscribeTopic: "topic/" + clientId,
websocket: websocket,
}
return iotClient, nil
}
func (i *IotClient) ListenMessage() {
var messageSubHandler mqtt.MessageHandler = func(client mqtt.Client, msg mqtt.Message) {
i.websocket.Broadcast(msg.Payload())
}
if token := i.mqttClient.Subscribe(i.subscribeTopic, 0, messageSubHandler); token.Wait() && token.Error() != nil {
panic(token.Error())
}
}
func (i *IotClient) SendMessage(payload []byte) error {
if token := i.mqttClient.Publish(i.publishTopic, 0, false, payload); token.Wait() && token.Error() != nil {
return token.Error()
}
return nil
}
main.go
- mqtt 통신을 위한 IoTClient 생성시 아래 정보 전달
- 내 topic : client_kiki
- 상대방 topic : client_jiji
- websocket
- websocket 으로 front 에 입력한 메세지가 들어오면 상대방 topic 으로 SendMessage()
package main
import (
"backend/iot"
...
)
func main() {
...
iotClient, _ := iot.IoTClient("client_kiki", "client_jiji", websocket)
iotClient.ListenMessage()
...
websocket.HandleMessage(func(s *melody.Session, msg []byte) {
iotClient.SendMessage(msg)
})
...
}
MQTTX 로 동작 테스트
MQTTX
mqtt 를 테스트하기 위한 편리한 앱 mqttx.app/
설치하고 emqx broker 에 사용자 jiji 로 client 생성하여 연결
MQTTX 는 "jii" 앱처럼 동작하도록 설정한다.
- 자신의 topic "topic/client_jiji" 을 subscribe 하여 "kiki" 가 보낸 메세지를 확인한다.
- 자신이 보내는 메세지가 kiki의 topic "topic/client_kiki" 로 잘 보내졌는지 확인한다.
chat App 에서는 주고 받는 메세지가 정상적으로 찍히는것을 알 수 있다~ 동작확인 끝~!!
Step3. 최종 테스트
두 개의 별도 앱을 각각의 port 와 clientID, topic으로 설정해서 실행한다~
Summary
- Chat 용 Web app을 만들기 위해 front 는 bootstrap + React 로 backend 는 gin 으로 구현했다. front가 event를 받기 위해서는 websocket 이 필요해서 front 와 backend 는 websocket 으로 통신하도록 구현해서 동작을 확인해 보았다.
- 두개의 Chat Web app 이 서로 통신하기 위해서 emqx broker 를 이용했고 각자의 topic 을 만들어 해당 topic 을 통해 서로 메세지를 주고 받도록 구현했다. 이때 emqx 연결은 심플하게 mqtts (tls) 가 아닌 mqtt (tcp) 로 했다.
- MQTTX 라는 훌륭한 mqtt client 가 있어 내가 구현한 emqx 코드 동작 확인을 빠르게 할수 있었다. 앞으로 mqtt 관련 디버깅에 매우 유용할것 같다.
- tls 로 emqx broker 를 사용해보는것은 다음에 한번 정리해야겠다.
'자습' 카테고리의 다른 글
Zipkin Go (0) | 2021.07.17 |
---|---|
Localstack 살펴보기 (0) | 2021.07.16 |
Go OAuth2.0 서버 (0) | 2021.01.31 |
node-forge를 이용한 RSA 암호화, CSR 생성 (0) | 2021.01.16 |
Electron + Bootstrap 초보의 간단 앱 만들기 (0) | 2021.01.09 |