Skip to content

Conversation

kim-hyunjoo
Copy link
Contributor

@kim-hyunjoo kim-hyunjoo commented Oct 13, 2025

📝작업 내용

기존 구조의 문제점

// GameRoomController.tsx

useEffect(() => {
    sendMessage({
      destination: `${SOCKET.ROOM.CHANGE_PLAYER_NAME}`,
      body: {
        name: myName,
      },
    });
  }, [myName]);

  useEffect(() => {
    sendMessage({
      destination: `${SOCKET.ROOM.CHANGE_GAME}`,
      body: {
        gameId,
      },
    });
  }, [gameId]);
  • 현재 모달에 직접 전달해줄 수 있는 optionNumberprops의 타입은 string | number이기 때문에, 모달 내부에서 직접 sendMessage를 사용할 수 없었습니다.
  • 따라서 GameRoom에서 useEffect의 의존성 배열에 myName, gamdId를 넣어 간접적으로 변경을 감지하여 sendMessage를 통해 소켓 통신을 하도록 설계했습니다.
  • 그러나 해당 코드는 처음 GameRoom이 렌더링 될 때(방에 처음 입장했을 때)도 발동되기 때문에, 종종 의도하지 않은 소켓 통신이 일어났습니다.
image image

현재 발견된 에러 상황

  • 카드 게임을 제외한 다른 게임에서 게임 변경하기를 통해 카드 게임으로 변경
  • 현재 상황은 소켓에 연결된 상태에서 게임만 변경하고 있으므로 sendMessage가 유효함
  • 여기서 카드 게임이 서버에는 존재하지만 아직 클라이언트 개발이 진행되지 않아 GamePanel에서 에러 발생
  • 그러나 GameRoom컴포넌트는 상위 컴포넌트이므로 렌더링에 문제x, 정상적으로 렌더링됨
  • GameRoom 렌더링 중 useEffect문의 sendMessage가 발동하여 게임 변경, 닉네임 변경 로직 각각 2번씩 수행(react의 strictMode때문)
  • GamePanel의 에러로 홈으로 이동
  • 이때 원래는 토스트 메세지에 카드 플립 게임이 없다고 떠야하지만, 닉네임 변경 로직이 수행되었기 때문에 사용자에게 엉뚱한 경고 메세지가 뜸

개선된 구조

  • useSocketStore 생성, 소켓 연결 시 sendMessage 함수 저장
  • 게임 변경 모달(GameListCarousel), 닉네임 변경 모달(CreateUserNameModal)에서 직접 store에 저장된 sendMessage를 통해 핸들러 함수에서 소켓 통신하도록 변경
  • 기존 useEffect문 삭제
  • 사용되지 않는 store의 set함수 삭제
image
  • 사용자에게 올바른 에러 토스트 메세지가 뜹니다.

✨PR Point

@kim-hyunjoo kim-hyunjoo self-assigned this Oct 13, 2025
@kim-hyunjoo kim-hyunjoo added ✨기능 새로운 기능 추가 🐛버그 버그 수정 ♻️리팩토링 코드 리팩토링 (성능 및 코드 로직 개선) labels Oct 13, 2025
Copy link

Summary of Changes

Hello @kim-hyunjoo, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

이 Pull Request는 모달 내 소켓 통신 방식의 근본적인 문제를 해결하고, 전반적인 컴포넌트 구조를 개선하여 코드의 유지보수성과 확장성을 향상시키는 것을 목표로 합니다. 기존에는 GameRoom 컴포넌트의 useEffectmyName이나 gameId 변경에 반응하여 불필요하거나 의도치 않은 소켓 메시지를 전송하는 문제가 있었습니다. 이를 해결하기 위해 sendMessage 함수를 전역적으로 관리하는 useSocketStore를 도입하여 모달에서 직접 소켓 통신을 수행하도록 변경했습니다. 또한, 게임 관련 로직과 UI를 분리하고 공통 컴포넌트를 재구성하여 코드의 응집도를 높이고 가독성을 개선했습니다.

Highlights

  • 소켓 통신 로직 개선: 모달 내에서 직접 소켓 통신을 할 수 있도록 useSocketStore를 생성하고 sendMessage 함수를 저장하여, 기존 GameRoom 컴포넌트의 useEffect에서 간접적으로 소켓 통신을 처리하던 로직을 제거했습니다. 이로 인해 의도치 않은 소켓 통신 발생 문제를 해결했습니다.
  • 컴포넌트 구조 리팩토링: 게임 관련 컴포넌트들을 BalanceGamePanel, QnaGamePanel, PreGame, GameActionButtons 등으로 분리하고, 공통으로 사용되는 컴포넌트들을 Common 디렉토리로 이동하여 모듈성과 재사용성을 높였습니다.
  • 상태 관리 최적화: useRoomStore, useBalanceGameStore, useQnaGameStore에서 더 이상 필요 없는 setGameIdsetTotalRounds 액션을 제거하여 스토어의 불필요한 상태를 줄였습니다.
  • 게임 종료 자동화 및 유틸리티 추가: 최소 인원 미달 시 게임을 자동으로 종료하는 useAutoEndGame 훅을 추가하고, 게임 유형을 매핑하는 gameComponentMap, gameControlMap, gameTypeMap 및 게임 시작 핸들러를 중앙화하는 gameStartHandlers 유틸리티를 도입하여 게임 로직 관리를 개선했습니다.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

이번 PR은 useSocketStore를 도입하여 모달 내에서 직접 소켓 통신을 하도록 구조를 개선한 점이 인상적입니다. 이를 통해 불필요한 useEffect를 제거하고 의도하지 않은 소켓 통신 문제를 해결한 것은 매우 좋은 접근입니다. 또한, 컴포넌트 구조를 Common 디렉토리 중심으로 대대적으로 리팩터링하여 코드의 재사용성과 유지보수성을 높인 점도 훌륭합니다.

