728x90
BooQuiz 프로젝트의 React Custom Hook을 활용한 퍼널 구현하기
목차
문제 상황
실시간 퀴즈 플랫폼인 BooQuiz를 개발하면서 가장 큰 도전 과제는 퀴즈 진행 과정의 복잡한 상태 관리였습니다. 퀴즈 참여자들이 대기실에서 시작하여 문제 풀이, 결과 확인까지 이어지는 일련의 과정(퍼널)을 자연스럽게 경험할 수 있도록 만드는 것이 중요했습니다. 특히 다음과 같은 문제들이 있었습니다:
- 퀴즈 진행 단계별로 복잡한 상태 전이가 필요했습니다.
- 실시간 타이머와 답안 제출이 동기화되어야 했습니다.
- 여러 컴포넌트에서 퀴즈 상태를 일관되게 관리해야 했습니다.
- 네트워크 지연이나 오류 상황에 대한 견고한 처리가 필요했습니다.
구현 목표
이러한 문제들을 해결하기 위해 다음과 같은 목표를 설정했습니다:
핵심 요구사항
- 대기실, 준비, 문제 풀이, 결과 확인까지 명확한 퍼널 구조 구현
- 문제별 타이머와 답안 제출의 정확한 동기화
- 실시간 상태 업데이트와 오류 처리
- TypeScript를 활용한 타입 안정성 보장
주요 기능
- 퀴즈 시작/종료의 자연스러운 전환
- 5초 준비 시간 카운트다운
- 30초 문제 풀이 시간 관리
- 답안 제출 및 시간 초과 처리
- 결과 화면으로의 자연스러운 전환
설계 및 구현
상태 구조 설계
퀴즈 퍼널의 각 단계를 명확하게 표현하기 위한 타입 구조를 설계했습니다:
type MainStage = 'LOBBY' | 'QUIZ_PROGRESS' | 'RESULT';
type QuizSubStage = 'WAITING' | 'IN_PROGRESS' | 'COMPLETED';
interface QuizProgress {
currentQuizIndex: number;
totalQuizzes: number;
timeLimit: number;
}
interface StageData {
Lobby: {
participants: number;
isHost: boolean;
quizTitle: string;
description?: string;
};
quizProgress: {
currentQuiz: {
question: string;
options?: string[];
timeLimit: number;
submissionResult?: SubmissionResult;
type?: 'MULTIPLE_CHOICE' | 'SHORT_ANSWER';
};
progress: QuizProgress;
};
result: {
score: number;
rank: number;
totalParticipants: number;
correctAnswers: number;
};
}
타이머 로직 분리
재사용 가능한 타이머 커스텀 훅을 구현했습니다:
interface TimerConfig {
initialTime: number;
onComplete?: () => void;
autoStart?: boolean;
}
export const useTimer = ({ initialTime, onComplete, autoStart = false }: TimerConfig) => {
const [time, setTime] = useState<number | null>(autoStart ? initialTime : null);
const [isRunning, setIsRunning] = useState(autoStart);
useEffect(() => {
if (!isRunning || time === null) return;
const timer = setInterval(() => {
setTime(prev => {
if (prev === null || prev <= 1) {
setIsRunning(false);
onComplete?.();
return null;
}
return prev - 1;
});
}, 1000);
return () => clearInterval(timer);
}, [isRunning, time, onComplete]);
const start = () => {
setTime(initialTime);
setIsRunning(true);
};
const stop = () => {
setIsRunning(false);
setTime(null);
};
return { time, isRunning, start, stop };
};
퀴즈 퍼널 흐름
BooQuiz의 퍼널 진행 과정을 시퀀스 다이어그램으로 표현했습니다:
sequenceDiagram
participant User
participant QuizComponent
participant QuizManager
participant Timer
participant State
User->>QuizComponent: Enter Quiz Zone
QuizComponent->>QuizManager: Initialize
QuizManager->>State: Set Initial State (LOBBY)
User->>QuizComponent: Click Start Quiz
QuizComponent->>QuizManager: startQuiz()
QuizManager->>State: Set Stage(QUIZ_PROGRESS)
QuizManager->>Timer: Start PrepareTimer(5s)
Timer-->>QuizManager: PrepareTimer Complete
QuizManager->>State: Set SubStage(IN_PROGRESS)
QuizManager->>Timer: Start SolutionTimer(30s)
상세 구현
useQuizZoneManager Hook
퀴즈 퍼널 전체를 관리하는 핵심 커스텀 훅입니다:
export const useQuizZoneManager = (config: QuizStageConfig) => {
// 상수 정의
const PREPARE_TIME = 5;
const SOLVE_TIME = 30;
// 상태 관리
const [mainStage, setMainStage] = useState<MainStage>('LOBBY');
const [subStage, setSubStage] = useState<QuizSubStage>('WAITING');
const [quizProgress, setQuizProgress] = useState<QuizProgress>({
currentQuizIndex: 0,
totalQuizzes: config.totalQuizzes,
timeLimit: SOLVE_TIME,
});
const [stageData, setStageData] = useState<Partial<StageData>>({});
const [isTransitioning, setIsTransitioning] = useState(false);
// 타이머 설정
const prepareTimer = useTimer({
initialTime: PREPARE_TIME,
onComplete: () => handleQuizCycle('IN_PROGRESS')
});
const solutionTimer = useTimer({
initialTime: SOLVE_TIME,
onComplete: handleTimeExpired
});
// 메인 로직...
핵심 기능 구현
타이머 만료 처리:
function handleTimeExpired() { solutionTimer.stop(); setIsTransitioning(true); try { const nextIndex = quizProgress.currentQuizIndex + 1; const isLastQuiz = nextIndex >= quizProgress.totalQuizzes; if (isLastQuiz) { setMainStage('RESULT'); config.onQuizComplete?.(); } else { setQuizProgress(prev => ({ ...prev, currentQuizIndex: nextIndex })); setSubStage('WAITING'); prepareTimer.start(); } updateStageData('quizProgress', { currentQuiz: { ...stageData.quizProgress?.currentQuiz, submissionResult: { submitted: false, timeExpired: true }, }, }); } catch (err) { config.onError?.(err instanceof Error ? err : new Error('Unknown error')); } finally { setIsTransitioning(false); } }
퀴즈 사이클 관리:
function handleQuizCycle(stage: QuizSubStage, data?: any) { setIsTransitioning(true); try { setSubStage(stage); if (stage === 'WAITING') { prepareTimer.start(); solutionTimer.stop(); } else if (stage === 'IN_PROGRESS') { prepareTimer.stop(); solutionTimer.start(); } if (data) { updateStageData('quizProgress', data); } config.onSubStageChange?.(stage); if (stage === 'COMPLETED') { solutionTimer.stop(); proceedToNextQuiz(); } } catch (err) { config.onError?.(err instanceof Error ? err : new Error('Unknown error')); } finally { setIsTransitioning(false); } }
사용 예시
BooQuiz 프로젝트에서 실제 사용된 컴포넌트 예시입니다:
const QuizZone: React.FC<QuizZoneProps> = ({ pinNumber }) => {
const {
mainStage,
subStage,
quizProgress,
stageData,
prepareTime,
solutionTime,
startQuiz,
submitAnswer,
} = useQuizZoneManager({
totalQuizzes: 5,
onQuizComplete: () => console.log('퀴즈 완료!'),
onError: (error) => console.error('에러 발생:', error),
});
return (
<div className="quiz-zone">
{mainStage === 'LOBBY' && (
<LobbyScreen
onStart={startQuiz}
participants={stageData.Lobby?.participants}
/>
)}
{mainStage === 'QUIZ_PROGRESS' && (
<QuizScreen
subStage={subStage}
currentQuiz={stageData.quizProgress?.currentQuiz}
timer={subStage === 'WAITING' ? prepareTime : solutionTime}
onSubmit={submitAnswer}
/>
)}
{mainStage === 'RESULT' && (
<ResultScreen result={stageData.result} />
)}
</div>
);
};
최적화 및 개선
향후에 적용 할 수 있을 것 같은 부분입니다.
성능 최적화
- React.memo를 활용한 컴포넌트 메모이제이션
- useCallback과 useMemo를 통한 함수와 값의 안정성 확보
- 상태 업데이트 배치 처리로 리렌더링 최소화
에러 처리
- ErrorBoundary 컴포넌트를 통한 장애 격리
- 타입 가드를 활용한 런타임 타입 안정성 강화
- 사용자 친화적인 에러 메시지와 복구 UI 제공
확장성
- 다양한 퀴즈 유형(객관식, 주관식) 지원
- 호스트와 참가자 권한 분리
- WebSocket을 통한 실시간 데이터 동기화
결론
BooQuiz 프로젝트에서 React Custom Hook을 활용한 퍼널 구현을 했고 다음과 같은 항목을 적용했다고 생각합니다.
사용자 경험 개선
- 자연스러운 퀴즈 진행 흐름
- 직관적인 타이머 및 상태 표시
- 안정적인 답안 제출 처리
개발 생산성 향상
- 재사용 가능한 커스텀 훅 구조
- 명확한 타입 시스템
- 테스트 용이성 확보
유지보수성 강화
- 관심사 분리를 통한 코드 구조화
- 확장 가능한 아키텍처
728x90
'DEV > FE' 카테고리의 다른 글
[FE] 쉽게 이해하는 Vitest 주요 기능 완성 가이드 (0) | 2024.11.17 |
---|---|
[why] 📚 테스트 코드는 왜 작성해야 할까요? 특히 프론트엔드에서는요? (4) | 2024.11.16 |
[why] JavaScript는 왜 undefined를 사용할까? (2) | 2024.10.26 |
[why] 프론트엔드 개발자로써 알아야 되는 부분들 (1) | 2024.10.26 |