Wordle Maker Project Spring 9일차

PROLCY·2023년 2월 2일
0

오늘은 2월 2일 9일차이다.

목표

  • 웹소켓
  • 로컬 배포

진행

드디어 웹소켓이 끝났다.

package prolcy.wordle_maker_spring.websocket;

import com.google.gson.Gson;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import prolcy.wordle_maker_spring.dto.MakerDTO;
import prolcy.wordle_maker_spring.dto.SolversResponseDTO;
import prolcy.wordle_maker_spring.service.SolverService;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Component
@RequiredArgsConstructor
@Log4j2
public class WebSocketHandler extends TextWebSocketHandler {
    private final Map<String, WebSocketSession> makers = new HashMap<>();
    private final SolverService solverService;
    
    @Override
    public void afterConnectionEstablished(WebSocketSession session) {
        log.info("-----------websocket connected------------");
    }
    
    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
        log.info("-----------websocket disconnected---------");
    }
    
    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        String makerNickname = message.getPayload();
        if(makerNickname.startsWith("#")) {
            makers.put(makerNickname.substring(1), session);
        } else {
            WebSocketSession maker = makers.get(makerNickname);
            if(maker == null)
                return;
            MakerDTO makerDTO = MakerDTO.builder()
                    .nickname(makerNickname)
                    .build();

            List<SolversResponseDTO> solvers = solverService.getSolversByMaker(makerDTO);

            Gson gson = new Gson();
            TextMessage textMessage = new TextMessage(gson.toJson(solvers));
            maker.sendMessage(textMessage);
        }
    }
}

WebSocketHandler이다. makers는 닉네임에 따라 WebSocketSession을 저장하는 해시이다. handletextMessage에서 받은 메시지를 관리하는데, 만약 받은 메시지의 맨 앞의 #이라면, 이것은 LoadPage에서 등록목적으로 보낸 것이므로 makers 해시에 저장한다. 아니라면 메시지를 SolverPage에서 보낸 것이므로, solvers를 쿼리해서 닉네임에 해당하는 웹소켓 세션에 메시지로 전송한다.

package prolcy.wordle_maker_spring.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
import prolcy.wordle_maker_spring.websocket.WebSocketHandler;

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
    private final WebSocketHandler webSocketHandler;

    public WebSocketConfig(WebSocketHandler webSocketHandler) {
        this.webSocketHandler = webSocketHandler;
    }
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(webSocketHandler, "/typing").setAllowedOrigins("*");
    }
}

WebSocketConfig 클래스이다. WebSocket 설정파일이다. 핸들러를 경로에 따라 추가할 수 있다.

        //개발용
        //const ws = new WebSocket('ws://localhost:8080/typing');

        //배포용
        const ws = new WebSocket("wss://" + window.location.host + "/typing");

        ws.onopen = () => {
            ws.send("#" + nickname);
        }
        ws.onmessage = message => {
            setSolvers(JSON.parse(message.data));
        }

클라이언트의 LoadPage의 일부분이다. 웹소켓을 새로 생성하고, 생성하는 즉시 #{nickname} 형식으로 메시지를 보낸다. 이는 백엔드에서 WebSocketSession 등록 목적으로 보내는 것이다. 이후 메시지가 도착한다면 setSolvers를 통해 실시간으로 LoadPage를 업데이트해준다.

            client.post(`/solve/${params.maker}/typing`, { newWord: JSON.stringify(word), keyState: JSON.stringify(keyState), listIndex: listIndex }) // 입력한 단어 서버에 등록
                .then( res => {
                    if ( word.length !== 0 && word[0].state !== 'filled' ) { // 색칠된 단어인 경우
                        setListIndex(listIndex + 1);
                        setWord([]);
                    }
                    ws.send(params.maker);
                })

클라이언트의 SolverPage의 일부분이다. 이 부분은 solver가 문자 하나를 입력할 때마다 실행되는데, 입력한 문자를 DB에 저장 후, 응답이 오면 ws.send를 통해 업데이트되었다는 신호를 웹소켓으로 보내고, 백엔드에서 그걸 받아서 LoadPage에 다시 보내는 것이다.

순서를 정리하자면 다음과 같다.
1. Maker가 LoadPage에 접속, 서버와 웹소켓 연결
2. 연결 즉시 서버로 nickname을 보내서 WebSocketSession 등록
3. Solver가 SolverPage에 접속, 서버와 웹소켓 연결
4. Solver가 문자 입력 시 POST solve/{makerNickname}/typing 요청 보냄(데이터베이스 최신화 목적)
5. 응답이 오면 웹소켓을 통해 makerNickname을 서버로 전송
6. 서버에서 makerNickname에 해당하는 WebSocketSession을 makers 해시에서 찾은 후, DB에서 쿼리한 solvers 리스트를 그 세션으로 전송
7. LoadPage에서 받아서 페이지 업데이트

다 하고 나서는 지난번 프로젝트처럼 ngork을 활용해 로컬 환경에서 배포를 해보았다. 그러기 위해서는 클라이언트와 백엔드를 합쳐야 했는데, 라우팅을 어떻게 할 지를 계속 찾다가 하나의 방법을 겨우 알아냈다.

package prolcy.wordle_maker_spring.controller;

import org.springframework.boot.web.servlet.error.ErrorController;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class IndexController implements ErrorController {
   @RequestMapping("/error")
   public String redirect() {
       return "forward:/";
   }
}

IndexController이다. ErrorController를 구현하고 있는데, 에러 페이지, 즉 주소를 찾을 수 없는 페이지에 접속 시 무조건 index.html로 렌더링하는 역할을 한다. 이를 통해서 SPA 형태인 클라이언트와 API 서버 형태인 백엔드를 합쳐서 한 포트에서 로컬 배포를 할 수 있었다.
친구에게 ngrok에서 나온 주소를 보내주고 테스트 해보니 다 잘 작동하였다.

내일 할 것

  • 코드 정리, 주석 처리
  • 프로젝트 마무리

마무리

목표대로 10일차 안에 마무리할 수 있었다. 역시 새로 만드는 것이 아니라, 내가 전에 만들어 놓은 것을 다른 플랫폼으로 옮기는 것은 그렇게가지 오래 걸릴 일은 아닌 것 같다. 그래도 spring과 java를 이번에 처음 접해서 마냥 쉽지만은 않았지만, 확실히 프레임워크를 다루는 것이다보니 express로 구성할 때보단 재미있었고, 얻어간 것도 많았다. 앞으로는 spring과 java 위주로 공부하고 프로젝트를 해서 더 나은 실력을 가지도록 해야겠다.

0개의 댓글