본문 바로가기

AIoT

[Inside AIoT] 기계 데이터 모니터링의 핵심, 실시간 웹알림 SSE로 구현하기

안녕하세요!
모든 기계를 스마트하게 만들고 있는 중인 엣지크로스입니다.

저희는 더 많은 분들에게 실제 현장에서 쓰이는 AI와 IoT 기술 에 대해 쉽고 친숙하게 알려드리기 위해 Inside AIoT 를 연재하고 있습니다.
현장에서 스마트머신 솔루션을 도입하시면서 가장 크게 만족하는 기능 중 하나가 바로, 이상 상황이 발생했을 때 빠르게 실시간으로 알림을 드리는 것인데요. 이번 글을 읽어보시면서, 엣지크로스가 어떻게 기계 원격제어·모니터링 솔루션인 머신매니저(PROGIX, SCAUTR)에서 실시간 알림 기능을 구현하고 있는지 확인해보세요.

 


Written by. 김진성 주임 (엣지크로스 백엔드 개발)

안녕하세요, 저는 엣지크로스에서 웹 애플리케이션의 서버 개발 및 유지보수를 담당하고 있는 백엔드 엔지니어 김진성입니다. 오늘 저는 IoT 플랫폼의 핵심 기능 중 하나인 실시간 웹 브라우저 알림을 어떻게 구현하였는지 소개해 드리려고 합니다.

실시간 모니터링이 IoT 시스템에서 가지는 중요도는 매우 큽니다. 사람이 지속적으로 모니터링을 수행하는 것은 한계가 있기 때문에, 엣지크로스의 머신매니저 솔루션을 이용하시면 웹 또는 앱을 통해 실시간 알림을 전송받을 수 있는데요. 사용자는 자신의 기계 상태와 현장 상황에 맞는대로 알림을 개별 설정할 수 있으며, 웹/앱을 통해 중요한 상태 데이터의 알림을 받을 수 있습니다. 이러한 알림은 앱 푸시, SMS, 웹 브라우저 알림 모두에서 지원됩니다. 또한 상황에 따라 다양한 알림 서비스를 추가로 선택할 수도 있니다.

이 글에서는 그 중에서도 웹 브라우저를 통한 알림 전송 방법에 대해 자세히 설명하고자 합니다. 웹 브라우저 알림은 사용자의 브라우저를 통해 실시간으로 정보를 전달하는 기능입니다. 이를 구현하는 방법은 여러 가지가 있으나, 엣지크로스는 Server-Sent Events(SSE)라는 기술을 활용하여 구현하고 있습니다.

Server-Sent Events(SSE), 개념부터 확인하기

Server-Sent Events(SSE)는 클라이언트가 서버의 최초 연결 요청을 한 이후에 별도의 요청 없이 서버로부터 새로운 데이터를 전송받을 수 있도록 하는 기술입니다. 이 기술은 주로 실시간 알림과 같은 기능을 웹 브라우저에 구현할 때 활용됩니다. 예를 들어, 사용자가 새로운 메시지나 업데이트를 기다릴 때마다 서버에 요청을 보내는 대신, 서버가 자동으로 관련 정보를 클라이언트에게 전송하게 할 수 있습니다.

SSE 동작 흐름도

SSE 동작 원리

SSE는 HTTP 프로토콜을 기반으로 동작합니다. 클라이언트 측에서 EventSource 인터페이스를 사용하여 서버와의 초기 연결을 시작합니다. 이 연결은 HTTP GET 요청을 통해 수행되며, 이 요청은 특별히 text/event-stream 이라는 MIME 타입을 사용하여 서버에게 클라이언트가 SSE를 지원함을 알립니다.

서버가 이 연결 요청을 수락하면, 연결을 닫지 않고 열린 상태로 유지합니다. 이렇게 지속적인 연결을 통해 서버는 신규 데이터가 발생할 때마다 텍스트 데이터를 이벤트 스트림 형식으로 클라이언트에 실시간으로 전송할 수 있고 각 데이터 전송은 다음과 같은 형식을 따릅니다.

data: Message\\n\\n

서버에서 이벤트를 전송할 때마다 클라이언트의 EventSource 객체는 이를 감지하고 해당 데이터를 onmessage 이벤트로 처리하여 사용자에게 필요한 액션을 취하게 됩니다. 이 과정은 서버에서 클라이언트로의 단방향 통신을 가능하게 하며, 클라이언트는 별도의 요청을 보낼 필요 없이 계속해서 데이터를 받을 수 있습니다.

SSE와 WebSocket의 특징과 차이점

