Notice
Recent Posts
Recent Comments
Link
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
Tags
more
Archives
Today
Total
관리 메뉴

개발자입니다

[프로젝트] SSE 방식으로 서버(SpringBoot)에서 클라이언트(React)로 데이터 보내기 본문

네이버클라우드 AIaaS 개발자 양성과정 1기/프로젝트

[프로젝트] SSE 방식으로 서버(SpringBoot)에서 클라이언트(React)로 데이터 보내기

끈기JK 2023. 4. 19. 20:46

 

SSE 란?

 

SSE(Server-Sent Events)는 서버와 클라이언트 간에 양방향 통신을 가능하게 하는 기술 중 하나입니다. 이 기술은 서버에서 이벤트를 생성하고, 클라이언트는 이벤트를 수신하여 동적인 콘텐츠를 렌더링할 수 있습니다.

SSE는 HTTP 연결을 유지하면서 서버에서 이벤트를 전송하는 단방향 통신 방식입니다. 이벤트는 JSON, XML, 텍스트 등의 형식으로 전송될 수 있으며, 이벤트에는 이벤트 이름, 데이터 등의 정보가 포함될 수 있습니다.

클라이언트는 SSE 연결을 유지하면서 서버에서 이벤트를 수신합니다. 이벤트가 도착하면 클라이언트는 이를 처리하고, 동적인 콘텐츠를 렌더링할 수 있습니다. 이를 통해 서버와 클라이언트 간에 양방향 통신을 쉽게 구현할 수 있으며, 실시간 업데이트가 필요한 웹 애플리케이션에서 유용하게 사용됩니다.

SSE는 WebSocket과 유사한 기술이지만, WebSocket은 양방향 통신을 가능하게 하는 반면, SSE는 단방향 통신만 가능합니다. 따라서, WebSocket이 필요한 경우와 SSE가 필요한 경우를 구분하여 사용해야 합니다.

 

 

 

코드 작성

 

 

서버

 

SseManager
package bitcamp.app;

import java.util.ArrayList;
import java.util.List;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

public class SseManager {
  private List<SseEmitter> emitters = new ArrayList<>();

  public synchronized void addEmitter(SseEmitter emitter) {
    emitters.add(emitter);
  }

  public synchronized void removeEmitter(SseEmitter emitter) {
    emitters.remove(emitter);
  }

  public synchronized void sendToAll(String message) {
    List<SseEmitter> deadEmitters = new ArrayList<>();
    emitters.forEach(emitter -> {
      try {
        emitter.send(message);
      } catch (Exception e) {
        deadEmitters.add(emitter);
      }
    });
    emitters.removeAll(deadEmitters);
  }
}

 

 

SseController 

클라이언트에서 구독할 주소를 가진 SseController 를 작성한다.

package bitcamp.app.controller;

import java.util.Map;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import bitcamp.app.SseManager;

@RestController
public class SseController {

  Logger log = LogManager.getLogger(getClass());

  {
    log.trace("SseController 생성됨!");
  }

  private SseManager sseManager = new SseManager();
  private final ObjectMapper objectMapper = new ObjectMapper();

  @GetMapping("/sse")
  public SseEmitter handleSse() {

    SseEmitter emitter = new SseEmitter();
    log.info("등록된 emitter 주소 >>> " + emitter.toString());
    sseManager.addEmitter(emitter);

    emitter.onCompletion(() -> sseManager.removeEmitter(emitter));
    emitter.onTimeout(() -> sseManager.removeEmitter(emitter));

    return emitter;
  }

  public void sendMessageToAll(Map<String, String> message) {
    try {
      String jsonMessage = objectMapper.writeValueAsString(message);
      // log.info("jsonMessage >>> " + jsonMessage);
      sseManager.sendToAll(jsonMessage);
    } catch (JsonProcessingException e) {
      // JSON 변환 중 에러 처리
      e.printStackTrace();
    }
  }
}

 

 

BoardController

SseController 주입받고 각 단계마다 클라이언트로 메시지를 전송한다.

