서버 ↔ 클라이언트 간의 실시간 메세지 교환에 사용되는 통신 기술!
자세한 설명은 이전에 정리해둔 글을 링크해둔다 ㅎㅎ 웹소켓에 대하여. SockJS와 socket.io
티키타자에서 실시간 게임 기능을 구현하기 위해 실시간 소통인 웹소켓을 사용했다.
웹소켓의 기본 동작 흐름과 기본 코드 구현에 대해 정리해보려고 한다.
yarn add @stomp/stompjs
yarn add sockjs-client
게임방 선택 및 입장 시 ⇒ 3연결 및 4구독
[준비] 를 누른다 ⇒ 5발행
다른 유저가 누른 [준비]가 나에게 실시간으로 보인다 ⇒ 5응답
웹소켓 훅을 사용했다.
입장할 방 id를 가지고 웹소켓 로직에서 서버 연결-구독을 하고 특정 행동에 따라 발행 등의 핸들러 함수가 필요하기에, 커스텀훅을 사용했다.
플로우 : GamePage에서 useWebsocket훅에 입장할 roomId를 넘기며 호출 → 훅 안에서 웹소켓 연결, 구독 등을 → 이후 핸들러 함수들을 반환 → 이 핸들러 함수들을 가지고 컴포넌트에서 이벤트에 대해 행동
게임방 “접속”, “준비”, “시작”, “게임중” 모든 부분에 웹소켓 통신이 연결되어 있는데, 그 중 “준비” 로직에 대해 설명해보겠다!
SockJS를 사용하여 엔드포인트에 연결한 Client 객체를 만든다.
이 객체는 onConnect, onDisconnect, onStompError 등의 속성을 가지고 있다.
const useWebsocket = (roomId: number | null) => { const stompClient = useRef<Client>(); useEffect(() => { const client = new Client({ // 객체 생성!! webSocketFactory: () => new SockJS(`${BASE_PATH}/ws`), connectHeaders: connectHeaders, onConnect: () => { // 연결 성공시 실행 함수 }, onStompError: (err) => { console.log(err); }, }); client.activate(); stompClient.current = client; return () => { client.deactivate(); }; }, []); return { }; // 추후 }; export default useWebsocket;
각각 웹소켓 연결, 연결해제, 오류 등이 발생했을 때 작동한다.
각 상태에 따라 발생할 콜백 함수들을 속성의 값에 넣으면 된다!
웹소켓이 연결되면 위에서 말한 onConnect 속성에 지정된 함수들이 실행된다.
이 때 실행해야 할 함수들은 특정 채널(이하 게임방)을 “구독” 하는 것이다.
구독에는 subscribe 메소드가 사용된다.
subscribe(destination:string, callback:messageCallbackType, headers:StompHeaders)
티키타자 ver
우선, 응답값의 필드를 확인해야 한다. 아마 백엔드에서 정해준 형태가 있을것!
우리의 경우엔 다음과 같았다.
{ **"type": "READY",** "roomId": 1, "roomInfo": { }, "allMembers": [ { "memberId": 7, "nickname": "유저51", **"readyStatus": true** }, { "memberId": 1, "nickname": "유저23", "ranking" : 1 **"readyStatus": true** } ] }이렇게 ‘type’을 통해 누군가 ‘READY’ 메시지를 보냈음을 알 수 있다.
그럼 subscribe의 두번째 인자 callback 함수를 만들어보자
const onMessageReceived = ({ body }: { body: string }) => { const responsePublish = JSON.parse(body); if ( responsePublish.type === 'ENTER' || responsePublish.type === 'READY' ){ // 입장(ENTER), 준비(READY) 메시지를 받았을 때 실행할 로직 ! setAllMembers(responsePublish.allMembers); } };
입장이나 준비 메시지를 받으면 새로운 참여자들 정보 목록으로 상태를 set하라는 함수를 넣었다.
그럼 이제 Client 객체의 onConnect 에 연결하면 된다!
추가로 구독할 엔드포인트가 늘어날 것을 고려해 함수로 분리하였다.
const client = new Client({ // .. onConnect: () => onConnected(); // <- 여기! // .. 중략 const onConnected = () => { client.subscribe( `/from/game-room/${roomId}`, (e) => onMessageReceived(e), connectHeaders );
티키타자에는 두가지 발행 플로우가 있다.
훅으로 분리했기 때문에, 1번의 경우만 훅안에서 실행되고 2의 경우는 훅 바깥의 컴포넌트에서 onClick 이벤트에 걸려야 할 함수이다!
publish({destination:string, headers, body}) destination만 필수. 객체형태로 인자를 줘야함!
onConnect 에 주면 된다!onConnect: () => { if (roomId) { onConnected(); handleEnterGameRoom(roomId); //<-! } },const handleEnterGameRoom = (roomId: number) => { client.publish({ destination: `/to/game-room/${roomId}/enter`, headers: connectHeaders, // 보낼 메시지가 있는 경우가 아니라 body도 생략 }); };
// publish 함수 기본 속성값을 지정 한 함수 const publishGameRoom = (destination: string) => { stompClient.current?.publish({ destination: `/to/game-room${destination}`, headers: connectHeaders, }); }; // 위 함수를 확장해서 준비 이벤트 함수 const handlePubReadyGame = () => { publishGameRoom(`/${roomId}/ready`); }; // .. return { handlePubReadyGame, }; }; export default useWebsocket;사용하는 곳에서
const { handlePubReadyGame } = useWebsocket(roomId); // ... return ( <button onClick={() => { handlePubReadyGame(); }}
발행(전송) 에 대해서 compatClient 클래스는 send 메소드를, Client 클래스는 publish 메소드를 사용한다.
공식문서에서도 이전버전부터와의 호환성을 위해 Client 클래스로 이전을 권장한다.