웹소켓 구독이 무한대로 되는 문제를 해결하려면, 구독이 이미 된 상태인지 확인하고, 불필요한 구독을 방지해야 합니다. 이를 위해 useEffect 내부에서 구독 상태를 관리하고, 라운드가 변경될 때 기존 구독을 정리하는 방식으로 문제를 해결할 수 있습니다.

아래는 BattleField 컴포넌트의 수정된 코드입니다. 기존 구독을 관리하고 새 구독을 설정하는 부분을 개선했습니다.

import { useEffect, useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { CompatClient } from '@stomp/stompjs';

import { getAccessToken } from '../../../constants/api';
import { PlayersTypes, ResultTypes } from '../../../types/GameTypes';
import useUserStore from '../../../store/userStore';
import useGameStore from '../../../store/gameStore';
import useResultStore from '../../../store/resultStore';
import { isOverlap } from '../../../utils/isOverlap';

import styled from 'styled-components';
import GrayBox from '../../../components/GrayBox';
import PlayerNickname from '../../../components/PlayerNickname';
import { getBattleField } from '../../../services/game';

const BattleFieldLayout = styled.div`
  position: relative;
  width: 100%;
  height: 100%;
`;

const Dot = styled.div<{ $top: string; $left: string }>`
  position: absolute;
  top: ${(props) => props.$top};
  left: ${(props) => props.$left};
`;

type DotPosition = {
  top: string;
  left: string;
};

type BattleFieldProps = {
  client: CompatClient;
  changeToResult: () => void;
};

export default function BattleField({
  client,
  changeToResult,
}: BattleFieldProps) {
  const navigate = useNavigate();
  const ACCESS_TOKEN = getAccessToken();
  const header = {
    Authorization: `Bearer ${ACCESS_TOKEN}`,
    'Content-Type': 'application/json',
  };
  const { userId } = useUserStore().userData;
  const { gameData, setGameData } = useGameStore();
  const { setResultData } = useResultStore();

  const [dots, setDots] = useState<DotPosition[]>([]);
  const [playerList, setPlayerList] = useState<PlayersTypes[]>([]);
  const subscribedRef = useRef<Record<string, boolean>>({});

  const currentTime = new Date().toISOString();

  const subscribeToTopic = () => {
    const topic = `/topic/games/${gameData.roundId}/battle-field`;

    if (!subscribedRef.current[topic]) {
      client.subscribe(
        topic,
        (message) => {
          const fetchedData = JSON.parse(message.body);
          if (message.body === 'ROOM IS EXPIRED') {
            navigate(`/lobby`);
          } else if (fetchedData.isFinished) {
            console.log('끝났다');
            console.log(fetchedData);

            setGameData({
              ...gameData,
              roundNumber: fetchedData.roundNumber,
              nextRoundId: fetchedData.nextRoundId,
            });
            checkResult(fetchedData.results);
            changeToResult();
          } else {
            console.log(fetchedData);

            setPlayerList(fetchedData.players);
            checkAlive(fetchedData.players);
          }
        },
        header
      );
      subscribedRef.current[topic] = true;
    }
    showBattleField();
  };

  const checkAlive = (newDataList: PlayersTypes[]) => {
    newDataList.forEach((player) => {
      if (userId === player.userId && player.stamina === 0) {
        setGameData({ ...gameData, isAlive: false });
        return;
      }
    });
  };

  const checkResult = (newDataList: ResultTypes[]) => {
    newDataList.forEach((result) => {
      if (userId === result.userId) {
        setResultData(result);
        return;
      }
    });
  };

  const showBattleField = async () => {
    try {
      await getBattleField(gameData.roundId);
    } catch (error: any) {
      console.error('배틀필드 요청 에러', error);
      navigate(`/error`);
    }
  };

  useEffect(() => {
    if (client && client.connected) {
      subscribeToTopic();
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [client, gameData.roundId]);

  useEffect(() => {
    const generatedDots: DotPosition[] = [];
    const generateRandomPosition = () => ({
      top: `${Math.random() * 92}%`,
      left: `${Math.random() * 92}%`,
    });

    while (generatedDots.length < gameData.playersNumber) {
      const newPosition = generateRandomPosition();
      if (!isOverlap(newPosition, generatedDots)) {
        generatedDots.push(newPosition);
      }
    }
    setDots(generatedDots);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [gameData.roundId]);

  const handleClick = (victimId: number) => {
    if (userId && userId !== victimId) {
      const message = JSON.stringify({
        roundId: gameData.roundId,
        userId: userId,
        victimId: victimId

위 코드에서는 subscribedRef 객체를 사용하여 이미 구독된 주제를 추적하고, 새로운 라운드가 시작될 때 중복 구독을 방지합니다. 이를 통해 무한 구독 문제를 해결할 수 있습니다.

또한 **useEffect**에서 **gameData.roundId**가 변경될 때마다 subscribeToTopic 함수가 실행되도록 하여 새로운 라운드가 시작될 때 적절한 구독이 설정됩니다.


GameResult Component

GameResult 컴포넌트에서도 비슷한 방식으로 구독을 관리합니다.

import { useEffect, useState } from 'react';
import { CompatClient } from '@stomp/stompjs';
import useGameStore from '../../../store/gameStore';
import useResultStore from '../../../store/resultStore';

import { FlexLayout } from '../../../styles/layout';
import { RegularText, SmallText } from '../../../styles/fonts';
import GrayBox from '../../../components/GrayBox';
import Input from '../../../components/Input';

type GameResultProps = {
  client: CompatClient;
  isWinner?: boolean;
  changeState: () => void;
};

export default function GameResult({
  client,
  isWinner,
  changeState,
}: GameResultProps) {
  const { gameData, setGameData } = useGameStore();
  const { roundNumber } = useGameStore().gameData;
  const { killCount, rank } = useResultStore().resultData;
  const [countdown, setCountdown] = useState(30);

  const ResultText = ({
    title,
    results,
    timeText,
  }: {
    title: string;
    results: string[][];
    timeText: string;
  }) => {
    const RESULTS_LENGTH = 5;

    return (
      <FlexLayout $isCol gap={isWinner && roundNumber < 3 ? '4rem' : '6rem'}>
        <RegularText color='black'>{title}</RegularText>
        <FlexLayout $isCol gap='2.4rem'>
          {results.slice(0, RESULTS_LENGTH - 1).map((value, index) => {
            return (
              <SmallText
                key={index}
                color='black'
              >{`${value[0]} : ${value[1]}`}</SmallText>
            );
          })}
          {isWinner && roundNumber < 3 ? (
            <FlexLayout $isCol gap='1rem'>
              <SmallText color='black'>{`${
                roundNumber + 1
              } 라운드 도발 멘트 입력`}</SmallText>
              <Input name='winnerComment' width='60rem' />
            </FlexLayout>
          ) : (
            <FlexLayout gap='1.2rem'>
              <SmallText color='black'>{`${
                results[RESULTS_LENGTH - 1][0]
              } : `}</SmallText>
              <SmallText color='#725bff'>
                {results[RESULTS_LENGTH - 1][1]}
              </SmallText>
            </FlexLayout>
          )}
        </FlexLayout>
        <SmallText color='black'>{timeText}</SmallText>
      </FlexLayout>
    );
  };

  useEffect(() => {
    if (countdown === 0) {
      client.unsubscribe(`/topic/games/${gameData.roundId}/battle-field`);
      client.unsubscribe(`/topic/games/${gameData.roundId}/kill-log`);
      client.unsubscribe(`/topic/games/${game