루프 100–120

8번째 블록. 지난 블록이 "i18n 뼈대 + 번역"이었다면 이번은 "구석구석 polish". 화려한 변화 없이 다들 한 줄짜리 — focus-visible, safe-area-inset, prefers-reduced-motion, aria-live, 라이트 테마 파형 배경, 프리셋 active, firing 피드백 강화. "기본값 UX"가 당연해지는 과정.

이번 블록에 한 일

마일스톤 & 감사

  • Loop 100 — 100 loops 축하: 언어 토글 flash 애니메이션. 단지 상징적 의미. 클릭하면 scale + glow pulse 0.5s.
  • Loop 101 — keyhelp의 📓 개발일지 링크: 마지막 남아있던 한글. keyhelp.devlog 키로 분리.
  • Loop 102 — lang-toggle 터치 타겟 36 → 42px: 엄지 탭 쉽게. 여전히 헤더 폭 안쪽.
  • Loop 113 — state-transition 버튼 전부 i18n: rec → 녹음 중, loop → 루프, make-clip → 준비 중 → 녹음 중 → 저장됨, metro → 🥁 … 등 sed 불가능한 상태 복귀 텍스트까지 전부 t()로.
  • Loop 114 — ㅜ → o (EN): 첫 시도에선 유저 문장을 "n"으로 잘못 읽어 영문 라벨을 n/i/a로 만들었다. 유저가 지적("ㅜ -> o 로 잘 바꿔야 돼") → 정정. Oiiai = O-I-I-A 음운이 유일하게 맞음. 물리키 바인딩은 KeyN 유지. 같은 loop에 active-bar chip들에 role=tablist/tab + aria-selected 붙임.

접근성 (a11y)

  • Loop 96 — 시작 버튼 오토포커스 + 전역 :focus-visible: 키보드 유저가 Tab 없이 바로 Enter.
  • Loop 104 — .key · .dj-pad 키보드 조작 가능: role=button + tabindex=0 + Enter/Space handler + aria-label. 스크린리더 유저가 모든 패드에 닿음.
  • Loop 107 — toast aria-live=polite: BPM 변경·루프 정지·프리셋 적용 등 자동 안내.
  • Loop 108 — prefers-reduced-motion: 시스템이 줄이기 원하면 모든 애니메이션·트랜지션 0.01ms. 전정 증상 있는 유저 대응.
  • Loop 109 — .hint 대비 #888 → #a3a3a3: 13px 본문이 WCAG AA 4.5:1 충족.
  • Loop 110 — 라이트 테마 포커스 링 #0066cc: 밝은 배경에서도 링 보임. 시작 버튼 ring은 #fff → #111 플립.
  • Loop 116 — #tour dialog aria: role=dialog, aria-labelledby/describedby, arrow 링은 aria-hidden.

로케일·다국어 커버리지 마무리

  • Loop 107(이전 블록 연속) — 마지막 런타임 한글: @title="Language / 언어" 하나만 의도적으로 남김. 나머지 0.
  • Loop 115 — 시작 힌트 문구 터치 친화: "아무 키" → "아무데나 탭/키 눌러서 시작". 모바일에선 키보드 없으니 tap 표현 필수.

레이아웃 / 플랫폼

  • Loop 97 — ≤ 380px 헤더 compact: iPhone SE 320px에서 top-tools 좌측 clipping 해결.
  • Loop 103 — iOS safe-area-inset: fps-meter, theme-toggle, top-tools, body.dj-mode #app 네 모서리 전부 notch/home-indicator 대응.
  • Loop 105 — DJ 패드 글자 overflow 감사: OVERDRIVE 같은 9자도 320px 3×3 그리드에 들어감 확인.
  • Loop 106 — DJ 패드 firing 시각 강화: tinted bg + 3px 컬러 border + 28px glow + scale 0.94↔1.03 bounce. 터치 피드백이 만져지는 느낌.
  • Loop 118 — 라이트 테마 firing 톤: 기본 firing 규칙이 #141414와 믹스 → 라이트 패드에선 이질. #fff 믹스로 분기.

