본문 바로가기
프로젝트

24/11/20 ~ 21 - [팀] 최종: 트러블 슈팅 - 방 나가기 noti 전달 오류

by Jini_Lamp 2024. 11. 20.

문제 상황

방을 빠져나갔을 때, 방에 남아있는 유저들에게 누가 방에서 나갔는지 알려줘야 한다.

그러나 leaveRoomNotification 패킷을 클라이언트에게 전달하고, 클라이언트는 이를 제대로 받았음에도 불구하고 화면 갱신이 안되는 문제가 생겼다.

클라이언트에서도 leaveRoomNotification 패킷을 받는 걸 확인

 

 

 

원인 파악

정확한 테스트를 위해 로그를 남겨보았다.

상황을 설명하자면, 1, 2, 3 유저가 있고, 그중 3번 유저가 방을 만든 상태이다. 그리고 차례대로 2, 1이 입장했으며, 이후 2번이 방을 나가고(첫번째 이미지), 다시 1번이 방을 나갔다.(두번째 이미지)

 

서버 쪽도 확인을 하면, 제대로 동작하는 걸 볼 수 있다.

 

해서, 제대로 된 원인을 파악할 수 없을 때 즈음.

다른 곳에서 발생한 오류를 해결한던 팀원 중 한 분이 다음과 같은 채팅을 남겼다.

에이.... 설마....

 

하여, 패킷 정보와 코드를 확인하면 다음과 같다.

message S2CLeaveRoomNotification { // 패킷 정보
    int64 userId = 1;
}
const leaveRoomNotification = (socket, userId, room, ownerOut) => {
  // 응답 패킷 생성
  let leaveRoomNotification;
  if (ownerOut === true) {
    // 방 주인이 나갔을 경우
    // 중략
  } else {
    leaveRoomNotification = createResponse(
      config.packet.packetType.LEAVE_ROOM_NOTIFICATION,
      socket.sequence,
      userId,
    );
  }

  // 나를 제외한 = 방에 남은 유저들에게만 알림 전송
  // 중략
};

export default leaveRoomNotification;

 

여기서 createResponse()의 첫번째 인자는 클라이언트에게 전달할 패킷 타입을 결정하고, 두번째 인자는 클라이언트와 서버가 주고받는 시퀀스, 마지막 세번째 인자가 클라이언트에게 전달할 데이터가 들어간다.

 

해서, 지금 코드에선 userId만 전달하면 되므로 세번째 인자에 그대로 userId를 사용한다. 또한 userId의 타입을 확인해도 숫자형이라는 걸 확인했다.

 

 

 

해결과정

하지만 그동안 클라이언트에게 데이터를 전달할 때, responseData라는 객체로 한번 데이터를 감싼 상대에서 값을 전달했다.

해서, 다음과 같이 코드를 수정했다.

// 응답 패킷 생성
  const responseData = {
    userId: userId,
  };

  let leaveRoomNotification;
  if (ownerOut === true) {
    // 방 주인이 나갔을 경우
    // 중략
  } else {
    leaveRoomNotification = createResponse(
      config.packet.packetType.LEAVE_ROOM_NOTIFICATION,
      socket.sequence,
      responseData,
    );
  }

 

그랬더니 드디어 클라이언트 화면이 제대로 갱신된다!

 

 

 

문제 이유

해결은 했으나, 여전히 이유는 모르겠다. 그래서 챗GPT에게 물어보니, 다음과 같은 대답을 얻었다.

 

즉, 클라이언트는 객체 형태로 값을 받을 줄 알았는데, 서버는 값만 보내서 생긴거라는 뜻이다.

좀 더 명확하게 비교하면 다음과 같다.

  • 단일 값 (문제가 되는 경우):
    서버: 123 (숫자)
    클라이언트 기대: {"userId": 123}
    클라이언트는 숫자만 받았기 때문에 이를 매핑할 수 없음.
  • 객체 (올바른 경우):
    서버: {"userId": 123}
    클라이언트 기대: {"userId": 123}
    클라이언트가 정확히 매핑 가능.

무엇보다 단일 값만 보내는 경우, 프로토콜에서는 값에 대한 메타데이터(여기서는 userId라는 키)를 포함되지 않기 때문에, 클라이언트는 전해받은 값이 무엇을 의미하는지 알 수 없게 된다.

 

 

 

앞으로 명심 할 점

이런 문제를 빨리 발견하고, 해결하기 위해선 디버깅 환경 구축과 통신 오류 추적을 체계적으로 관리해야 한다.

특히 이번 오류의 경우, 어느 쪽에서도 오류 메시지가 나타나지 않아 원인을 찾는데 시간을 잡아 먹게 됐다.

 

  • 직렬화 데이터를 한 번 감싸는 방식 유지
    문제 해결을 위해 responseData로 데이터를 한번 감싸는 방식이 유효하다는 걸 확인했으므로, 앞으로는 이 방식을 체계적으로 적용해야 한다.

 

 

 

개선된 사항

지금까지 createResponse()는 단순히 패킷에 맞게 데이터를 직렬화하는 작업만을 진행헀다.

하지만 이번 일로 데이터가 패킷 명세대로 작성되었는지 검증해야 할 필요성을 느꼈다.

 

먼저, 데이터는 객체 타입으로 들어와야 한다. 또 패킷 타입이 제대로 존재하는지도 확인해야 하고, 데이터가 패킷 타입의 필드명대로 작성되었는지도 확인해야 한다.

하여, 수정된 코드는 다음과 같다.

const findMessageSchema = (protoMessages, messageType) => {
  for (const namespace of Object.values(protoMessages)) {
    if (namespace[messageType]) {
      return namespace[messageType];
    }
  }
  return null; // 메시지를 찾지 못한 경우 null 반환
};

export const createResponse = (packetType, sequence, payloadData = {}) => {
  // 1. payloadData가 객체인지 확인
  if (typeof payloadData !== 'object') {
    throw new Error(
      `[createResponse] payloadData가 객체가 아니다. type: ${typeof payloadData}, Value: ${payloadData}`,
    );
  }

  // 2. 패킷 타입에 따른 메시지 타입 이름 가져오기
  const protoMessages = getProtoMessages();
  const messageType = PACKET_MAPS_ERROR_TEST[packetType]; // packetType에 매핑된 메시지 이름
  if (!messageType) {
    throw new Error(`[createResponse] 알 수 없는 패킷: ${packetType}`);
  }

  // 3. 메시지 스키마 찾기 (모든 네임스페이스 탐색)
  const messageSchema = findMessageSchema(protoMessages, messageType);
  if (!messageSchema) {
    throw new Error(
      `[createResponse] 메시지 유형 ${messageType}을 protoMessages에서 찾을 수 없다. 패킷 타입: ${packetType}`,
    );
  }

  // 4. 스키마 필드 검증
  const schemaFields = Object.keys(messageSchema.fields);
  const missingKeys = schemaFields.filter((key) => !(key in payloadData));
  if (missingKeys.length > 0) {
    throw new Error(
      `[createResponse] 잘못된 패킷 타입: ${packetType}, 누락된 필드 값: ${missingKeys.join(', ')}`,
    );
  }

  // 중략
};