실시간 통신을 위한 또 다른 일반적인 방법은 Websocket을 사용하는 것입니다. SSE와 Websocket은 각각 독특한 특성을 가지고 있습니다. 가장 두드러진 차이는 통신 방향성에 있습니다. SSE는 단방향 통신, 즉 서버에서 클라이언트로만 데이터를 전송하는 방식이며, Websocket은 양방향 통신이 가능하여 서버와 클라이언트가 서로 데이터를 주고받을 수 있습니다.

이 두 기술의 프로토콜 차이도 중요한 고려사항입니다. SSE는 기존 HTTP 프로토콜을 이용하여 연결을 유지하며 데이터를 스트림 형태로 데이터를 전송합니다. 반면에 Websocket은 별도의 Websocket 프로토콜을 사용하여 메시지 교환을 가능하게 합니다.

 

  Web Socket SSE
브라우저 지원 대부분 브라우저에서 지원 대부분 브라우저에서 지원(polyfills 가능)
통신방향 양방향 단방향
리얼타임 O O
데이터 형태 Binary, UTF-8 UTF-8
자동 재접속 X O (3초마다 시도)
최대 동시 접속 수 브라우저 연결 한도는 없지만 서버 셋업에 따라 다름 HTTP를 통해서 할때는 브라우저당 6개까지 가능 / HTTP2로는 100개 기본
프로토콜 Websocket HTTP
배터리소모량 작음
Firewall 친화적 X O

엣지크로스는 실시간 알림 기능 구현에 있어서 서버에서 클라이언트로 데이터를 전송하는 단방향 통신만 필요하기 때문에 SSE를 선택하였습니다. SSE는 HTTP 연결을 재활용하여 데이터를 전송하므로, 별도의 프로토콜 인터페이스의 구현 없이도 빠르고 효율적으로 실시간 웹 알림 기능을 구현할 수 있습니다. 이로 인해 개발 시간과 비용을 절감할 수 있으며, 기존 웹 인프라와의 호환성도 높일 수 있습니다.

SSE를 이용한 웹 브라우저 알림 구현

그럼 실제로 엣지크로스의 솔루션에서 알림이 어떻게 구현되었는 서버 아키텍처, 실제 구현 단계, 샘플코드를 참고해서 설명드리도록 하겠습니다.

엣지크로스의 알림 아키텍

위 그림은 현재 저희 웹 알림 시스템 구조의 간단한 아키텍처 입니다.

  1. AIoT 디바이스: 기계 데이터를 수집하고 처리하여 특정 알림 데이터를 감지합니다. 감지된 알림 알림 데이터를 MQTT Broker로 전송합니다.
  2. MQTT 브로커 서버: 이 서버는 AIoT 디바이스로부터 데이터를 수신하고, 사전에 협의된 알림 토픽으로 데이터를 받습니다. (MQTT에 대한 더 자세한 정보는 이전 테크 블로그를 참조하세요.)
  3. 푸시 서버: 이 서버는 알림 토픽을 구독하고 있으며, 새로운 데이터가 도착하면 이를 클라이언트에게 전송합니다.
  4. 클라이언트: 클라이언트는 푸시 서버로부터 데이터를 받아서 알림창을 통해 유저에게 보여줍니다.

위 아키텍처 중 웹 알림 기능을 추가하기 위해 서버와 클라이언트 모두에 변경이 필요했으며 다음과 같은 단계를 통해 웹 알림 기능을 추가하였습니다.

  1. 서버 설정: 푸시 서버는 Spring Boot 프레임워크를 기반으로 구현되어 있으며 , SSE 기능을 추가하여 EventStream 형식으로 데이터를 전송합니다. 이 과정에서 서버는 클라이언트의 연결 요청을 받아들이고, 구독한 알림 토픽을 통해 들어오는 데이터를 실시간으로 전송합니다.
  2. 클라이언트 설정: 클라이언트는 Next.js 프레임워크를 기반으로 구현되어 있으며, EventSource 객체를 사용하여 서버로부터 데이터를 수신하고, 이를 바탕으로 사용자에게 실시간 알림을 제공합니다.

샘플 코드와 예제

이제 실제 샘플 코드로 예제를 만들어서 설명 드리겠습니다.

서버설정

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.http.MediaType;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Sinks;
import java.time.Duration;

@RestController
public class NotificationController {

    private final Sinks.Many<String> sink = Sinks.many().multicast().onBackpressureBuffer();

    // 알림 이벤트 보내기
    @CrossOrigin(origins = "<http://localhost:3000>")
    @GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<String> sendNotification() {
        return sink.asFlux().delayElements(Duration.ofSeconds(1)); // 각 이벤트 사이에 1초 간격
    }

    // 알림 발생 시 호출되는 API
    @GetMapping("/send")
    public void triggerEvent(@RequestParam String message) {
        sink.tryEmitNext("Received message: " + message);
    }
}

