quiz 'ALL' 모드 로딩 UX 개선 (진행률 바 + 프리페치 + 에러 토스트)
요약Sprint 228의 lazy-load 전환(route 84.8→13.6kB) 이후 'ALL' 모드(기본값)는 10분야 청크를 Promise.all 병렬 로드하며 그동안 정적 SkeletonCard만 노출(진행 표시 없음), 동적 import 실패 시 SP228 P2로 idle 복귀하나 사용자 피드백이 없었다. 로딩 UX를 3축으로 개선했다: ① start() catch에 sonner toast(toast.error)를 추가해 동적 import 실패를 사용자 가시 피드백으로 보강(Toaster 기마운트라 신규 인프라 0) ② getRandomQuestions에 onProgress(loaded,total) 콜백을 5번째 positional로 추가(rng 4번째 유지, 하위호환)해 각 청크 로드마다 진행을 page state로 받아 신규 QuizLoading(quiz 로컬, Progress+SkeletonCard 재사용→신규 토큰 0) 진행률 바에 시각화 ③ prefetchQuestions(category)를 신규 export(fire-and-forget, 에러 swallow)하고 QuizStart Start 버튼 hover/focus에서 선택 분야 청크를 워밍(동적 import 캐시 재사용→체감 로딩 감소). total=0 division 가드 포함. jest 1649 PASS(+14), 글로벌 lines 88.07%/branches 79.39%, route /[locale]/quiz 13.9kB(+0.3kB, lazy-load 회귀 없음). 프론트 전용, merge≠라이브.
목표
- Sprint 228의 분야별 lazy-load 전환(
/[locale]/quizroute 84.8 → 13.6kB) 이후, 'ALL' 모드(기본값)의 로딩 UX를 3축(에러 피드백 / 진행 표시 / 프리페칭)으로 개선한다. - 신규 디자인 토큰 0 + 신규 인프라 0 +
components/ui신규 0(quiz 로컬만)으로 처리하고, Sprint 228의 lazy-load 회귀(route Size)를 내지 않는다. - 프론트 전용 — merge ≠ 라이브(재배포 후 라이브 검증은
quiz-ui-verification런북 이월에 합류).
배경
Sprint 228은 문항 데이터를 시작 시 CATEGORY_LOADERS 동적 import로 전환해 초기 번들에서 제거했다(route 84.8→13.6kB). 다만 'ALL' 모드(기본값)는 10분야 청크를 Promise.all로 병렬 로드하며, 그동안 사용자는 정적 SkeletonCard만 본다(진행 표시 없음). 또한 동적 import가 실패하면 Sprint 228 P2 수정으로 idle로 복귀하지만 사용자에게 보이는 피드백이 없다(stale chunk / 오프라인 / CDN 실패 시 조용히 시작 화면으로 되돌아감).
Sprint 229는 이 로딩 UX 갭을 3축으로 해소한다.
결정
D0. 확정 결정 (사용자)
- 세 축 모두 포함 — 에러 피드백 + 진행 표시 + 프리페칭.
- 로딩 UI = 진행률 바 — 기존 Radix
Progress재사용. - 프리페칭 트리거 = Start 버튼 hover/focus.
D1. 에러 피드백 = sonner toast (Wave C)
start()catch에toast.error(t('start.loadError'))를 추가해, Sprint 228 P2의idle복귀를 사용자 가시 피드백으로 보강한다.- sonner는
AppLayout에Toaster가 이미 마운트되어 있어 신규 인프라 0.
D2. 로딩 진행 = onProgress 콜백 + QuizLoading 진행률 바 (Wave A·B·C)
getRandomQuestions에onProgress(loaded, total)를 5번째 positional 선택 인자로 추가한다(rng는 4번째로 유지 → 하위호환). 각 청크의.then에서loaded를 증가시켜 콜백한다.page.tsx가loadProgressstate로 진행을 받아, 신규QuizLoading(quiz 로컬 컴포넌트 —components/ui아님 → Palette 트리거 회피) 진행률 바에 시각화한다.- 기존
Progress(Radix) +SkeletonCard재사용으로 신규 디자인 토큰 0.
D3. 프리페칭 = prefetchQuestions fire-and-forget (Wave A·D)
prefetchQuestions(category)를 신규 export한다 — fire-and-forget(결과 버림, 에러 swallow).QuizStart의 Start 버튼onMouseEnter/onFocus에서 현재 선택category청크를 워밍한다. 동적 import는 캐시되므로 클릭 시 재사용되어 체감 로딩이 감소한다.- 실제 로드 실패는 prefetch가 아니라 본 경로
start()의getRandomQuestions에서 토스트로 표면화된다(prefetch는 에러를 삼킴 → 사용자 경로 피드백은 D1이 보장).
D4. total=0 division 가드 (Wave B)
QuizLoading은total > 0 ? round(loaded / total * 100) : 0로 계산해 초기 스냅샷(아직 total 미설정)의 division을 안전하게 처리한다.
구현
총 6 atomic commit (start 834d07c):
| 커밋 | Wave | 내용 |
|---|---|---|
eae614c | A | data/quiz/index.ts — getRandomQuestions에 onProgress 5번째 positional 추가 + prefetchQuestions(category) 신규 export(fire-and-forget) |
d34bb99 | B | components/quiz/QuizLoading.tsx 신규 — Progress(Radix) + aria-live title + SkeletonCard, total=0 가드 |
94eab78 | C | page.tsx — loadProgress state, onProgress 배선, start() catch toast.error, SkeletonCard → QuizLoading |
3608f64 | D | QuizStart.tsx — Start 버튼 hover/focus에서 prefetchQuestions 호출 |
0ae0d87 | E | messages/{ko,en}/quiz.json — start.loadError + loading 네임스페이스(title/progress/progressAria) |
2c01053 | F | 테스트 — index(onProgress ALL 0..10·단일 0..1·prefetch reject swallow)·page(loading=QuizLoading progressbar·reject 시 toast.error 회귀)·QuizLoading 신규(6)·QuizStart(hover/focus/분야별 prefetch spy) |
검증
- tsc: 오류 0 (전 파일).
- jest: 1649 PASS / 0 FAIL (Sprint 228 1635 → +14).
- 글로벌 커버리지: lines 88.07% / branches 79.39% / statements 87.56% / functions 85.23% (게이트 83/71/81/82 통과).
- next lint: 0 errors.
- next build: "Compiled successfully".
/[locale]/quizroute Size 13.9kB (Sprint 228 13.6kB 대비 +0.3kB — 문항 데이터는 여전히 route 밖 on-demand 청크로 분리, lazy-load 회귀 없음). - i18n: ko/en 52키 정합.
- ADR 게이트: index count (sprint 167) / adr-en coverage (KR/EN 1:1) / adr-links 0 broken / doc-refs no broken
Critic 교차 리뷰
- 도구: Codex codex-cli (Wave별 Auto-Critic + 최종 브랜치 전체)
최종 브랜치 전체 Critic: 머지 직전 Oracle이 별도 실행(브랜치 전체 = 권위 리뷰) — async 진행 콜백 정합·prefetch 에러 swallow·toast 피드백·total=0 가드 중점.
종합 판정: ✅ 머지 가능 — jest 1649 PASS(+14), route Size 13.9kB(lazy-load 회귀 없음), 커버리지·i18n·ADR 게이트 전 통과.