[React] Socket.io 통신 (feat.MQTT)

문지은·2023년 8월 27일
post-thumbnail

IoT 프로젝트를 진행하며 MQTT 통신을 사용하려고 하였으나, 버전 문제로 어려움을 겪어 socket.io 통신으로 MQTT와 유사한 Publish/Subscribe 메커니즘을 구현하였다.
프로젝트의 주요 기능별로 topic을 정하고 연결된 message를 주고 받음으로써 기능을 구현하였는데, 사용했던 코드를 소개해보고자 한다.

socket.io vs MQTT

먼저 Socket.ioMQTT에 대해 알아보자.
Socket.ioMQTT는 모두 실시간 통신을 위한 프로토콜이지만, 몇 가지 차이점이 있다.

프로토콜 유형

  • Socket.io
    • WebSocket을 기반으로 한 양방향 통신 라이브러리
    • WebSocket은 TCP 연결을 통해 실시간 양방향 통신을 제공
    • Socket.io는 WebSocket을 기본적으로 사용하지만, 필요한 경우 다른 통신 방식도 대체할 수 있음.
  • MQTT
    • 경량 메시지 전달 프로토콜로서, Publish/Subscribe(Pub/Sub) 패턴을 따르며 메시지 기반의 통신 지원
    • MQTT는 TCP 또는 다른 프로토콜 위에서 동작하며, 제한된 대역폭과 불안정한 네트워크 환경에서도 효율적으로 동작

메시지 패턴

  • Socket.io
    • 주로 WebSocket을 사용하여 실시간 양방향 텍스트 또는 이진 데이터를 주고 받음.
    • 주로 채팅 애플리케이션, 실시간 대시보드 등에서 사용
  • MQTT
    • 주로 메시지 기반의 Publish/Subscribe 패턴을 사용하여, 특정 주제에 대한 정보를 주고받음.
    • IoT 애플리케이션, 센서 네트워크 등에서 사용

메시지 형식

  • Socket.io
    • 주로 텍스트 또는 이진 데이터를 주고받을 수 있음.
    • 데이터 형식은 개발자가 직접 정의할 수 있음.
  • MQTT
    • 텍스트 기반의 주제와 메시지로 구성
    • 메시지의 형식은 응용 프로그램에 따라 달라질 수 있음.

프로토콜 특성

  • Socket.io
    • WebSocket을 사용하므로 브라우저와 서버 간의 실시간 양방향 통신이 가능
    • 다양한 브라우저와 플랫폼에서 동작하며, 추가적인 기능과 실패 대비 기능을 제공
  • MQTT
    • 주로 네트워크 장치 및 서버 간의 통신에 사용되며, 메시지 브로커를 통해 중개
    • 효율적인 대량 데이터 전송이 가능하며, 일반적으로 IoT 및 M2M 통신에 적합

socket.io 설치

socket.io를 사용하기 위해서는 서버와 클라이언트 모두 패키지를 설치해야한다. (socket.io 공식문서)
리액트에서는 socket.io-client를 설치한다.

npm install socket.io-client

socket.io로 메시지 주고 받기

socket 통신은 기본적으로 socket.emit('eventName', Func) 으로 데이터를 보내고, socket.on('eventName', Listener)로 데이터를 받는 형식으로, 이러한 구조를 통해 데이터의 교환이 이루어진다.

서버와 소켓 연결 설정하기

  • io 함수 호출하여 클라이언트 측에서 서버와 소켓 연결을 생성
  • useEffect문을 사용하여 초기 렌더링 후에만 실행하고, 컴포넌트가 언마운트될 때 소켓 연결을 해제하도록 구현
const [socket, setSocket] = useState(null); // 소켓 상태 변수와 상태 변경 함수

useEffect(() => {
  // 서버와 소켓 연결 생성
  const newSocket = io('http://localhost:3001'); // 서버 주소로 변경
  setSocket(newSocket); // 생성한 소켓 인스턴스를 상태 변수에 저장

  // 컴포넌트가 언마운트될 때 소켓 연결 해제
  return () => {
    newSocket.disconnect();
  };
}, []); // 초기 렌더링 후에만 실행됨, 의존성 배열이 비어 있음

sendMessage 함수 작성하기

  • 메시지를 입력하고 'Send' 버튼을 누르면 메시지를 전송하도록 작성
  • sendMessage 함수
    • 현재 소켓 연결이 있고, 메시지가 비어있지 않을 경우에만 메시지를 전송
    • socket.emit('sendMessage', message) : 소켓 객체를 통해 sendMessage 이벤트를 메시지와 함께 서버로 보냄