위 예제는 /stream 엔드포인트를 통해 SSE 커넥션을 한 이후에 /send API로 메세지를 전송해 연결되어 있는 클라이언트에게 메세지를 보내는 예제 코드입니다.

다음은 샘플 코드의 중요 구성요소입니다.

  • Sinks.Many sink:
    • Sinks.many().multicast().onBackpressureBuffer()를 사용하여 여러 구독자에게 메시지를 전송할 수 있는 sink를 생성합니다. 이는 클라이언트로부터의 요청에 대해 동시에 다수의 응답을 처리할 수 있게 합니다.
  • stream 엔드포인트:
    • @GetMapping 어노테이션을 사용하여 /stream 경로에 대한 GET 요청을 처리합니다. produces = MediaType.TEXT\_EVENT\_STREAM\_VALUE로 설정하여 서버에서 클라이언트로 텍스트 스트림을 지속적으로 전송합니다.
    • sink.asFlux().delayElements(Duration.ofSeconds(1)) 호출을 통해 생성된 데이터를 1초 간격으로 클라이언트에게 스트리밍합니다.
    • @CrossOrigin(origins = "[http://localhost:3000](http://localhost:3000)"): 크로스 오리진 리소스 공유를 허용하여 지정된 출처의 클라이언트가 리소스에 접근할 수 있도록 설정합니다.
  • send 엔드포인트:
    • /send 경로에서 메시지를 받아 sink를 통해 구독자(클라이언트)에게 "Received message: + 메시지" 형식으로 전송합니다. 이는 실시간 알림이나 이벤트를 트리거하는 데 사용됩니다.

클라이언트 설정

import { useEffect, useState } from 'react';

export default function Home() {
    const [messages, setMessages] = useState([]);

    useEffect(() => {
        const eventSource = new EventSource('<http://localhost:8080/stream>');
        eventSource.onmessage = function(event) {
            setMessages(prev => [...prev, event.data]);
        };

        return () => {
            eventSource.close();
        };
    }, []);

    return (
        <div>
            <h1>Notifications</h1>
            <ul>
                {messages.map((msg, index) => (
                    <li key={index}>{msg}</li>
                ))}
            </ul>
        </div>
    );
}

위 예제는 서버의 최초 연결 한 이후에 지속적으로 서버로부터 데이터를 받아 화면에 그려주는 예제입니다.

다음은 샘플 코드의 중요 구성요소입니다.

  • useState를 사용한 메시지 상태 관리:
    • useState[]를 통해 서버로부터 수신된 메시지 목록을 관리합니다.
  • useEffect와 EventSource:
    • useEffect 훅 내에서 EventSource 객체를 생성하여 서버의 /stream 엔드포인트에 연결합니다.
    • onmessage 핸들러를 사용하여 서버로부터 수신된 메시지를 상태에 추가합니다. 이로써 서버에서 보낸 데이터가 실시간으로 화면에 반영됩니다.
  • 화면 출력:
    • messages 배열을 사용하여 수신된 메시지를 리스트 형태로 화면에 출력합니다.

결과

예제를 실행시키게 되면 위와 같이 동작을 하는 것을 확인할 수 있습니다. 산출물을 보시면 최초 연결시에 네트워크 펜딩이 결려있고 끝나지 않는 것을 확인할 수 있습니다. 그 이후에 서버에서 데이터를 지속적으로 보내면 클라이언트(웹 브라우저)에서 별도의 요청없이 실시간으로 데이터를 받는 것을 확인하실 수 있습니다.

본 예제에서는 API를 통해 직접 이벤트를 발생시켜 보았지만, 이를 MQTT 브로커를 통해 특정 토픽을 구독하거나, 정기적인 배치 작업을 통해 특정 시간마다 이벤트를 발생시키는 방식으로 응용할 수도 있습니다. 이와 같은 변형은 각자의 프로젝트 요구사항에 따라 매우 다양하게 적용될 수 있습니다.

이상으로, 저희 엣지크로스에서는 실시간 웹 브라우저 알림을 구현하는 방법 중 하나인 Server-Sent Events (SSE) 기술을 중심으로 소개해 드렸습니다. SSE는 사용자 경험을 향상시키고 실시간 데이터 모니터링의 효율성을 높이는 데 큰 장점을 가지고 있습니다. 복잡한 설정 없이도 웹 애플리케이션에서 서버로부터의 데이터 스트림을 쉽게 구현할 수 있기 때문에, 특히 동적인 데이터 처리가 필요한 경우에 적합합니다.

저희가 제공한 구현 사례와 샘플 코드가 여러분의 프로젝트에 유용한 인사이트를 제공하고, SSE를 활용한 다양한 방법을 모색하는 데 도움이 되기를 바랍니다.

감사합니다.


엣지크로스 솔루션, 더 자세히 알아보고 싶으시다면 ✅ https://edgecross.ai/solution