[웹퍼즐] 퍼즐 플레이 페이지: socket

🐈 JAELEE 🐈·2021년 11월 26일
0

웹퍼즐 프로젝트

목록 보기
3/7

멀티플레이 환경을 위해 필요한 것

  • 같은 이미지, 난이도의 퍼즐 플레이 페이지라도 고유한 페이지, 일종의 방
  • 사용자가 움직이는 퍼즐의 실시간 위치 정보
    ->서버와의 실시간 상호작용을 지원하는 기술이 필요하다

socket

WebSocket과 Socket.io
웹소켓과 socket.io
WebSocket과 Socket.IO

webSocket은 웹 페이지의 한게에서 벗어나 실시간으로 상호작용하는 웹 서비스를 만드는 HTML5 표준 기술이다.

웹소켓이 있기까지

전형적인 브라우저 렌더링 방식에서 벗어나 실시간으로 사용자와 상호작용하는 방식이 나타나고 사용자와 상호작용하는 웹 서비스를 선호하는 사용자가 증가하며 RIA(Rich Internet Application) 기술의 발달이 촉진되었다. Long Polling, Stream 등의 방식은 브라우저가 HTTP 요청를 보내고 웹 서버가 이 요청에 대한 HTTP 응답를 보내는 단방향 메세지 교환 '규칙'을 변경하지 않고 구현한 방식이라 복잡하고 어려운 코드로 상호작용을 구현된다.
보다 쉽게 상호작용하는 웹 페이지를 위해 HTML5 표준안의 일부로 WebSocket API(이후 WebSocket)가 등장했다.

Socket.io

socket.io는 node.js 기반으로 만들어진 기술로, 웹소켓이 HTML5의 기술이기에 오래된 버전의 웹 브라우저는 웹소켓을 지원하지 않는 반면, socket.io는 거의 모든 웹 브라우저와 모바일 장치를 지원하는 실시간 웹 애플리케이션 지원 라이브러리이다.

websocket과 socket.io

WebSocketSocket.io
HTML5 웹 표준 기술표준 기술이 아니며 라이브러리임
매우 빠르게 동작하며 통신할 때 아주 적은 데이터를 사용함소켓 연결 실패시 fallback을 통해 다른 방식으로 알아서 해당 클라이언트와 연결을 시도함
이벤트를 단순히 듣고 보내는 것만 가능함방 개념을 이용해 일부 클라이언트에게만 데이터를 전송하는 브로드캐스팅이 가능함

때문에 서버와 빠른 상호작용, 다양한 웹 브라우저, 같은 이미지, 난이도의 퍼즐 플레이 페이지라도 고유한 방을 만들 수 있는 브로드캐스팅을 지원하는 socket.io를 사용했다

React에서 소켓 관리하기

https://dev.to/bravemaster619/how-to-use-socket-io-client-correctly-in-react-app-o65

우리의 프로젝트는 React 환경에서 제작되었다. 사용자는 고유한 제각각의 퍼즐 페이지에 드나들 수 있어야 하기 때문에 소켓 생성과 관리 및 필요한 컴포넌트에 어떻게 소켓을 전달할지가 고민이었다. socket을 필요한 컴포넌트들의 부모에서 생성한 후 뿌리는 방법을 생각 했다.
하지만 socket 개체가 한번 생성되고 나면 변경될 여지가 없는 우리의 프로젝트를 고려해 컨텍스트나 전역객체로 다루면 좋을 것이란 조언을 들었다. 그래서 context를 활용하여 소켓 관리를 했다.

//context/socket.tsx
import React from "react";
import io from "socket.io-client";

export const socket = io(`${process.env.REACT_APP_ROOT_URL}`);
export const SocketContext = React.createContext(socket);
//pages/play-puzzle/index.tsx
import { SocketContext, socket } from "@src/context/socket";
const PlayPuzzle: FC<{
  	match: { params: { puzzleID: string; roomID: string } };
}> = (props) => {
  //socket.emit, socket.on을 바로 쓸 수 있다.
}

플레이룸 생성하기

같은 이미지, 난이도의 퍼즐 플레이 페이지라도 고유한 방을 만들기 위해서 퍼즐 목록에서 퍼즐 페이지의 주소는 다음과 같다. /room/:puzzleID/:roomID"
puzzleID야 퍼즐 목록에서 id를 받아오면 되지만 고유한 roomID를 만들기 위해선 어떻게 해야 할까? 또 해당 그룹의 멤버끼리만 통신을 하기 위해선 어떻게 소켓을 처리해야 할까?

  1. 퍼즐 목록을 클릭한다
  2. ${process.env.REACT_APP_API_URL}/room/urlcheck로 post요청을 보낸다.
  3. server는 Math.random().toString(36).substr(2, 11)을 이용해 랜덤한 방주소를 만든다.
    3-1. 생성한 방주소가 roomURL(생성된 방 주소를 관리하는 set)에 있는 값이라면 while문을 돌며 재생성한다.
  4. 서버가 생성한 방주소를 client에게 보내주고, client는 history API의 push를 사용하여 플레이룸 페이지에 입장한다.
  5. 입장 후 클라이언트는 socket.emit("joinRoom", {roomID: roomID})를 보낸다.
  6. 서버는 socket.join(res.roomID);으로 해당 클라이언트의 소켓을 해당 roomID의 방에 넣는다.
  7. 이후 서버는 io.sockets.in(res.roomID).emit이나 socket.broadcast.to(res.roomID).emit 등으로 해당 방의 클라이언트들에게 메시지를 전송할 수 있다.

퍼즐의 이동 통신하기

  • 사용자가 퍼즐을 mouseDown-mouseDrag를 한 이후 mouseUp을 할 때 클라리언트는socket.emit("tilePosition")을 한다.
config.groupTiles.forEach((gtile, idx) => {
  if (gtile[0] === tile) { //타일 한장을 놓았을 때
    if (gtile[1] === undefined) {
      socket.emit("tilePosition", {
        roomID: roomID,
        tileIndex: idx,
        tilePosition: gtile[0].position,
        tileGroup: gtile[1],
        changedData: gtile[0],
      });
    } else { //그룹화된 타일을 놓았을 때
      config.groupTiles.forEach((tileNow, index) => {
        if (gtile[1] === tileNow[1]) {
          socket.emit("tilePosition", {
            roomID: roomID,
            tileIndex: index,
            tilePosition: tileNow[0].position,
            tileGroup: tileNow[1],
            changedData: tileNow[0],
          });
        }
      });
    }
  }
});
  • 퍼즐 그룹이 합쳐지고, 퍼즐 각각의 위치가 보정되었을 때 클라이언트는 socket.emit("groupIndex")를 한다.
socket.emit("groupIndex", {
  roomID: room,
  groupTileIndex: config.groupTileIndex,
});
  • 서버는 각 방별 PuzzleInfo들을 저장하고 업데이트한다. 중간에 들어온 사용자들이 현재 시점에서 플레이할 수 있도록 하기 위함이다. 서버는 client에게 위의 메세지를 받으면 서버의 퍼즐 정보를 업데이트하고 해당 방의 클라이언트들에게 브로드캐스트를 하여 각 클라이언트들이 변경된 퍼즐 위치를 볼 수 있도록 한다.

  • 클라이언트가 "groupIndex"와 "tilePosition" 소켓 이벤트를 받을 때 (on), 클라이언트 측 퍼즐의 position 값을 변경한다.

0개의 댓글