문제 : 데이터를 전하는 방식이 될 때도 있고 안될 때도 있음
다음은 내가 강사님에게 질문한 전문이다.
안녕하세요 ! 질문이 생겨 질문 드립니다.
강사님이 알려주셨던 코드 중, 메시지를 주고 받는 코드에서 다음과 같이 익명 객체를 통해 데이터를 주고 받았던 코드가 있습니다.
public void SendMessage(string roomId, string nickName, string message)
{
_socket.Emit("sendMessage", new { roomId, nickName, message });
}
C# (유니티)
-----------------
Node.js( 서버 )
socket.on('sendMessage', function(message) {
console.log('메시지를 받았습니다: ' + message.roomId + ' ' + message.nickName + ' : ' + message.message);
socket.to(message.roomId).emit('receiveMessage', { nickName: message.nickName, message: message.message });
});
그래서 이번에도, 익명 객체를 통해 데이터를 주고받는 형식을 사용해 보려고 했습니다.
이 때 문제가 발생했습니다. 똑같이 익명 객체를 통해 데이터를 주고받는데, 하나는 작동하고, 하나는 작동하지 않는 현상이 있습니다.
작동하는 것
public void SendPlayerMove(string roomId, int position)
{
_socket.Emit("doPlayer", new { roomId, position });
Debug.Log(new {roomId, position});
}
C#
socket.on('doPlayer', (data) => {
console.log(`${data.position}에 돌을 두었습니다.`);
socket.to(data.roomId).emit('doOpponent', { position: data.position });
});
Node.js
작동하지 않는 것 .
public void RequestMatch(int grade)
{
string roomId = "abc";
_socket.Emit("joinMatch" , new { grade ,roomId });
}
socket.on('joinMatch', (data)=> {
console.log('사용자가 매칭을 시작했습니다. 데이터 타입:', typeof data);
var grade = data.grade;
const matchedRoom = findMatchingRoom(grade);
혹시 익명 객체의 변수가 하나라서 안되는건가 ? 싶어서 하나를 더 넣어봐도 안되고, grade를 string값으로 변경해 봐도 안되었습니다. 뭐가 다른지 잘 모르겠어서 질문 드립니다.
여러 AI에게 물어보았을 때 '운이 좋아서' 라고 나오길래 정확한 원리를 알고 싶습니다 !
'운이 좋다'는 말이 안된다고 생각했다.
물론 멀티스레드 환경에서는 문제가 생길 수도 있지만, 기본적으로 유니티는 싱글 스레드 환경이기 때문에 그 가능성이 낮다고 보았다.
멘토님께서 내가 '데이터 형식'에 매몰되어서 보지 못하고 있던 부분을 몇 가지 알려주셨다.
1. 데이터의 형식이 올바르지 않다면 , 계속해서 읽어서 실행이 되지 않을 수도 있다.
2. 이벤트 이름들이 캐시에 남아 있어서 문제가 있을 수도 있다 ( 서버를 강제 종료하는 방식으로 종료하기 때문 )
3. 동시에 넘어와서 문제가 생길 수도 있다. ( connect가 보장되지 않은 상태에서, emit을 해 버리면 문제가 생길 수 있다)
나는 보낸 데이터를 제대로 읽지 못하는 JS만의 문제라고 보았는데, 클라이언트 쪽에서도 문제가 있을 수 있었다.
결국 문제는 3번 Connect가 보장되지 않은 상태에서 emit을 같은 프레임에서 진행해서 올바르게 호출하지 못한 문제와
Unity에서 보낸 데이터 형식에서 올바르게 데이터를 추출하지 못해 발생한 문제였다.
문제의 클라이언트쪽 코드를 보자면,
public GameLogic(BlockController blockController,GameUIController gameUIController,
Constants.GameType gameType)
{
// 보드 초기화 등 기초 작업 (생략)
case Constants.GameType.MultiPlayer:
{
//_multiplayManager.RequestMatch();
Debug.LogFormat("Set-up Multiplayer Manager");
_multiplayManager = new MultiplayManager((state, roomId) =>
{
//멀티플레이 매니저 관련 작업 ( 생략)
});
_multiplayManager.RequestMatch(18);
}
}
기본적으로 씬이 로드되면서 GameLogic이 생성되고, GameLogic이 생성되며 만일 게임타입이 멀티플레이라면
멀티플레이매니저가 생성된다. 이 때 멀티플레이 매니저는 자연스럽게 socket을 연결하게 된다.
public MultiplayManager(Action<Constants.MultiplayManagerState, string> onMultiplayStateChanged)
{
_onMultiplayStateChanged = onMultiplayStateChanged;
var uri = new Uri(Constants.GameServerURL);
_socket = new SocketIOUnity(uri, new SocketIOOptions
{
Transport = SocketIOClient.Transport.TransportProtocol.WebSocket
});
_socket.On("createRoom", CreateRoom);
_socket.On("joinRoom", JoinRoom);
_socket.On("startGame", StartGame);
_socket.On("exitRoom", ExitRoom);
_socket.On("endGame", EndGame);
_socket.On("doOpponent", DoOpponent);
_socket.On("getPlayerInfo", GetPlayerInfo);
_socket.On("timeOutOpponent",TimeOutOpponent);
_socket.Connect();
Debug.LogFormat("Socket connect ok");
}
그리고 같은 프레임에서 _multiplayManager.requestMatch(18)을 요청하게 된다.
즉, 아직 Connect();가 완료되었는지 보장되지 않은 상태에서 요청을 하게 되어서 올바르게 JSON 객체/ 파라미터들을 올바르게 전달하지 못하는 것이었다.
그럼 왜 ! JsonConvert.SerializeObject()로 바꿔서 했을 때는 똑같이 동시에 실행하는데( Connect();가 완료된 것을 보장하지 않은 상태) 올바르게 실행되었을까 ? (다음은 실제 똑같이 실행했는데 잘 작동하던 부분)
//이게 잘 작동하는 부분
//var matchData = new MatchData { grade = grade };
// _socket.Emit("joinMatch", JsonConvert.SerializeObject(matchData));
JsonConvert.SerializeObject()가 CPU 연산이 들어가는 동기 작업이기 때문이다.
따라서 실제 Emit 호출이 객체 생성 -> JSON 직렬화 -> 이후 Emit 호출 되는 과정을 지나
실제 호출 시간이 늦어지기 때문이다. (실제 Emit 호출 시점이 자연스럽게 뒤로 미뤄져 리스너 등록 이후에 실행되도록 만들어주는 완충 역할을 한다)
JsonConvert를 사용해서 직렬화 하는 과정 없이도 올바르게 실행되게 하기 위해 Emit() 호출 시점 자체를 뒤로 미뤄서
해결하는 방법도 있다.
GameLogic 자체는 MonoBehaviour가 아니기 때문에 코루틴을 돌릴 수 없다. 따라서 따로 만들어준다. (대리실행)
GameManager Class에 만들어준다( 싱글톤 객체로써 접근이 편하기 때문)
public void OnDelegate(Action action, float time)
{
StartCoroutine(DoDelegate(action,time));
}
IEnumerator DoDelegate(Action action, float time)
{
yield return new WaitForSeconds(time);
action?.Invoke();
}
그리고 GameLogic에서 실행할 때 바로 실행하지 않고, 딜레이를 주어 실행하면 올바르게 작동한다.
GameManager.Instance.OnDelegate(() =>
{
_multiplayManager.RequestMatch(18);
}, 0.1f);
두 번째 문제는 데이터 형식의 문제이다. 모든 데이터를 올바르게 (알아서 잘 ) 받아오지 않는다.
이를 실험해 보기 위해 다양한 코드를 짜 봤다.
socket.on('joinMatch', (grade) => {
console.log('사용자가 매칭을 시작했습니다'+ grade);
});
// { Set-up Events, JS Side
socket.on('joinMatch001', (param1) => {
grade = parseInt(param1);
console.log("[joinMatch001] Grade: " + grade);
});
socket.on('joinMatch002', (param1) => {
matchData_ = param1;
console.log("[joinMatch002] Grade: " + matchData_.grade);
});
socket.on('joinMatch003', (param1, param2, param3) => {
console.log("[joinMatch003] Param1: " + param1 + ", Param2: " + param2 + ", Param3: " + param3);
});
socket.on('joinMatch004', function(param1, param2, param3){
grade = param1;
nickNameStr = param2;
rankStr = param3;
console.log("[joinMatch004] grade: " + grade + ", nickNameStr: " + nickNameStr + ", rank_: " + rankStr);
});
socket.on('joinMatch005', function(params){
nickName_ = params.nickNameStr;
grade_ = params.grade;
rank_ = params.rankStr;
console.log("[joinMatch005] nickName_: " + nickName_ + ", grade_: " + grade_ + ", rank_: " + rank_);
});
socket.on('joinMatch006', function(param1){
matchData_ = param1;
console.log("[joinMatch006] Grade: " + matchData_.grade);
});
socket.on('joinMatch007', function(param1){
matchData_ = param1;
console.log("[joinMatch008] Grade: " + matchData_.grade + " rankeStr : "+ matchData_.rankStr);
});
socket.on('joinMatch008', function(param1){
matchData_ = param1;
console.log("[joinMatch008] Grade: " + matchData_.grade + " rankeStr : "+ matchData_.rankStr + " nickNameStr :"+ matchData_.nickNameStr );
});
var matchData = new MatchData { grade = grade };
// _socket.Emit("joinMatch", JsonConvert.SerializeObject(matchData));
// 기본적인 grade 테스트
_socket.Emit("joinMatch" ,new{ grade});
string nickNameStr = "Hello World";
string rankStr = (4.5).ToString();
_socket.Emit("joinMatch001", grade);
_socket.Emit("joinMatch002", matchData);
_socket.Emit("joinMatch003", 10, "Hello world", 0.001f);
_socket.Emit("joinMatch004", new {grade, nickNameStr, rankStr});
_socket.Emit("joinMatch005", new {nickNameStr, grade, rankStr});
_socket.Emit("joinMatch006", new { grade });
_socket.Emit("joinMatch007", new { grade , rankStr });
_socket.Emit("joinMatch008", new { grade , rankStr , nickNameStr });
결과는 차례대로 다음과 같다.
사용자가 매칭을 시작했습니다[object Object]
[joinMatch001] Grade: 18
[joinMatch002] Grade: undefined
[joinMatch003] Param1: 10, Param2: Hello world, Param3: 0.00100000005
[joinMatch004] grade: [object Object], nickNameStr: undefined, rank_: undefined
[joinMatch005] nickName_: Hello World, grade_: 18, rank_: 4.5
[joinMatch006] Grade: 18
[joinMatch008] Grade: 18 rankeStr : 4.5
[joinMatch008] Grade: 18rankeStr : 4.5 nickNameStr :Hello World
하나씩 해석해보자면,
1. joinMatch
new {grade} 는 익명 객체로 { grade: 18} 형태가 전송된다.
JS에서는 grade 변수로 받지만, 실재로 객체 전체를 받기 때문에 이 객체를 문자열로 출력하려니 [object Object]가 되었다.
* 해결하려면 JSON.stringfy(grade)를 해야 한다.
2. joinMatch001
grade가 그냥 숫자(int)로 보내주었고, JS에서도 그대로 받아 주어서 정상 출력된다.
3. joinMatch002
( { grade: 18}) 인 matchData로 보내주었다(객체), Unity에서 직렬화 없이 보냈기 때문에
JS에서는 완전히 파싱되지 않을 수 있어, undefined가 나왔다.
( SocketIO 내부에서 JSON 변환 과정을 거쳐 전달되지만, 이 구조가 JS에서 예상하는 것과 다르다면
정상적으로 접근이 되지 않는다 )
(기본적으로 string, int 같이 자동 파싱이 되는 것은 괜찮지만, 나머지는 직렬화 해서 보내주어야 한다)
4. joinMatch003
각각 int, string, float를 따로 전송했다. JS에서도 각 파라미터를 정확히 받아주었다.
5. joinMatch004
하나의 객체에 { grade, nickNameStr, raneStr } 을 담아서 보냈다, 다만 받을 때 세 개로 나누어 받으려 해서 문제가 발생한다. 결국 param1에는 { grade, nickNameStr, raneStr } 객체가 통째로 들어가고, 나머지 param2와 param3는 전달되지 않아 undefined가 나온다.
6. joinMatch005
4번에서 한 것의 순서만 바꿔주었다. 정상 작동된다.
7. joinMatch006
matchData라는 객체 안에 garde 하나만 들어 있다, 잘 받아진다.
8. joinMatch007
객체 안에 grade와 rankStr이 있고, 알맞게 잘 받았다.
9. joinMatch008
마찬가지로 잘 받아진다.
결론적으로, 보자면 Emit을 할 때 , 인자를 명확히 전달해 주어야 한다.
객체 형태로 보냈으면, 받을 때에도 객체를 분해해서 사용해야 하고 ( 인자를 하나만 받아서 ) ,
여러 개의 인자를 나눠서 보냈다면 (JSON으로 자동 변환이 가능한) 나눠진 인자를 받아서 사용하면 된다.
- Emit(new { a, b, c}) -> JS에서 하나의 param으로 받고 param.a / param.b 등으로 접근
- Emit( " event", a,b,c) -> JS에서도 (a,b,c)로 받아야 함
- 직렬화가 없어도 괜찮지만, 구조 맞추기 어려우면 직렬화를 직접 보장하는 것이 좋음
- Emit() 시점은 항상 connect이후가 보장되어야 안전함
이렇게 간단해 보이는 것을 왜 오래 고민했냐면, 결국 이 데이터 형식에 집착해서 다른쪽 클라이언트 문제일 것이라고는 생각 못했기 때문이다.
앞으로는 , 문제가 있을 때 조금 더 넓은 시선으로 바라봐야 문제를 해결할 수 있을 것이다.
'시행착오' 카테고리의 다른 글
02.25 회고 + Scroll View 응용 (0) | 2025.02.26 |
---|---|
Scroll View With ObjectPool (0) | 2025.02.25 |
애니메이션 이벤트 + DOTween 회전 실행 오류 // 02/21 수정(Root Motion) (0) | 2025.02.20 |