Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Refactor] 프론트엔드 성능 개선 #227

Merged
merged 6 commits into from
Dec 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 2 additions & 8 deletions packages/frontend/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { RouterProvider } from 'react-router-dom';
import { router } from './routes/router';
import { useEffect } from 'react';
import AppRouter from './routes/router';

function App() {
useEffect(() => {
Expand All @@ -10,17 +9,12 @@ function App() {
};

window.addEventListener('beforeunload', handleUnload);

return () => {
window.removeEventListener('beforeunload', handleUnload);
};
}, []);

return (
<>
<RouterProvider router={router} />
</>
);
return <AppRouter />;
}

export default App;
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
11 changes: 11 additions & 0 deletions packages/frontend/src/components/common/LoadingSpinner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import React from 'react';

const LoadingSpinner: React.FC = () => {
return (
<div className="flex items-center justify-center h-screen">
<div className="w-16 h-16 border-t-2 border-b-2 rounded-full animate-spin border-primary"></div>
</div>
);
};

export default LoadingSpinner;
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Button from '@/components/common/Button';
import { memo } from 'react';

interface IVotingPhaseProps {
userId: string;
Expand All @@ -9,7 +10,33 @@ interface IVotingPhaseProps {
handleVote: () => void;
}

export default function Voting({
const VoteButton = memo(
({
userId,
isSelected,
isVoteSubmitted,
onClick,
}: {
userId: string;
isSelected: boolean;
isVoteSubmitted: boolean;
onClick: () => void;
}) => (
<button
onClick={onClick}
disabled={isVoteSubmitted}
className={`w-full p-4 text-lg font-medium transition-colors rounded-lg ${
isSelected
? 'bg-green-default text-white-default'
: 'bg-white text-gray-800 hover:bg-gray-100'
} ${isVoteSubmitted && 'opacity-60 cursor-not-allowed'}`}
>
{userId}
</button>
),
);

const Voting = memo(function Voting({
userId,
allUsers,
selectedVote,
Expand All @@ -30,19 +57,14 @@ export default function Voting({
<div className="flex flex-col items-center justify-center w-full h-full space-y-6">
<h2 className="text-2xl font-bold text-white-default">피노코를 지목해주세요!</h2>
<div className="flex flex-col w-full max-w-md space-y-3">
{Array.from(allUsers).map((userId: string) => (
<button
key={userId}
onClick={() => !isVoteSubmitted && setSelectedVote(userId)}
disabled={isVoteSubmitted}
className={`w-full p-4 text-lg font-medium transition-colors rounded-lg ${
selectedVote === userId
? 'bg-green-default text-white-default'
: 'bg-white text-gray-800 hover:bg-gray-100'
} ${isVoteSubmitted && 'opacity-60 cursor-not-allowed'}`}
>
{userId}
</button>
{Array.from(allUsers).map((voterId: string) => (
<VoteButton
key={voterId}
userId={voterId}
isSelected={selectedVote === voterId}
isVoteSubmitted={isVoteSubmitted}
onClick={() => !isVoteSubmitted && setSelectedVote(voterId)}
/>
))}
</div>
{isVoteSubmitted ? (
Expand All @@ -64,4 +86,6 @@ export default function Voting({
)}
</div>
);
}
});

export default Voting;
Original file line number Diff line number Diff line change
@@ -1,23 +1,30 @@
import { useState } from 'react';
import { useState, useCallback, memo } from 'react';

interface IGuessInputProps {
onSubmitGuess: (word: string) => void;
}

export default function GuessInput({ onSubmitGuess }: IGuessInputProps) {
const GuessInput = memo(function GuessInput({ onSubmitGuess }: IGuessInputProps) {
const [inputValue, setInputValue] = useState('');

const handleSubmit = () => {
const handleSubmit = useCallback(() => {
if (!inputValue.trim()) return;
onSubmitGuess(inputValue);
setInputValue('');
};
}, [inputValue, onSubmitGuess]);

const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
handleSubmit();
}
};
const handleKeyDown = useCallback(
(e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
handleSubmit();
}
},
[handleSubmit],
);

const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
setInputValue(e.target.value);
}, []);

return (
<div className="flex flex-col items-center justify-center space-y-4">
Expand All @@ -26,7 +33,7 @@ export default function GuessInput({ onSubmitGuess }: IGuessInputProps) {
type="text"
placeholder="제시어 입력"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onChange={handleChange}
onKeyDown={handleKeyDown}
className="p-2 text-lg rounded-md bg-gray-800 text-white-default outline-none"
/>
Expand All @@ -38,4 +45,6 @@ export default function GuessInput({ onSubmitGuess }: IGuessInputProps) {
</button>
</div>
);
}
});

export default GuessInput;
153 changes: 90 additions & 63 deletions packages/frontend/src/components/gamePage/leftSection/MainDisplay.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState, useEffect } from 'react';
import { useState, useEffect, useCallback, useMemo, memo } from 'react';
import { useRoomStore } from '@/store/roomStore';
import StartButton from './GameButtons/StartButton';
import ReadyButton from './GameButtons/ReadyButton';
Expand All @@ -20,7 +20,7 @@ import VideoStream from '@/components/gamePage/stream/VideoStream';
import { useLocalStreamStore } from '@/store/localStreamStore';
import { useSpeakingControl } from '@/hooks/useSpeakingControl';

export default function MainDisplay() {
const MainDisplay = memo(function MainDisplay() {
const { userId } = useAuthStore();
const { isHost, isPinoco, allUsers } = useRoomStore();
const [gamePhase, setGamePhase] = useState<GamePhase>(GAME_PHASE.WAITING);
Expand All @@ -38,13 +38,21 @@ export default function MainDisplay() {
setGamePhase,
setSelectedVote,
);
function getCurrentStream() {
const localStream = useLocalStreamStore.getState().localStream;
const remoteStreams = usePeerConnectionStore.getState().remoteStreams;
if (currentSpeaker === userId) return localStream;
const currentStream = remoteStreams.get(currentSpeaker || '');
return currentStream;
}

const handleCountdownEnd = useCallback(() => {
setGamePhase(GAME_PHASE.WORD_REVEAL);
const timer = setTimeout(() => {
setGamePhase(GAME_PHASE.SPEAKING);
}, 3000);
return () => clearTimeout(timer);
}, []);

const handleVote = useCallback(() => {
if (!isVoteSubmitted) {
votePinoco(selectedVote ?? '');
setIsVoteSubmitted(true);
}
}, [isVoteSubmitted, selectedVote, votePinoco]);

useEffect(() => {
if (gameStartData) {
Expand All @@ -54,23 +62,76 @@ export default function MainDisplay() {
}
}, [gameStartData]);

const handleCountdownEnd = () => {
setGamePhase(GAME_PHASE.WORD_REVEAL);
setTimeout(() => {
setGamePhase(GAME_PHASE.SPEAKING);
}, 3000);
};
const currentStreamData = useMemo(() => {
if (currentSpeaker === userId) {
return useLocalStreamStore.getState().localStream;
}
return usePeerConnectionStore.getState().remoteStreams.get(currentSpeaker || '');
}, [currentSpeaker, userId]);

const handleVote = () => {
if (!isVoteSubmitted) {
if (selectedVote === null) {
votePinoco('');
} else {
votePinoco(selectedVote);
}
setIsVoteSubmitted(true);
const renderGamePhaseContent = useMemo(() => {
switch (gamePhase) {
case GAME_PHASE.WAITING:
return (
<div className="flex flex-col items-center justify-center h-full">
{isHost ? <StartButton /> : <ReadyButton />}
</div>
);
case GAME_PHASE.COUNTDOWN:
return <Countdown onCountdownEnd={handleCountdownEnd} />;
case GAME_PHASE.VOTING:
return (
<Voting
userId={userId!}
allUsers={allUsers}
selectedVote={selectedVote}
isVoteSubmitted={isVoteSubmitted}
setSelectedVote={setSelectedVote}
handleVote={handleVote}
/>
);
case GAME_PHASE.VOTING_RESULT:
return (
<VoteResult
deadPerson={deadPerson ?? ''}
voteResult={voteResult}
isDeadPersonPinoco={isDeadPersonPinoco ?? null}
/>
);
case GAME_PHASE.GUESSING:
return (
<div className="flex flex-col items-center justify-center h-full">
{isPinoco ? (
<GuessInput onSubmitGuess={submitGuess} />
) : (
<p className="text-xl text-center text-white-default">
피노코가 제시어를 추측 중입니다 🤔
</p>
)}
</div>
);
case GAME_PHASE.ENDING:
return <EndingResult endingResult={endingResult} />;
default:
return null;
}
};
}, [
gamePhase,
isHost,
userId,
allUsers,
selectedVote,
isVoteSubmitted,
handleVote,
handleCountdownEnd,
endingResult,
deadPerson,
voteResult,
isDeadPersonPinoco,
isPinoco,
submitGuess,
]);

return (
<div
className={`relative flex-1 flex flex-col w-full p-4 mt-4 rounded-lg overflow-hidden ${
Expand All @@ -80,7 +141,7 @@ export default function MainDisplay() {
{gamePhase === GAME_PHASE.SPEAKING && (
<div className="absolute inset-0">
<VideoStream
stream={getCurrentStream() || null}
stream={currentStreamData || null}
userName={currentSpeaker}
isLocal={true}
height="h-full"
Expand All @@ -105,43 +166,7 @@ export default function MainDisplay() {
)}
</div>

<div className="flex-1">
{gamePhase === GAME_PHASE.WAITING && (
<div className="flex flex-col items-center justify-center h-full">
{isHost ? <StartButton /> : <ReadyButton />}
</div>
)}
{gamePhase === GAME_PHASE.COUNTDOWN && <Countdown onCountdownEnd={handleCountdownEnd} />}
{gamePhase === GAME_PHASE.VOTING && (
<Voting
userId={userId!}
allUsers={allUsers}
selectedVote={selectedVote}
isVoteSubmitted={isVoteSubmitted}
setSelectedVote={setSelectedVote}
handleVote={handleVote}
/>
)}
{gamePhase === GAME_PHASE.VOTING_RESULT && (
<VoteResult
deadPerson={deadPerson ?? ''}
voteResult={voteResult}
isDeadPersonPinoco={isDeadPersonPinoco ?? null}
/>
)}
{gamePhase === GAME_PHASE.GUESSING && (
<div className="flex flex-col items-center justify-center h-full">
{isPinoco ? (
<GuessInput onSubmitGuess={submitGuess} />
) : (
<p className="text-xl text-center text-white-default">
피노코가 제시어를 추측 중입니다 🤔
</p>
)}
</div>
)}
{gamePhase === GAME_PHASE.ENDING && <EndingResult endingResult={endingResult} />}
</div>
<div className="flex-1">{renderGamePhaseContent}</div>

<div className="relative mt-auto">
{gamePhase === GAME_PHASE.SPEAKING && currentSpeaker === userId && (
Expand Down Expand Up @@ -170,4 +195,6 @@ export default function MainDisplay() {
</div>
</div>
);
}
});

export default MainDisplay;
Loading
Loading