퀴즈존 퍼널 패턴 리팩토링 회고 (2024년 11월 3주차)
들어가며
이번 주는 3주차에 제가 제안하고 구현했던 퀴즈존의 퍼널 패턴 코드를 전면적으로 리팩토링하는 작업을 진행했습니다. 토스의 SLASH 23 컨퍼런스에서 퍼널 패턴을 접하고 이를 우리 프로젝트에 적용하면 좋겠다고 생각했었는데, 3주차 당시에는 데모데이라는 시간적 제약 속에서 급하게 구현을 진행했었죠. 매주 현실적인 목표를 설정하고 이를 달성해나가는 과정에서, 이번 주에는 그동안 미뤄두었던 프론트엔드 코드 품질 개선을 위한 시간을 확보할 수 있었습니다. 특히 동현님과 페어 프로그래밍으로 진행하면서 설계와 구조에 대해 깊이 있는 논의를 할 수 있었던 점이 매우 의미 있었습니다.
퍼널 패턴과 우리의 퀴즈존
퍼널은 '깔때기'라는 뜻처럼, 사용자가 특정 목표를 달성하기까지의 단계를 위에서 아래로 시각화했을 때 깔때기 모양이 되는 것에서 유래했습니다. 우리의 퀴즈존은 로비에서 시작해서, 퀴즈 진행, 결과 확인이라는 명확한 단계적 흐름을 가지고 있었기에 이 패턴이 특히 적합했습니다.
graph TD
A[LOBBY] --> B[QUIZ_PROGRESS]
B --> C[RESULT]
subgraph QUIZ_PROGRESS State
D[WAITING]
E[IN_PROGRESS]
F[COMPLETED]
D --> E
E --> F
F --> |Next Quiz|D
end
B === D
F --> |Last Quiz|C
초기 구현의 한계와 리팩토링의 필요성
3주차에 처음 퍼널 패턴을 적용할 때는 시간에 쫓기다 보니, 패턴은 도입했지만 실제 구현에 있어서는 많은 아쉬움이 있었습니다. 크게 세 가지 주요 문제점이 있었는데, 첫 번째는 상태 관리의 복잡성이었습니다. 여러 개의 독립적인 useState로 상태를 관리하다 보니 상태 간의 관계가 복잡해졌고, 하나의 상태를 변경할 때 다른 연관된 상태들도 함께 업데이트해야 하는 상황이 자주 발생했습니다.
// 복잡했던 초기 구현
const [quizZone, setQuizZone] = useState<QuizZone>('LOBBY');
const [solveStage, setSolveStage] = useState<SolveStage>('WAITING');
const [quizProgress, setQuizProgress] = useState<QuizProgress>({...});
const [quizZoneData, setQuizZoneData] = useState<Partial<QuizZoneData>>({});
두 번째 문제는 비즈니스 로직의 분산이었습니다. 퀴즈 진행에 관한 로직이 여러 함수에 걸쳐 나누어져 있었고, 이로 인해 전체적인 흐름을 파악하기가 어려웠죠.
// 분산된 비즈니스 로직
function changeMainStage(stage: QuizZone, data?: any) {...}
function handleQuizCycle(stage: SolveStage, data?: any) {...}
function proceedToNextQuiz() {...}
세 번째로는 타이머와 웹소켓 관련 로직이 다른 비즈니스 로직과 강하게 결합되어 있었던 점입니다. 특히 타이머 로직은 퍼널의 상태 관리 로직과 너무 밀접하게 연결되어 있어서, 타이머 동작을 수정하거나 테스트하기가 매우 까다로웠습니다.
페어 프로그래밍을 통한 개선
이번 주에 동현님과 함께한 페어 프로그래밍 세션에서는 이러한 문제점들을 하나씩 해결해나갔습니다. 가장 먼저 상태 관리 구조를 전면적으로 개선했는데, useReducer를 도입하여 상태 변경 로직을 중앙화하고 각 상태 변경의 의도를 명확히 드러내도록 했습니다.
flowchart TB
subgraph Before["리팩토링 전: 분산된 상태 관리"]
direction TB
X1[퀴즈존 상태] --> Y1[changeMainStage]
X2[풀이 상태] --> Y2[handleQuizCycle]
X3[진행 상태] --> Y3[proceedToNextQuiz]
Y1 --> U1[상태 업데이트]
Y2 --> U1
Y3 --> U1
end
Before --> After
subgraph After["리팩토링 후: 통합된 상태 관리"]
direction TB
A1[Action Dispatch] --> B1[QuizZone Reducer]
B1 --> C1{Action Type}
C1 -->|init/start/submit/nextQuiz| E1[새로운 상태]
end
// 개선된 상태 관리 구조
type QuizZoneAction =
| { type: 'init'; payload: QuizZoneLobbyState }
| { type: 'join'; payload: { players: Player[] } }
| { type: 'start'; payload: undefined }
| { type: 'submit'; payload: undefined }
| { type: 'nextQuiz'; payload: CurrentQuiz };
const [state, dispatch] = useReducer(quizZoneReducer, initialState);
성능 최적화와 실시간 처리
페어 프로그래밍 과정에서 특히 중점을 둔 것이 성능 최적화였습니다. 0.1초마다 업데이트되는 타이머로 인한 불필요한 리렌더링이 가장 큰 문제였는데, 이를 해결하기 위해 타이머 로직을 완전히 분리하고 useRef를 활용하여 최적화했습니다.
const useTimer = ({ initialTime, onComplete }: TimerConfig) => {
const isRunningRef = useRef(false);
const timerRef = useRef<NodeJS.Timeout | null>(null);
// 최적화된 타이머 로직
};
웹소켓 연결도 useRef를 사용하여 관리하도록 변경했는데, 이를 통해 컴포넌트 리렌더링과 무관하게 안정적인 연결을 유지할 수 있게 되었습니다. 특히 웹소켓 메시지 핸들러를 리듀서 액션과 직접 매핑하는 구조로 개선하여 실시간 상태 업데이트의 안정성을 크게 높였습니다.