Gu Doyoon
  • React
  • TypeScript
  • Vite
  • Zustand
  • Tanstack Query
  • Styled-Components

CoKo

JS를 재미있게 학습하기 위한 퀴즈 기반 교육 웹 사이트

📅 2024.09 - 2025.03 (6개월)👥 6명🛠 프론트엔드 팀장, 어드민 페이지 개발

핵심 기여 및 성과

Core Engineering & UX
Core Engineering
  • highlight.js 와 직접 구현한 라인 넘버링 로직을 결합하여 코드 가독성을 IDE 수준으로 개선
  • Suspense와 ErrorBoundary를 활용한 비동기 처리 표준화 및 도메인 기반 폴더 구조 도입 주도
DevOps & Productivity
Operational Efficiency
  • Swagger 수동 입력의 비효율을 해소하기 위해 Admin 페이지 개발, 데이터 CRUD 및 AWS S3 이미지 업로드 기능 구현
  • GitHub Actions와 EC2를 연동하여 메인 브랜치 병합 시 자동 배포되는 파이프라인 설계

트러블 슈팅

ref 콜백 함수 전달로 인한 무한 리렌더링 이슈 해결
문제 상황
reference image

팝업 컴포넌트 마운트 시 무한 리렌더링 발생

DOM rect 정보를 전역 상태로 저장하기 위해 콜백 ref로 DOM 노드에 접근해야 했습니다. 하지만 특정 팝업 컴포넌트가 마운트되면 무한 리렌더링이 발생하여 브라우저가 프리징되는 현상이 있었습니다.

원인 파악

React의 ref 콜백은 렌더링마다 새로운 함수 참조가 전달되면 다시 실행됩니다. ref 실행 → 상태 변경 → 리렌더링 → ref 재실행의 무한 루프가 원인이었습니다.

해결 과정
reference image

ref 콜백을 useCallback으로 감싸 참조를 고정

결과 요약
useCallback을 활용한 ref 참조 안정화 및 브라우저 프리징 문제 제거
퀴즈 데이터 유실 방지
문제 상황

퀴즈 풀이 도중 실수로 새로고침이나 뒤로가기를 눌렀을 때, 전역 상태가 초기화되어 진행 상황이 모두 날아가는 문제가 있었습니다.

해결 과정

복잡한 복구 로직 대신, 브라우저의 beforeunload 이벤트를 활용해 실수를 방지하는 것이 MVP 단계에서 가장 효율적인 해결책이라 판단했습니다.

useEffect(() => { if (!enabled) return; window.addEventListener("beforeunload", handleBeforeUnload); return () => { window.removeEventListener("beforeunload", handleBeforeUnload); }; }, [enabled, handleBeforeUnload]);
결과 요약
실수로 인한 퀴즈 데이터 유실 방지 및 사용자 경험 향상
HOC 패턴과 제네릭을 활용한 복잡한 데이터 로직 추상화
도입 이유

퀴즈 컴포넌트가 실전 모드, 튜토리얼 모드, 잠김 상태 등 다양한 상황에서 재사용되어야 했습니다. 하지만 각 모드마다 데이터를 가져오는 로직이 달라, 컴포넌트 내부에 비즈니스 로직이 강하게 결합되고 코드 중복이 발생했습니다.

문제 발생

로직 분리를 위해 HOC를 도입했으나, TypeScript 환경에서 Props 타입 추론이 깨지는 문제가 발생했습니다.

  • 래핑된 컴포넌트는 quizzes 데이터가 필요함
  • 하지만 HOC를 사용하는 부모 입장에서는 quizzes를 전달할 필요가 없어야 함
  • 단순 래핑 시 TS는 여전히 부모에게 quizzes Props를 요구함
해결 과정

제네릭교차 타입을 활용해 타입 안전성을 확보했습니다.

  1. 제네릭 P를 통해 원본 컴포넌트의 props 타입을 보존 후 HOC가 제공해주는 데이터를 InjectedProps로 분리
  2. 고차 컴포넌트가 반환할 컴포넌트의 props 타입을 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; };
결과 요약
복잡한 분기 로직을 분리 및 추상화하고 타입 안전성을 유지하며 컴포넌트 재사용성 극대화

성능 개선

React.lazy를 활용한 번들 최적화 및 초기 로딩 속도 개선
도입 배경

SPA의 특성상 초기 진입 시 모든 페이지의 리소스를 한 번에 다운로드하여 FCP 지연이 우려되었습니다. 특히 네트워크가 느린 모바일 환경에서 사용자 이탈 우려가 컸습니다.

구현 내용

페이지별 진입 시점에 필요한 리소스만 로드하는 전략을 도입했습니다.

  1. React.lazy와 동적 import()를 사용하여 라우터 레벨에서 컴포넌트를 분리했습니다.
  2. 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> );
성능 측정
주요 성과 지표
초기 번들 사이즈
2.0 MB300 KB85% 감소
결과 요약
초기 리소스 용량을 85% 감축하여 쾌적한 진입 속도와 데이터 비용 절감 달성