웹소켓 구독이 무한대로 되는 문제를 해결하려면, 구독이 이미 된 상태인지 확인하고, 불필요한 구독을 방지해야 합니다. 이를 위해 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 컴포넌트에서도 비슷한 방식으로 구독을 관리합니다.
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