JS를 재미있게 학습하기 위한 퀴즈 기반 교육 웹 사이트
React의 ref 콜백은 렌더링마다 새로운 함수 참조가 전달되면 다시 실행됩니다. ref 실행 → 상태 변경 → 리렌더링 → ref 재실행의 무한 루프가 원인이었습니다.
퀴즈 풀이 도중 실수로 새로고침이나 뒤로가기를 눌렀을 때, 전역 상태가 초기화되어 진행 상황이 모두 날아가는 문제가 있었습니다.
복잡한 복구 로직 대신, 브라우저의 beforeunload 이벤트를 활용해 실수를 방지하는 것이 MVP 단계에서 가장 효율적인 해결책이라 판단했습니다.
useEffect(() => { if (!enabled) return; window.addEventListener("beforeunload", handleBeforeUnload); return () => { window.removeEventListener("beforeunload", handleBeforeUnload); }; }, [enabled, handleBeforeUnload]);
퀴즈 컴포넌트가 실전 모드, 튜토리얼 모드, 잠김 상태 등 다양한 상황에서 재사용되어야 했습니다. 하지만 각 모드마다 데이터를 가져오는 로직이 달라, 컴포넌트 내부에 비즈니스 로직이 강하게 결합되고 코드 중복이 발생했습니다.
로직 분리를 위해 HOC를 도입했으나, TypeScript 환경에서 Props 타입 추론이 깨지는 문제가 발생했습니다.
quizzes 데이터가 필요함quizzes를 전달할 필요가 없어야 함quizzes Props를 요구함제네릭과 교차 타입을 활용해 타입 안전성을 확보했습니다.
P를 통해 원본 컴포넌트의 props 타입을 보존 후 HOC가 제공해주는 데이터를 InjectedProps로 분리as P를 통해 타입 단언하여 TS가 부모 컴포넌트에게 quizzes Props를 요구하지 않도록 처리const withQuizzes = <P extends object>( WrappedComponent: FC<P & InjectedProps> ) => { const ComponentWithQuizzes: FC<P & WithQuizzesProps> = ({ partId, partStatus, ...props }) => { return ( <WrappedComponent {...(props as P)} quizzes={quizzes} /> ); }; return ComponentWithQuizzes; };
SPA의 특성상 초기 진입 시 모든 페이지의 리소스를 한 번에 다운로드하여 FCP 지연이 우려되었습니다. 특히 네트워크가 느린 모바일 환경에서 사용자 이탈 우려가 컸습니다.
페이지별 진입 시점에 필요한 리소스만 로드하는 전략을 도입했습니다.
React.lazy와 동적 import()를 사용하여 라우터 레벨에서 컴포넌트를 분리했습니다.Suspense 컴포넌트를 상위 트리에 배치하여, 청크(Chunk) 파일이 다운로드되는 동안 스켈레톤 UI를 노출해 UX 저하를 방지했습니다.const About = lazy(() => import('./pages/About')); const Contact = lazy(() => import('./pages/Contact')); return ( <Suspense fallback={<PageSkeleton />}> <Routes> <Route path="/about" element={<About />} /> <Route path="/contact" element={<Contact />} /> </Routes> </Suspense> );