SSE Protocol 활용해서 Spring => React 단방향 통신하자.

공부는 혼자하는 거·2021년 9월 3일
2

Spring Tip

목록 보기
6/52
post-custom-banner

백엔드 서버가 두개가 있고, 프론트엔드 서버가 1개가 있다. 문제는 이거, A라는 벡 서버가 B라는 q벡서버에 소켓으로 데이터를 쏴주면
B라는 벡서버는 C라는 프론트서버에게 그 데이터를 다시 쏴주어야 한다. C는 그 데이터를 화면에 뿌려줘야 된다.

리액트에서는 Rest Controller를 달 수도 없는 노릇이고, 어떻게 백서버에서 프론트로 데이터를 일방적으로 쏠 수 있을까.

이럴때 사용하는 방법은 크게 두 가지다.
1. 웹 소켓
2. SSE

웹소켓은 양방향 통신이 가능하고, SSE 는 서버에서 프론트로 단방향 통신만 가능하다. 물론 어떻게 만드느냐에 따라서 다 방법은 있더라.
처음에는 웹 소켓을 이용해 작업하려다가, SSE 가 더 간편하고 내 목적에 적합해서 방법을 교체했다. 브라우저 호환성, 스프링 버전 같은 거 일단 집어치우고

작업 환경은 스프링부트, 리액트, A는 생략하고,

스프링부트에서는 Web-flux를 활용하는 기법과 Web-mvc 를 활용하는 기법이 있는데, 나는 후자 쪽을 선택했다.

먼저 이 주소로 프론트가 구독하도록 컨트롤러를 하나 만들어준다.


	@GetMapping("/sse") //발행
	public SseEmitter streamDateTime() { // webmvc
        SseEmitter sseEmitter = new SseEmitter(Long.MAX_VALUE);  
        
        log.info("처음 등록된 emitter 주소"+ sseEmitter.toString());
        serversentService.register(sseEmitter);

		return sseEmitter;
	}

서비스를 하나 만든다


import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

import lombok.RequiredArgsConstructor;
import net.lunalabs.central.config.MeasureDataSse;

@RequiredArgsConstructor
@Service
public class ServersentService {

	
	private static final Logger logger = LoggerFactory.getLogger(ServersentService.class);

	private final MeasureDataSse measureDataSse;
	
	public void register(SseEmitter emitter) {
		
		logger.info("브라우저마다 eventsource 연결할때마다 emitter 객체 생성하여 등록(새로고침 시 혹은 페이지 이동, 새로 브라우저 띄울때마다");
		
	    emitter.onTimeout(() -> timeout(emitter));
	    emitter.onCompletion(() -> complete(emitter));

	    measureDataSse.emitters.add(emitter);
	}

	private void complete(SseEmitter emitter) {
	    System.out.println("emitter completed");
	    measureDataSse.emitters.remove(emitter);
	}

	private void timeout(SseEmitter emitter) {
	    System.out.println("emitter timeout");
	    measureDataSse.emitters.remove(emitter);
	}

	//@Scheduled(fixedDelay = 3000)
	public void sendSseEventsToUI(String seeMeasurePatientData) { 
		
		logger.info("서버에서 단방향으로 브라우저에 보낼 데이터: "+seeMeasurePatientData);
		
		Iterator<SseEmitter> iter = measureDataSse.emitters.iterator();

		while (iter.hasNext()) { //일종의 브로드캐스팅 이벤트소스 연결된 브라우저마다 다 쏜다.
		    SseEmitter emitter = iter.next();
		   
	        try {
	        	logger.info("data 보내는 객체 주소: " + emitter.toString());
//	            emitter.send(SseEmitter.event().reconnectTime(500).data(seeMeasurePatientData), MediaType.APPLICATION_JSON);
	            emitter.send(SseEmitter.event().reconnectTime(500).data(seeMeasurePatientData));
	        } catch (Throwable e) {
	            emitter.complete();
	        }
		}
		 		
		
//	    for(SseEmitter emitter : measureDataSse.emitters) {
//	    	
//	    	logger.info(emitter.toString());
//	        try {
//	        	
//	        	logger.info("보냈는데?");
//	            emitter.send(SseEmitter.event().reconnectTime(500).data(seeMeasurePatientData), MediaType.APPLICATION_JSON);
//	        } catch (Throwable e) {
//	            emitter.complete();
//	        }
//	    };
	}
	
}
		try {
			//보내고 싶은 객체를 json 문자열로 만들고,
			seeMeasurePatientData = objectMapper.writeValueAsString(dataJoinPatientBean);
			serversentService.sendSseEventsToUI(seeMeasurePatientData); //쏘아준다.
		} catch (JsonProcessingException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}



리액트에서 구독




import React, { useEffect, useState } from "react";
import useUpdateEffect from "../store/hooks/useUpdateEffect";

function See() {
  const [listening, setListening] = useState(false);
  const [data, setData] = useState([]);
  const [value, setValue] = useState(null);

  const [meventSource, msetEventSource] = useState(undefined);

  let eventSource = undefined;

  useEffect(() => {
    console.log("매번 실행되는지");
    console.log("listening", listening);

    if (!listening) {
      eventSource = new EventSource("http://localhost:8080/sse"); //구독

      //msetEventSource(new EventSource("http://localhost:8088/sse"));

      msetEventSource(eventSource);

      //Custom listener
      // eventSource.addEventListener("Progress", (event) => {
      //   const result = JSON.parse(event.data);
      //   console.log("received:", result);
      //   setData(result)
      // });

      console.log("eventSource", eventSource);

      eventSource.onopen = event => {
        console.log("connection opened");
      };

      eventSource.onmessage = event => {
        console.log("result", event.data);
        setData(old => [...old, event.data]);
        setValue(event.data);
      };

      eventSource.onerror = event => {
        console.log(event.target.readyState);
        if (event.target.readyState === EventSource.CLOSED) {
          console.log("eventsource closed (" + event.target.readyState + ")");
        }
        eventSource.close();
      };

      setListening(true);
    }

    return () => {
      eventSource.close();
      console.log("eventsource closed");
    };
  }, []);

  useUpdateEffect(() => {
    console.log("data: ", data);
  }, [data]);

  const checkData = () => {
    console.log(data);
  };

  return (
    <div className="App">
      <button onClick={checkData}>확인</button>
      <header className="App-header">
        <div style={{ backgroundColor: "white" }}>
          Received Data
          {data.map((d, index) => (
            <span key={index}>{d}</span>
          ))}
        </div>
      </header>
      <div>value: {value}</div>
    </div>
  );
}

export default See;


https://turkogluc.com/server-sent-events-with-spring-boot-and-reactjs/
https://dev.to/sirwanafifi/server-sent-events-and-react-5ec9
https://velog.io/@devky/TIL-Javascript-%EC%B5%9C%EC%8B%A0-%EB%AC%B8%EB%B2%95-%EC%A0%95%EB%A6%AC
profile
시간대비효율
post-custom-banner

0개의 댓글