콘텐츠로 이동

메신저 웹뷰 H5 게임용 원격 치트 콘솔

메신저 플랫폼의 인스턴트 게임으로 출시한 H5 게임은 대부분 모바일 메신저 앱의 웹뷰 안에서 실행된다. 이 환경에서는 브라우저 개발자 도구를 열 수 없어 디버깅이 어렵고, QA 중에 게임 상태를 바꿔 볼 방법도 없다. 그래서 웹 브라우저에서 원격으로 게임을 조작하는 치트 콘솔을 만들었다.

원격 환경과 실시간으로 주고받아야 하므로 통신은 채팅 구현에 많이 쓰는 이벤트 기반 WebSocket 라이브러리인 socket.io(당시 2.x)를 썼다.

구성 요소 역할
게임 클라이언트 빌드 때 넣어 둔 치트 서버 주소로 접속한다. 자기 상태 데이터를 서버로 보내고, 받은 액션을 실행한다.
프록시 서버 Node.js(express) 위에 socket.io를 올린 서버다. 접속한 클라이언트 목록을 들고, 콘솔과 클라이언트 사이의 메시지를 양방향으로 중계한다.
운영자 콘솔 React로 만든 웹 UI다. 클라이언트를 고르고, 클라이언트가 보낸 데이터를 보여 주고, 미리 정의한 액션 버튼을 제공한다.
  1. 게임 클라이언트가 실행되면 치트 서버에 접속한다. 서버는 클라이언트를 구분할 식별자를 만들어 토큰으로 내려 준다.
  2. 서버는 접속한 클라이언트 목록을 유지한다.
  3. 운영자가 콘솔에 접속하면 서버는 아직 콘솔과 짝을 이루지 않은 클라이언트 목록을 보여 준다.
  4. 콘솔에서 클라이언트를 고르면 콘솔과 클라이언트가 짝으로 묶인다. 이후 클라이언트가 보내는 데이터가 콘솔에 표시된다.
  5. 콘솔에서 액션 버튼을 누르면 액션과 관련 데이터가 서버를 거쳐 클라이언트로 간다.
  6. 클라이언트는 자기 코드에 구현된 해당 액션을 실행한다. 실제 치트 효과는 클라이언트가 게임 서버에 보내는 요청으로 일어난다.
  • 치트 로직은 클라이언트에 둔다. 콘솔은 실행할 수 있는 액션 목록과 클라이언트가 보낸 데이터를 보여 주기만 한다.
  • 치트 효과는 게임 서버의 정상 요청 경로를 탄다. 콘솔이 게임 서버에 직접 접근하지 않는다.
  • 콘솔 UI는 개발용이라 최소한으로 만들었다. 목적을 달성하는 데 필요한 화면만 둔다.
  • 로컬 개발에서는 개발 서버 스크립트가 치트 서버를 함께 띄운다. dev·QA 환경의 치트 서버는 CI 작업으로 따로 띄운다.

콘솔과 클라이언트의 일대일 중계는 socket.io의 room으로 표현할 수 있다.

  • 서버의 각 소켓은 자기 id와 같은 이름의 room에 자동으로 들어간다. 그래서 특정 소켓 하나에만 보낼 때 io.to(socketId).emit(...)을 쓸 수 있다(Server API: socket.id). socket.id는 재접속할 때마다 바뀌는 임시 값이므로, 콘솔에 보여 줄 클라이언트 식별자는 서버가 따로 발급한 토큰을 쓴다.
  • room은 서버에만 있는 개념이다. 클라이언트는 자기가 어느 room에 들어가 있는지 모른다(Rooms). 짝 정보는 서버가 관리한다.
  • 콘솔용과 게임 클라이언트용 연결을 나누고 싶으면 namespace(io.of('/console') 같은 경로 단위의 소켓 묶음)를 쓸 수 있다(Server API: Namespace).
// 서버: 콘솔이 고른 클라이언트에게 액션을 넘기고, 실행 결과를 콘솔로 되돌린다
consoleSocket.on('action', ({ token, name, payload }, reply) => {
const clientSocketId = clientsByToken.get(token);
io.to(clientSocketId).timeout(5000).emit('action', { name, payload }, (err, results) => {
reply(err ? { ok: false } : results[0]);
});
});

emit의 마지막 인자로 함수를 넘기면 상대가 처리한 뒤 그 함수를 호출해 응답을 돌려준다. socket.io는 이것을 acknowledgement라고 부른다. 응답 대기 시간 제한(timeout())은 v4.4.0부터 쓸 수 있다(Emitting events: Acknowledgements). room으로 브로드캐스트하면서 ack를 받으면 콜백은 클라이언트별 응답 배열을 받는다(Emit cheatsheet).