전반적으로 코드 품질을 크게 향상시키는 변경이라고 생각합니다. 다만, 리팩터링 과정에서 한 가지 심각한 문제가 발견되었습니다. GameActionButtonsController 컴포넌트에서 React 훅을 조건부로 호출하고 있어 런타임 에러를 유발할 수 있습니다. 이 부분에 대한 수정 제안을 포함하여 몇 가지 개선 사항을 리뷰 코멘트로 남겼으니 확인 부탁드립니다.

Comment on lines 19 to 56
const GameActionButtonsController = ({
game,
isRoomManager,
sendMessage,
}: GameActionButtonsControllerProps) => {
const gameType = gameToType(game);
const { roomStatus } = useRoomStore();

const gameControl = gameType ? GAME_CONTROL_MAP[gameType] : null;

const { round } = gameControl?.useStore() || { round: null };

if (!gameControl || !round || !isRoomManager) return null;

const roundEndActionText =
round.currentRound === round.totalRounds
? '최종 결과 보기'
: '다음 라운드로 이동';
const nextDestination = gameControl.nextDestination;
const endDestination = gameControl.endDestination;

const handleEnterNextRound = () => {
sendMessage({ destination: nextDestination });
};

const handleMoveToWaitingRoom = () => {
sendMessage({ destination: endDestination });
};

return (
<GameActionButtonsView
roomStatus={roomStatus}
buttonText={roundEndActionText}
handleEnterNextRound={handleEnterNextRound}
handleMoveToWaitingRoom={handleMoveToWaitingRoom}
/>
);
};

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

현재 구현에서는 gameControl?.useStore()를 통해 조건부로 훅을 호출하고 있습니다. 이는 React의 훅 규칙(Rules of Hooks)에 위배되며, 렌더링 시점에 따라 훅 호출 순서가 달라져 런타임 에러를 발생시킬 수 있습니다. 훅은 항상 컴포넌트의 최상위 레벨에서 호출되어야 합니다. 이 문제를 해결하기 위해 훅 호출 로직을 별도의 내부 컴포넌트로 분리하고, 부모 컴포넌트에서는 조건부로 이 내부 컴포넌트를 렌더링하는 방식으로 리팩터링하는 것을 제안합니다.

const GameActionButtonsContent = ({
  gameControl,
  isRoomManager,
  sendMessage,
}: {
  gameControl: (typeof GAME_CONTROL_MAP)[keyof typeof GAME_CONTROL_MAP];
  isRoomManager: boolean;
  sendMessage: GameActionButtonsControllerProps['sendMessage'];
}) => {
  const { roomStatus } = useRoomStore();
  const { round } = gameControl.useStore();

  if (!round || !isRoomManager) return null;

  const roundEndActionText =
    round.currentRound === round.totalRounds
      ? '최종 결과 보기'
      : '다음 라운드로 이동';
  const nextDestination = gameControl.nextDestination;
  const endDestination = gameControl.endDestination;

  const handleEnterNextRound = () => {
    sendMessage({ destination: nextDestination });
  };

  const handleMoveToWaitingRoom = () => {
    sendMessage({ destination: endDestination });
  };

  return (
    <GameActionButtonsView
      roomStatus={roomStatus}
      buttonText={roundEndActionText}
      handleEnterNextRound={handleEnterNextRound}
      handleMoveToWaitingRoom={handleMoveToWaitingRoom}
    />
  );
};

const GameActionButtonsController = ({
  game,
  isRoomManager,
  sendMessage,
}: GameActionButtonsControllerProps) => {
  const gameType = gameToType(game);
  const gameControl = gameType ? GAME_CONTROL_MAP[gameType] : null;

  if (!gameControl) {
    return null;
  }

  return (
    <GameActionButtonsContent
      gameControl={gameControl}
      isRoomManager={isRoomManager}
      sendMessage={sendMessage}
    />
  );
};

Comment on lines 49 to 53
useEffect(() => {
if (sendMessage) {
setSendMessage(sendMessage);
}
}, [sendMessage, setSendMessage]);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

setSendMessage 함수는 Zustand store에서 반환된 setter 함수로, 컴포넌트의 생명주기 동안 항상 동일한 참조를 유지합니다. 따라서 useEffect의 의존성 배열에 포함하지 않아도 괜찮습니다. 의존성 배열을 간소화하여 코드 가독성을 높이고 불필요한 재실행 가능성을 줄일 수 있습니다.

  useEffect(() => {
    if (sendMessage) {
      setSendMessage(sendMessage);
    }
  }, [sendMessage]);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

완료

Comment on lines 55 to 62
useEffect(() => {
if (roomDetail.players.length > 0) {
const host = roomDetail.players.find((player) => player.isHost);
if (host) {
setHostName(host.name);
}
}
}, [roomDetail.players, setHostName]);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

setHostName 함수 또한 Zustand store의 setter 함수이므로 항상 동일한 참조값을 가집니다. useEffect의 의존성 배열에서 제거하여 코드를 더 명확하게 만들 수 있습니다.

  useEffect(() => {
    if (roomDetail.players.length > 0) {
      const host = roomDetail.players.find((player) => player.isHost);
      if (host) {
        setHostName(host.name);
      }
    }
  }, [roomDetail.players]);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

완료

@ksjdev ksjdev changed the base branch from dev to GRPHI-227-refactor/game-component-branching October 14, 2025 12:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

♻️리팩토링 코드 리팩토링 (성능 및 코드 로직 개선) ✨기능 새로운 기능 추가 🐛버그 버그 수정

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

[GRPHI-237] 모달 내 소켓 통신을 위한 useSocketStore 생성 및 적용

1 participant