🤖
1773 in / 2000 out / 3773 total tokens
브릿지 크로스 게임에서 호스트가 시작 버튼을 누르기 전, "베팅 1/2명" 같은 숫자만 보니까 도대체 누가 안 했는지 모르겠더라. 그래서 베팅 안 한 사람 이름을 버튼에 바로 띄우도록 바꿨다.
서버 측에서는 bridge-cross:selectionCount 이벤트의 payload에 bettorNames 배열을 추가했다. 기존엔 count: Object.keys(bc.userColorBets).length만 보냈는데, 여기에 bettorNames: Object.keys(bc.userColorBets)를 같이 실어서 보낸다. 서버 입장에선 userColorBets 객체의 키가 곧 베팅한 유저의 이름이니까 Object.keys() 한 번이면 끝이다. 변경량 +3/-2로 거의 비용이 안 든다.
클라이언트는 조금 더 손이 갔다. 전역 변수로 currentBettorNames = []를 추가하고, selectionCount 이벤트 수신 시 이 배열을 업데이트한다. 핵심 로직은 updateStartButton() 안에 있다. 전체 유저 이름 배열에서 currentBettorNames에 포함되지 않은 사람을 filter로 걸러내서 nonBettors를 계산한다. 그리고 상황에 따라 버튼 텍스트를 다르게 조합한다.
케이스가 세 가지다. 베팅이 2명 미만이고 미참여자가 있으면 "베팅 안 함: A, B"로 누가 안 했는지 직접 보여준다. 2명 이상인데 아직 안 한 사람이 있으면 호스트가 판단할 수 있도록 여전히 이름을 보여주되, 시작은 가능하게 한다. 전원 다 했으면 "전원 베팅"으로 깔끔하게 정리. 그리고 4명 이상은 "A, B, C 외 N명"으로 잘라서 버튼 텍스트가 너무 길어지지 않게 했다.
사소해 보이는 변경인데 UX 관점에서는 꽤 차이가 난다. 예전엔 "1/2명" 보고 "음 누가 안 했지?" 하면서 채팅창이나 참여자 목록을 확인해야 했다. 이제 버튼 하나로 바로 알 수 있으니 호스트의 멘탈 피로도가 줄어든다. 실시간 멀티플레이어 게임에서 이런 피드백 루프를 짧게 만드는 게 중요하다. 유저가 상황을 파악하기 위해 추가 행동을 해야 하면 그 순간 이탈이나 불만이 생긴다.
구현하면서 한 가지 고민했던 건 nonBettors 계산을 클라이언트에서 할지 서버에서 할지였다. 서버에서 nonBettors를 계산해서 보내면 클라이언트는 그냥 표시만 하면 되니까 더 단순하다. 근데 서버는 users 배열에 접근해야 하고, room context에서 유저 목록을 또 뒤져야 한다. 반면 클라이언트는 이미 users 배열을 들고 있으니까 filter 한 번이면 된다. 그래서 서버는 베팅한 사람 이름만 주고, 차집합 계산은 클라이언트에 맡기는 구조를 선택했다. 책임 분산 측면에서도 맞고, 서버 부하 측면에서도 이득이다.
javascript const allUserNames = (users || []).map(u => u.name); const nonBettors = allUserNames.filter(n => !currentBettorNames.includes(n));
이런 식으로 클라이언트에서 차집합을 계산한다. users가 null일 수 있어서 || []로 방어 처리. UE5 C++에서 TArray로 비슷한 짓 할 때는 FilterByPredicate 쓰면 되는데, JS는 filter가 더 직관적이다.
bettingReady 핸들러에서 currentBettorNames를 빈 배열로 리셋하는 것도 잊으면 안 된다. 게임이 한 판 끝나면 상태를 깨끗하게 초기화해야 다음 판에서 이전 베팅자 이름이 잔류하는 버그가 안 생긴다. UE5에서도 게임 스테이트 리셋할 때 TArray Empty() 호출하는 거랑 같은 원리다.
전체 변경량 +20/-4. 겨우 16줄 추가한 건데 체감 품질은 꽤 올라간다. 이런 식의 작은 UX 개선이 게임의 완성도를 결정한다고 생각한다.
숫자는 상태를 알려주고, 이름은 행동을 유도한다.