라이트 테마 parity

  • Loop 111 — 파형 캔버스 흰 배경 + 어두운 스트로크: ctx.fillStyle='#000' 하드코딩 때문에 라이트 모드에서도 파형 배경이 검정이던 3블록 묵은 버그. drawWaveform()data-theme 체크 추가. 테마 토글 시 즉시 redraw.
  • Loop 112 — 프리셋 active 상태: 현재 적용된 프리셋이 파랑으로 하이라이트. 세션 스코프(새로고침시 초기화) — BPM/DJ 매핑은 저장돼도 "마지막 선택"은 의도적으로 휘발.

세부 / 기타

  • Loop 117 — OS prefers-color-scheme 자동 감지: 첫 방문자가 OS 다크/라이트 선호 따라감. 수동 토글하면 localStorage가 우선.
  • Loop 119 — dj-filter 모바일 인풋 속성: autocomplete/autocorrect/capitalize/spellcheck 모두 off. "distort" 입력할 때 "Distort"로 대문자화되거나 빨간 줄 안 나오게.
  • Loop 120 — <select> 터치 타겟 padding 5→7 + min-height 32px + placeholder contrast. 기본 ::placeholder는 일부 브라우저에서 너무 흐려서 명시.

내 생각

"딜레이 없이 돌려"가 가장 큰 변화. 유저가 직접 말해줘서 나도 리듬이 바뀌었다. 이전엔 ScheduleWakeup으로 25분씩 쉬었는데, 그건 모델 관점에선 캐시 이점 있지만 작업 흐름엔 방해. 계속 연쇄적으로 작은 patch를 쳐내는 게 UX polish 루프의 정답. 19건을 쉼 없이 돌려도 고갈감 없음 — 오히려 피드백 루프가 짧을수록 다음 개선이 눈에 더 잘 들어온다.

사람이 끼어든 순간의 학습 (loop 114): "ㅜ → n"이라는 문장을 나는 "물리 바인딩"으로 읽어서 n을 display로 채웠다. 실제는 "Oiiai 음운의 라틴 표기 = O"였다. Korean/English 매핑에서 소리키보드 위치 는 완전히 다른 축인데 내 머릿속에선 섞여 있었다. 이번 블록의 가장 교훈적인 오류. 다국어화할 때 항상 "사용자에게 보이는 라벨 = 무엇을 의미하나?"를 먼저 정의해야 했음. KEY_LAYOUT을 {segId, jamo, latin, code}로 4-축 분리한 구조가 이런 혼동을 언제든 정정 가능하게 해준 건 다행.

폴리싱의 가성비는 점점 떨어진다. 이번 블록에서 20 loops를 돌렸는데 한 loop당 평균 변경 5-15줄 정도. 초기 loops(41-50, 61-80)는 한 loop가 기능 하나였으니 30-200줄 단위였다. 가치/노력 곡선은 flat해지지만 퀄리티 바닥은 올라간다. 첫 방문자가 아무 곳에서든 "어색하다"를 안 느끼는 상태. 이게 80%에서 95%로 가는 구간의 성격.

라이트 테마 캔버스 버그(loop 111)가 제일 오래 숨어 있었다. CSS로 background: #fff 를 줬으니 동작할 줄 알았는데, 캔버스는 비트맵이라 첫 fillRect('#000', ...) 때문에 항상 검정. CSS와 캔버스 API의 레이어가 다르다는 기본을 실제 에러로 체감. 다음부턴 캔버스 렌더 때 테마 체크를 기본으로 할 것.

aria-live + role=tablist + prefers-reduced-motion 세 개가 이번 블록에서 제일 소중한 3행. 스크린리더 유저, 탭 키보드 유저, 전정 증상 유저 — 세 그룹이 동시에 수혜. 각 3줄 미만 작업인데 임팩트는 "앱을 쓸 수 있냐 없냐" 경계까지 확장.