const [message, setMessage] = useState('');

const sendMessage = () => {
    if (socket && message) {
      socket.emit('sendMessage', message); // 서버로 메시지 전송
      setMessage(''); // 메시지 입력 초기화
    }
  };

return (
    <div>
      <h1>Socket.io Example</h1>
      <input
        type="text"
        placeholder="Enter a message"
        value={message}
        onChange={(e) => setMessage(e.target.value)}
      />
      <button onClick={sendMessage}>Send</button>
    </div>
  );

전체 코드

import React, { useState, useEffect } from 'react'; 
import io from 'socket.io-client'; // socket.io-client 모듈 가져오기

const App = () => {
  const [message, setMessage] = useState(''); // 메시지 상태 변수와 상태 변경 함수
  const [socket, setSocket] = useState(null); // 소켓 상태 변수와 상태 변경 함수

  useEffect(() => {
    // 서버와 소켓 연결 생성
    const newSocket = io('http://localhost:3001'); // 서버 주소로 변경
    setSocket(newSocket); // 생성한 소켓 인스턴스를 상태 변수에 저장

    // 컴포넌트가 언마운트될 때 소켓 연결 해제
    return () => {
      newSocket.disconnect();
    };
  }, []); // 초기 렌더링 후에만 실행됨, 의존성 배열이 비어 있음

  // 메시지 전송 함수
  const sendMessage = () => {
    if (socket && message) {
      socket.emit('sendMessage', message); // 서버로 메시지 전송
      setMessage(''); // 메시지 입력 초기화
    }
  };

  // JSX 렌더링
  return (
    <div>
      <h1>Socket.io Example</h1>
      <input
        type="text"
        placeholder="Enter a message"
        value={message}
        onChange={(e) => setMessage(e.target.value)}
      />
      <button onClick={sendMessage}>Send</button>
    </div>
  );
};

export default App;

Publish/Subscribe 메커니즘 구현하기

이제 위 코드를 활용하여 MQTTPublish/Subscribe 메커니즘을 구현해보겠다.

서버와 소켓 연결 설정하기

  • io 함수 호출하여 클라이언트 측에서 서버와 소켓 연결을 생성
    • 모든 도메인에서의 교차 출처 요청 허용하도록 CORS 설정
  • useEffect문을 사용하여 초기 렌더링 후에만 실행하고, 컴포넌트가 언마운트될 때 소켓 연결을 해제하도록 구현
const [socket, setSocket] = useState(null); // 소켓 상태 변수와 상태 변경 함수

useEffect(() => {
  const BrokerAddress = 'http://localhost:3001'; // 브로커 주소

  // 새로운 소켓 인스턴스 생성
  const newSocket = io(BrokerAddress, {
    cors: {origin: '*'} // 모든 도메인에서의 교차 출처 요청 허용
  });

  // 소켓이 브로커에 연결되었을 때 실행되는 이벤트 핸들러
  newSocket.on('connect', () => {
    console.log('Connected to the broker.');
  });

  // 새로운 메시지가 도착했을 때 실행되는 이벤트 핸들러
  newSocket.on('message', (receivedMessage) => {
    console.log(`Received message: ${receivedMessage}`);
  });

  setSocket(newSocket); // 생성한 소켓 인스턴스를 상태 변수에 저장

  // 컴포넌트가 해제될 때 소켓 연결 해제
  return () => {
    newSocket.disconnect();
  };
}, []); // 초기 렌더링 후에만 실행됨, 의존성 배열이 비어 있음

주제 구독 하기 (Subscribe 함수 작성)

  • 특정 주제(topic)를 구독하고 해당 주제에 발행되는 메시지를 수신하도록 작성
    • 클라이언트의 주제 설정에 따라 해당 주제를 MQTT 브로커에게 구독하고자 함을 전달
  • 브로커는 주제 구독 요청을 처리하고, 해당 주제에 새로운 메시지가 발행되면 이를 클라이언트에게 전달.
    • console.log(`Subscribed to topic: ${topic}`); 을 통해 브로커로부터 전달 받은 메시지 출력
const [topic, setTopic] = useState(''); // 주제(topic) 상태 변수와 상태 변경 함수

 // 주제를 구독하는 함수
  const subscribeToTopic = () => {
    if (socket && topic) { // 소켓과 주제가 비어있지 않을 경우
      socket.emit('subscribe', topic); // 소켓을 통해 주제 구독 요청 전송
      console.log(`Subscribed to topic: ${topic}`);
    }
  };

  return (
    <div className="App">
      {/* 주제 입력 필드 */}
      <input
        type="text"
        placeholder="Topic"
        value={topic}
        onChange={(e) => setTopic(e.target.value)}
      />
      {/* 주제 구독 버튼 */}
      <button onClick={subscribeToTopic}>Subscribe</button>
  );