@RestController
@RequestMapping("/boards")
public class BoardController {

  @Autowired private SseController sseController;
  
  @PostMapping
  public Object insert(int writerNo, String originContent, HttpSession session) {
  
 		  /* GPU 서버로 명령어 전송 */
  
          Map<String, String> sseMap = new HashMap<>();
          sseMap.put("status", "process");
          sseMap.put("message", "GPU Server 이미지 생성 중");
          sseController.sendMessageToAll(sseMap);
          
          	  /* 이미지 생성 완료 */
              
              sseMap = new HashMap<>();
              sseMap.put("status", "success");
              sseMap.put("message", "GPU Server 이미지 생성, DB에 게시글, 파일 업로드 완료");
              sseController.sendMessageToAll(sseMap);
              
            /* 이미지 생성 중 에러 발생 */

            sseMap = new HashMap<>();
            sseMap.put("status", "failure");
            sseMap.put("message", "GPU Server 이미지 생성 중 에러 발생");
            sseController.sendMessageToAll(sseMap);
            
  }
}

 

 

 

클라이언트

 

 

SSEContext

Context 를 생성한다.

import { createContext } from "react";

const SSEContext = createContext();

export default SSEContext;

 

SSEProvider

EventSource("http://localhost:8080/sse") 로 서버 이벤트를 구독한다.

import React, { useState, useEffect } from "react";
import SSEContext from "./SSEContext";

const SSEProvider = ({ children }) => {
  const [sseMessage, setSseMessage] = useState("");

  useEffect(() => {
    let eventSource;

    // Create a new EventSource instance to connect to the server
    const setupEventSource = () => {
      eventSource = new EventSource("http://localhost:8080/sse", {
        withCredentials: true,
      });

      // Set up the event listener for the 'message' event
      eventSource.onmessage = (event) => {
        // console.log("Received SSE message:", event.data);
        const parsedData = JSON.parse(event.data);
        setSseMessage(parsedData);
      };

      // Set up the event listener for the 'error' event
      eventSource.onerror = (error) => {
        console.error("SSE error:", error);
        setTimeout(() => {
          setupEventSource();
        }, 5000);
      };
    };

    setupEventSource();

    // Clean up the connection when the component is unmounted
    return () => {
      if (eventSource) {
        eventSource.close();
      }
    };
  }, []);

  return (
    <SSEContext.Provider value={sseMessage}>{children}</SSEContext.Provider>
  );
};

export default SSEProvider;

 

App

SSEProvider 로 다른 컴포넌트를 감싼다.

function App() {

  return (
    <>
      <div>
        <SSEProvider>
          <BrowserRouter>
            <Navbars
            
    /* 생략 */
}

 

 

Navbars

useContext 로 SSEContext 를 가져온다.

sseMessage 가 업데이트 되면 내부에서 사용하는 message 의 state 를 업데이트 한다.

function Navbars(props) {
  const sseMessage = useContext(SSEContext);
  const [message, setMessage] = useState(null);
  
  useEffect(() => {
    setMessage(sseMessage);
  }, [sseMessage]);
  
  return (

              <div className="d-flex ms-2 me-2 justify-content-center align-items-center">
                {message || props.currentUser?.isGenerating === 1 ? (
                  (() => {
                    let variant, label, animated, status;

                    status = message
                      ? message.status
                      : props.currentUser?.isGenerating === 1
                      ? "process"
                      : "";

                    switch (status) {
                      case "success":
                        variant = "success";
                        label = "생성 완료";
                        animated = false;
                        break;
                      case "failure":
                        variant = "danger";
                        label = "에러 발생";
                        animated = false;
                        break;
                      case "process":
                      default:
                        variant = "info";
                        label = "생성 중";
                        animated = true;
                    }

                    return (
                      <ProgressBar
                        variant={variant}
                        now={100}
                        label={label}
                        animated={animated}
                        style={{
                          width: "70px",
                          height: "20px",
                          fontSize: "0.75rem",
                        }}
                      />
                    );
                  })()
                ) : (
                  <div></div>
                )}
              </div>