Coding History

chat 웹앱 수업.

BlackBirdIT 2024. 9. 11. 09:10

사실 이미 좀 진행했던 건데 안듣고 내 프로젝트 하다가 오늘은 제대로 들었다.
그래서 중간부터 서술하는 점 이해 바란다.

<script>
    // 채팅 메세지 읽기 (read)
    // 클라이언트가 받은 메세지의 번호를 입력해야함
    // --> 메세지 가져오기 요청시에 필요한 부분만 잘라서 가져올 수 있다.
    let Chat__lastLoadedId = 0;

    function Chat__loadMore() {
        fetchGet("/chat/messages", {
            fromId: Chat__lastLoadedId
        })
            .then(body => {
                console.log('body :' + body);
                console.log('body.data : ' + body.data);
                console.log('body.data.chatMessages : ' + body.data.chatMessages);
                Chat__drawMessages(body.data.chatMessages);
            });
    }


    const Chat_elMessageUl = document.querySelector('.chat__message-ul');

    function Chat__drawMessages(messages) {
        if (messages.length > 0) {
            // 메세지를 그리기 전에 Chat__lastLoadedUuid 변수를 갱신합니다.
            Chat__lastLoadedId = messages[messages.length - 1].id;
        }

        // 메세지를 그리기 전에 Chat__lastLoadedUuid 변수를 갱신합니다.
        Chat__lastLoadedId = messages[messages.length - 1].id;
        console.log(Chat__lastLoadedId);

        messages.forEach((message) => {
            Chat_elMessageUl
                .insertAdjacentHTML(
                    "afterBegin",
                    `<li>${message.authorName} : ${message.content}</li>`
                );
        });
        // Chat__loadMore(); 즉시실행
        setTimeout(Chat__loadMore, 500); // 0.5초 뒤에 실행
    }

    // 최초에 한번 불러오기
    Chat__loadMore();
</script>

이런 식으로 하면 폴링 방식으로 0.5초마다 채팅방을 갱신한다.

찔러보는 것은 나쁜건 아니지만 에너지 소모가 심하다. -> 무분별한 호출 야기를 방지하는 것이 목표.

그래서 이걸 SSE 방식으로 바꾼다. (아쉬운 부분을 수정.)

SSE

  • 브라우저 notify
  • 새 데이터가 들어온 것을 인식

새 데이터가 들어온 것을 인식하는 것이 핵심.

    const sse = new EventSource("/sse/connect");

    sse.addEventListener('chat__messageAdded', e => {
        Chat__loadMore();
    });
    // 최초에 한번 불러오기
    Chat__loadMore();

해당코드를 맨 뒤에 작성해준다.
'chat__messageAdded' 이 명령어를 감지하면 갱신한다.
그래서 이 명령어를 담당할 어떤 무언가를 또 만들어 주어야한다.

Java에서 Ut 클래스 생성해주고 아래와 같은 코드를 작성.

public class Ut {
    public static <K, V> Map<K, V> mapOf(Object... args) {
        Map<K, V> map = new LinkedHashMap<>();

        int size = args.length / 2;

        for (int i = 0; i < size; i++) {
            int keyIndex = i * 2;
            int valueIndex = keyIndex + 1;

            K key = (K) args[keyIndex];
            V value = (V) args[valueIndex];

            map.put(key, value);
        }

        return map;
    }
}

SseEmitters클래스와 SseController도 생성.

그리고 컨트롤러에 SseEmitters 클래스를 불러온다.

@Component
@Slf4j
public class SseEmitters {

    private final List<SseEmitter> emitters = new CopyOnWriteArrayList<>();

    public SseEmitter add(SseEmitter emitter) {
        this.emitters.add(emitter);
        emitter.onCompletion(() -> {
            this.emitters.remove(emitter);
        });
        emitter.onTimeout(() -> {
            emitter.complete();
        });

        return emitter;
    }

    public void noti(String eventName) {
        noti(eventName, Ut.mapOf());
    }

    public void noti(String eventName, Map<String, Object> data) {
        emitters.forEach(emitter -> {
            try {
                emitter.send(
                        SseEmitter.event()
                                .name(eventName)
                                .data(data)
                );
            } catch (ClientAbortException e) {

            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        });
    }
}
@Controller
@RequestMapping("/sse")
@RequiredArgsConstructor
public class SseController {
    private final SseEmitters sseEmitters;

    @GetMapping(value = "/connect", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public ResponseEntity<SseEmitter> connect() {
        SseEmitter emitter = new SseEmitter();
        sseEmitters.add(emitter);
        try {
            emitter.send(SseEmitter.event()
                    .name("connect")
                    .data("connected!"));
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        return ResponseEntity.ok(emitter);
    }
}

이후 write메서드 안쪽에 해당 코드를 불러와서 'chat__messageAdded' write메서드가 실행될 때 저걸 알아먹게 해준다.

    @PostMapping("/writeMessage")
    @ResponseBody
    public RsData<writeMessageResponse> writeMessage(@RequestBody writeMessageRequest req) {

        ChatMessage message = new ChatMessage(req.authorName, req.content);

        chatMessages.add(message);

        sseEmitters.noti("chat__messageAdded");

        return new RsData<>("S-1", "메세지가 작성됨", new writeMessageResponse(message.getId()) );
    }

이렇게 하면 채팅을 쳐서 데이터가 갱신될 때 자동으로 화면에 바로바로 갱신된다!