메시지 발행 하기 (Publish 함수 작성)

  • 특정 주제(topic)에 해당하는 메시지를 브로커에 전달하여, 해당 주제를 구독 중인 모든 클라이언트에게 메시지를 전송하도록 작성
    • 클라이언트의 주제와 메시지 설정에 따라 해당 주제에 해당하는 메시지를 브로커에게 발행하도록 요청
  • 브로커는 메시지 발행 요청을 받아 해당 주제를 구독 중인 모든 클라이언트에게 메시지를 전달
const [topic, setTopic] = useState(''); // 주제(topic) 상태 변수와 상태 변경 함수
const [message, setMessage] = useState(''); // 메시지(message) 상태 변수와 상태 변경 함수

// 메시지를 발행하는 함수
const publishMessage = () => {
  if (socket && topic && message) { // 소켓, 주제, 메시지가 비어있지 않을 경우
    socket.emit('publish', { topic, message }); // 주제와 메시지를 함께 서버로 전송
    console.log(`Published message "${message}" to topic: ${topic}`);
    setMessage(''); // 메시지 입력 상태 초기화
  }
};

return (
      {/* 메시지 입력 필드 */}
      <input
        type="text"
        placeholder="Message"
        value={message}
        onChange={(e) => setMessage(e.target.value)}
      />
      {/* 메시지 발행 버튼 */}
      <button onClick={publishMessage}>Publish</button>
    </div>
  );

전체 코드

import { useState, useEffect } from 'react'; 
import io from 'socket.io-client'; // socket.io-client 모듈 가져오기

const BrokerAddress = 'http://localhost:3001'; // 브로커 주소

function App() {
  const [topic, setTopic] = useState(''); // 주제(topic) 상태 변수와 상태 변경 함수
  const [message, setMessage] = useState(''); // 메시지(message) 상태 변수와 상태 변경 함수
  const [socket, setSocket] = useState(null); // 소켓 상태 변수와 상태 변경 함수

  useEffect(() => {
    // 새로운 소켓 인스턴스 생성
    const newSocket = io(BrokerAddress, {
      cors: {origin: '*'} // CORS 설정
    });

    // 소켓이 브로커에 연결되었을 때 실행되는 이벤트 핸들러
    newSocket.on('connect', () => {
      console.log('Connected to the broker.');
    });

    // 새로운 메시지가 도착했을 때 실행되는 이벤트 핸들러
    newSocket.on('message', (receivedMessage) => {
      console.log(`Received message: ${receivedMessage}`);
    });

    setSocket(newSocket); // 생성한 소켓 인스턴스를 상태 변수에 저장

    // 컴포넌트가 해제될 때 소켓 연결 해제
    return () => {
      newSocket.disconnect();
    };
  }, []); // 초기 렌더링 후에만 실행됨, 의존성 배열이 비어 있음

  // 주제를 구독하는 함수
  const subscribeToTopic = () => {
    if (socket && topic) { // 소켓과 주제가 비어있지 않을 경우
      socket.emit('subscribe', topic); // 소켓을 통해 주제 구독 요청 전송
      console.log(`Subscribed to topic: ${topic}`);
    }
  };

  // 메시지를 발행하는 함수
  const publishMessage = () => {
    if (socket && topic && message) { // 소켓, 주제, 메시지가 비어있지 않을 경우
      socket.emit('publish', { topic, message }); // 주제와 메시지를 함께 서버로 전송
      console.log(`Published message "${message}" to topic: ${topic}`);
      setMessage(''); // 메시지 입력 상태 초기화
    }
  };

  return (
    <div className="App">
      {/* 주제 입력 필드 */}
      <input
        type="text"
        placeholder="Topic"
        value={topic}
        onChange={(e) => setTopic(e.target.value)}
      />
      {/* 주제 구독 버튼 */}
      <button onClick={subscribeToTopic}>Subscribe</button>
      
      {/* 메시지 입력 필드 */}
      <input
        type="text"
        placeholder="Message"
        value={message}
        onChange={(e) => setMessage(e.target.value)}
      />
      {/* 메시지 발행 버튼 */}
      <button onClick={publishMessage}>Publish</button>
    </div>
  );
}

export default App;
profile
코드로 꿈을 펼치는 개발자의 이야기, 노력과 열정이 가득한 곳 🌈

0개의 댓글