루프 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 —
#tourdialog 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, 대비 개선. 완벽은 아니지만 "쓸 만한" 범위에 진입.
다음에 하고 싶은 것
- 세 번째 언어(일본어) — DICT.ja 하나 추가하면 됨. 영어 기반 현지화 테스트가 이미 탄탄해서 리스크 낮음.
- 실기기 iOS/Android 베타 — 실제 Safari/Chrome-mobile의 safe-area-inset, 햅틱, prefers-reduced-motion, VoiceOver 동작 확인.
- Lighthouse 감사 — Performance, Accessibility, Best Practices, SEO. 특히 a11y 점수 보고 싶음.
- 도메인 토글 단축키 — 현재 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 = { ... }+detectBrowserLocale에ja분기 +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%+.