백엔드 서버가 두개가 있고, 프론트엔드 서버가 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