- docs/design/311-llm-warmup/README.md — 기능 설계서. ChatWarmupController (5-state) + GemmaLlmService _loadingFuture concurrent guard + ModelLifecycle.quickCheck (lightweight ready). - docs/design/311-llm-warmup/UX-REVIEW.md — UX-Reviewer parallel pass. Strong 4 + Suggest 2 권고. 입력창 enabled 유지 (타이핑 가능) + hintText 만 교체 + 상태-행동 분리. - docs/design/311-llm-warmup/fn-chat_warmup_controller.md — start/retry 상세 + 빠른 경로 (isLoaded 시 Loading skip). - docs/design/311-llm-warmup/fn-concurrent_load_guard.md — _loadingFuture 패턴 + whenComplete cleanup. - .claude/agents/ux-reviewer.md — 신규 페르소나 (02-Architect 단계 내 parallel reviewer, 카테고리 부여 X). AC 8 → 12 (UX 신규 4건 통합). OQ 3건 모두 해소. ADR 없음 (backward-compatible 추가). Refs #311 #260
8.7 KiB
UX 리뷰: ChatScreen LLM warm-up (#311)
검토: [AI] UX-Reviewer · 대상:
./README.mdv1 + Planner AC 8개 · 날짜: 2026-06-15 위치: 02-Architect 단계 내 parallel review. Architect 가 흡수 후 03-Developer 인계.
요약
설계서가 백엔드 흐름 (state 머신, concurrent guard, lifecycle) 은 견고하다. 그러나 사용자가 실제로 보는 표면 — 라벨 톤, spinner 위치, 빠른 전이 시 깜빡임, 실패 시 다음 행동 — 에는 결정 안 된 것이 너무 많고 (OQ 3건), 결정된 부분도 마찰을 만들 위험이 있다. 6가지 권고를 아래에 정리.
[Strong] R1 — "AI 준비 중…" 을 입력창 자리에 박으면 안 됨
관점: 마찰 / 흐름 / 정신 모델 근거:
- README §5 / §6 은 "입력창 자리에 라벨 + spinner" 를 기본안으로 둠.
- 그러면 사용자가 메시지를 미리 타이핑해두는 행동 자체가 차단된다. 사용자는 모델 로드를 기다리는 동안에도 "어떤 질문을 할지" 머릿속에서 정리하면서 손가락은 이미 키보드 위에 있다.
- 입력창이 사라지면 사용자는 "왜 안 보이지?" 하고 한 번 더 추론해야 한다 (마찰 +1).
- 더 큰 문제: ChatScreen 의 ListView 영역이 비어 있는 첫 진입 시점에 입력창까지 사라지면 화면 전체가 spinner 하나뿐이 된다 — "이 앱이 멈췄나?" 시그널.
제안 (강력 권고):
- 입력창은 항상 보이게 유지.
enabled: false로만 잠그고hintText만 교체:- 평상:
"습관 추가, 기록, 카탈로그 질문…" - warmup:
"AI 준비 중… 잠시만요"
- 평상:
- send 버튼 자리에
CircularProgressIndicator(strokeWidth: 2)표시 (현재 isStreaming 처리와 동일 패턴 — 일관성 ↑). - 별도 상단 라벨/배지 추가 X. 사용자는 send 버튼이 spinner 인 것 + hint 한 줄로 충분히 추론 가능.
README 영향: §3 AC3, §5 다이어그램, §6 binding 절 모두 수정.
[Strong] R2 — 사용자가 텍스트 입력하고 send 누르면 어떻게 되나? (현재 설계는 침묵)
관점: 정신 모델 / 마찰 근거:
- R1 권고를 받아들이면 입력창은 보이지만
enabled: false. 사용자가 키보드를 띄우고 타이핑하려 하면 → 반응 없음. 또 다른 마찰. - 만약
enabled: true로 두고 send 만 disable 하면, 사용자가 메시지를 친 뒤 send 를 누르려는 순간 "왜 안 가지?" 로 또 다른 마찰. - 어느 쪽이든 사용자의 의도 (메시지를 보내고 싶음) 와 시스템의 상태 (아직 못 받음) 사이의 간극 이 풀리지 않음.
제안 (강력 권고):
- 입력창은
enabled: true로 두어 타이핑은 허용한다. 사용자가 미리 메시지를 작성하도록. - send 버튼은 disabled + spinner. 누를 수는 없음.
- warmup 완료 시점에 사용자가 이미 타이핑해둔 메시지가 있으면 → send 버튼 자동 활성화. (자동 send 까지는 X — 사용자 의도 확인 필요)
- AC4 에 한 줄 추가: "warmup ready 시점에 입력창의 텍스트가 비어있지 않으면 send 활성화."
README 영향: §3 AC3/AC4 보강.
[Suggest] R3 — 첫 warmup 은 "예상 시간" 한 마디 더
관점: 정신 모델 / 인지된 지연 근거:
- 사용자에게 "AI 준비 중" 만 보여주면 — 0.5초 후에도, 5초 후에도, 10초 후에도 같은 라벨. 정신 모델은 점점 "이게 멈췄나?" 로 기운다.
- Gemma 4 E2B native init + mmap 은 디바이스에 따라 2-8초 범위로 추정 (cold launch). 첫 진입 시 한 번뿐이고 두 번째 진입부터는 거의 즉시 (
isLoaded=true) — 즉 사용자가 이 라벨을 길게 보는 건 첫 진입 단 한 번. - 그 한 번을 부드럽게 만들 가치가 있다.
제안:
- hint 를
"AI 준비 중… 첫 시작은 몇 초 걸려요"로 한 번만 명시. - 1회성 SnackBar 도 검토할 수 있으나 — 사용자가 곧바로 입력창 영역으로 시선이 가므로 hint 한 줄로 통합하는 게 단순.
README 영향: §3 AC3 의 라벨 문안, §12 OQ-1 해소.
[Strong] R4 — 빠른 경로 (이미 loaded) 의 라벨 깜빡임을 명시적으로 차단
관점: 정신 모델 근거:
- README §9 의 "ChatScreen 재진입" 케이스 + fn-spec 의 "빠른 경로" 분기로 Loading state skip 처리가 들어가 있음 — 좋다.
- 하지만 §12 OQ-3 에 "라벨 깜빡임 가능 — 미해결" 이 남아있어 모순. fn-spec 의 빠른 경로가 Loading 을 스킵하므로 깜빡임은 일어나지 않음.
- 명확히 못 박을 것.
제안:
- OQ-3 를 OQ 에서 제거하고 §9 의 "빠른 경로" 분기 + fn-spec 의 step 2 를 명시적으로 인용한 결정 노트로 전환.
- min display time (300ms 등) 같은 인위 지연은 도입 금지 — 사용자에게 거짓 작업을 보여주는 안티패턴.
README 영향: §9 endorse 표현, §12 OQ-3 삭제.
[Strong] R5 — 실패 메시지의 다음 행동이 약함
관점: 에러 회복 / 마찰 근거:
- README §9 의 실패 메시지:
- "AI 모델 파일을 찾을 수 없어요. 설정에서 다시 다운로드해주세요." — 좋음 (다음 행동 명시).
- "AI 시작 중 오류가 발생했어요. 잠시 후 다시 시도해주세요." — 약함. "잠시 후" 가 얼마인지, "다시 시도" 가 어떻게 인지 불명.
- 사용자는 두 가지 의문: ① 이게 일시적 문제인가, 영구적 문제인가 ② 내가 뭘 해야 하나.
제안:
- "다시 시도" 버튼이 있으니, 메시지에서 "잠시 후 다시 시도해주세요" 는 빼고 상태 + 행동 분리:
- 상태:
"AI 를 시작하지 못했어요." - 행동: 별도 [다시 시도] 버튼 (이미 설계됨).
- 상태:
- AC5 에 "한국어 메시지는 상태만 기술, 행동은 버튼이 담당" 명시.
- 3회 연속 실패 시점에는 보조 안내 ("문제가 계속되면 앱을 재시작해보세요") — 후속 polish 로 deferrable.
README 영향: §9 메시지 사전 갱신, §3 AC5 보강.
[Suggest] R6 — 재시도 버튼 위치는 error container 안
관점: 흐름 / 접근성 근거:
- README §12 OQ-2 가 "error container 내부 vs 입력창 옆 icon" 으로 열어둠.
- 입력창 옆 icon 은 평상시에는 없는 자리에 갑자기 나타나 사용자가 학습해야 함. 게다가 send 자리 근처에 또 다른 액션 = 오탭 위험.
- error container 는 이미 실패 메시지 영역이라 컨텍스트 일관 + 사용자가 "여기서 다음 행동" 학습.
제안:
- OQ-2 → 결정: error container 내부 OutlinedButton('다시 시도').
- container 좌우 패딩, 메시지와 버튼은 column 으로 분리, 버튼은 우측 정렬.
README 영향: §12 OQ-2 해소.
AC 보강 권고 (UX-Reviewer 가 작성한 추가 AC)
UX 관점에서 검증 가능한 새 AC 를 제안 (Architect 가 흡수 시 README §3 에 추가):
- AC9 (신규) Warmup 중 입력창은
enabled: true로 타이핑 가능. send 만 disabled + spinner. → R1+R2. - AC10 (신규) Warmup ready 시점에 입력창에 비어있지 않은 텍스트가 있으면 send 자동 활성화 (자동 send 는 X). → R2.
- AC11 (신규)
isLoaded=true인 재진입 시 Loading state 가 1 frame 이라도 노출되지 않는다 (위젯 테스트로 verify). → R4. - AC12 (신규) 실패 메시지는 상태 기술만, 행동은 [다시 시도] 버튼이 담당. 메시지 본문에 "다시 시도해주세요" 같은 명령형 X. → R5.
마이크로카피 사전 (Architect 가 채택 시 README §6 또는 별도 부록)
| 상태 | 한국어 라벨 | 위치 |
|---|---|---|
| warmup loading | 입력창 hintText: AI 준비 중… 첫 시작은 몇 초 걸려요 |
입력창 |
| warmup ready | hintText: 습관 추가, 기록, 카탈로그 질문… |
입력창 (기존 유지) |
| warmup unavailable | (라벨 없음 — 평상시와 동일) | — |
| warmup failed (file missing) | error container: AI 모델 파일을 찾을 수 없어요. 설정에서 다시 다운로드해주세요. + [설정으로 가기] |
error container |
| warmup failed (other) | error container: AI 를 시작하지 못했어요. + [다시 시도] |
error container |
[설정으로 가기] 는 R5 의 file-missing 케이스에서 "설정에서 다시 다운로드" 문구의 다음 행동을 한 탭으로 짧게 만드는 보조 권고. 채택은 Architect 재량.
Architect 가 결정해야 할 것 (요약)
- Strong R1, R2, R4, R5 — 채택 또는 명시 거절 (OQ 로 남기지 말 것).
- Suggest R3, R6 + 마이크로카피 사전 + [설정으로 가기] — 재량.
- 새 AC 4건 — 채택 시 README §3 에 통합.