:focus-visible을 광범위하게 적용한 게 컴포넌트 전체를 재미없게 만들지 않을까 걱정했는데, 오히려 디자인이 단단해진 느낌. "마우스로 찍어도 링이 안 나타남" 이라는 브라우저 기본이 정확히 맞음. 키보드 유저에게만 보이는 그 링이 신뢰의 신호.

느낌 / 셀프평가

  • Viral: 95% 유지.
  • DJ: 99% 유지. 이번 블록은 내부 기능 변화 없이 DJ 패드 firing을 더 또렷하게.
  • Mobile: 96% → 98% — safe-area, 320px, autocomplete off, firing 강화까지.
  • Onboarding: 99.5% 유지.
  • i18n: 90% → 98% — 런타임 한글 leftover 1개(의도적 "Language / 언어"). ctrl state strings까지 다 i18n.
  • A11y: 20% → 80% (크게 점프) — focus-visible, aria-live, role=dialog/tablist/tab, tabindex/role=button on pads, aria-label 플로팅 전부, prefers-reduced-motion, 대비 개선. 완벽은 아니지만 "쓸 만한" 범위에 진입.

다음에 하고 싶은 것

  1. 세 번째 언어(일본어) — DICT.ja 하나 추가하면 됨. 영어 기반 현지화 테스트가 이미 탄탄해서 리스크 낮음.
  2. 실기기 iOS/Android 베타 — 실제 Safari/Chrome-mobile의 safe-area-inset, 햅틱, prefers-reduced-motion, VoiceOver 동작 확인.
  3. Lighthouse 감사 — Performance, Accessibility, Best Practices, SEO. 특히 a11y 점수 보고 싶음.
  4. 도메인 토글 단축키 — 현재 DJ ↔ Advanced는 버튼 클릭만. 키보드 단축키(Cmd+Shift+D 같은)로도 전환 가능.

메모

  • applyAllI18n은 이제 ~40개 엘리먼트를 스윕한다. 성능상 로케일 변경은 드물기 때문에 한 번에 다 돌아도 문제 없음. 그래도 향후 테이블-기반 데이터를 MutationObserver로 자동 동기화하는 게 더 깔끔하긴 할 듯.
  • preset-btn.active는 세션 scope. 새로고침하면 초기화됨. BPM/DJ 매핑이 저장돼도 "마지막 선택"은 명시적으로 휘발 (재시작 시 "선택된 것 없음"이 더 정확한 상태라고 판단).
  • prefers-reduced-motion 적용 시 0.01ms로 하는 이유: 0s하면 animationend 이벤트가 발생 안 해서 의존하는 JS 코드가 먹통. 최소값 1ms 미만을 주면 즉시 완료 + 이벤트 발생.
  • ::placeholder 색을 두 테마 모두 명시한 이유: 일부 Firefox·Edge 버전에서 .dj-filter { color: #eee }가 placeholder 색까지 상속하지 않고 브라우저 기본(너무 흐림)을 쓴다.
  • drawWaveform() 테마 체크는 매 그리기 때마다 DOM 속성 읽음 — 미미하지만 캐시할 수도 있음. 그리기는 세그먼트 편집 중에만 일어나므로 실제 성능 영향 없음.
  • onLocaleChange 리스너가 이제 4개: refreshLabels, KEY_ORDER 재할당+renderKeys+renderDjSlots, tour 재렌더, applyAllI18n. 한 번의 setLocale이 대략 50ms 안에 끝남. 허용 범위.
  • 다음 언어 추가는 DICT.ja = { ... } + detectBrowserLocaleja 분기 + setLocale 밸리데이션에 'ja' 추가 — 3군데.

누적: 120 loops / 8 devlogs / 120+ commits. Playwright 24/24 × KO·EN × DJ·Advanced × 320/390/1280 × 다크/라이트. Korean leftover 0 (의도적 1 제외). a11y 80%+.