webSocket은 웹 페이지의 한게에서 벗어나 실시간으로 상호작용하는 웹 서비스를 만드는 HTML5 표준 기술이다.
전형적인 브라우저 렌더링 방식에서 벗어나 실시간으로 사용자와 상호작용하는 방식이 나타나고 사용자와 상호작용하는 웹 서비스를 선호하는 사용자가 증가하며 RIA(Rich Internet Application) 기술의 발달이 촉진되었다. Long Polling, Stream 등의 방식은 브라우저가 HTTP 요청를 보내고 웹 서버가 이 요청에 대한 HTTP 응답를 보내는 단방향 메세지 교환 '규칙'을 변경하지 않고 구현한 방식이라 복잡하고 어려운 코드로 상호작용을 구현된다.
보다 쉽게 상호작용하는 웹 페이지를 위해 HTML5 표준안의 일부로 WebSocket API(이후 WebSocket)가 등장했다.
socket.io는 node.js 기반으로 만들어진 기술로, 웹소켓이 HTML5의 기술이기에 오래된 버전의 웹 브라우저는 웹소켓을 지원하지 않는 반면, socket.io는 거의 모든 웹 브라우저와 모바일 장치를 지원하는 실시간 웹 애플리케이션 지원 라이브러리이다.
WebSocket | Socket.io |
---|---|
HTML5 웹 표준 기술 | 표준 기술이 아니며 라이브러리임 |
매우 빠르게 동작하며 통신할 때 아주 적은 데이터를 사용함 | 소켓 연결 실패시 fallback을 통해 다른 방식으로 알아서 해당 클라이언트와 연결을 시도함 |
이벤트를 단순히 듣고 보내는 것만 가능함 | 방 개념을 이용해 일부 클라이언트에게만 데이터를 전송하는 브로드캐스팅이 가능함 |
때문에 서버와 빠른 상호작용, 다양한 웹 브라우저, 같은 이미지, 난이도의 퍼즐 플레이 페이지라도 고유한 방을 만들 수 있는 브로드캐스팅을 지원하는 socket.io를 사용했다
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를 만들기 위해선 어떻게 해야 할까? 또 해당 그룹의 멤버끼리만 통신을 하기 위해선 어떻게 소켓을 처리해야 할까?
${process.env.REACT_APP_API_URL}/room/urlcheck
로 post요청을 보낸다.Math.random().toString(36).substr(2, 11)
을 이용해 랜덤한 방주소를 만든다.socket.emit("joinRoom", {roomID: roomID})
를 보낸다.socket.join(res.roomID);
으로 해당 클라이언트의 소켓을 해당 roomID의 방에 넣는다.io.sockets.in(res.roomID).emit
이나 socket.broadcast.to(res.roomID).emit
등으로 해당 방의 클라이언트들에게 메시지를 전송할 수 있다.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 값을 변경한다.