루프 81–99
7번째 블록. 성격이 "기능 폭발"에서 완전히 UX polish + 다국어화로 전환된 첫 블록. 사용자가 /loop의 방향을 명시적으로 고정한 뒤의 자율 루프. 새 feature 한 건도 없이 19 loops — 대신 기존 전 기능이 영어권에서도 "당연하게" 동작하도록.
이번 블록에 한 일
키 리맵
- Loop 81 — A/B/C 슬롯 물리키 → Q/W/E: 영어 키보드에서 KeyA/B/C가 ㅏ 등과 충돌할 여지를 없앰. segId(
ka/kb/kc)·라벨·색·고양이 효과는 그대로.
i18n 인프라
- Loop 82 — i18n 기반:
src/i18n.js모듈.t(key, vars)/getLocale/setLocale/onLocaleChange.navigator.language감지,oiia-locale-v1저장.KO/EN플로팅 토글. 첫 커버: start-hint, DJ·Advanced 토글, TAP, quantize, 투어 4스텝, 주요 토스트. - Loop 83 — 로케일별 KEY_LAYOUT: 한국어엔
ㅜ/ㅣ/ㅏ라벨 +N/L/K바인딩. 영어엔n/i/a라벨 +N/I/A바인딩 (영어권 유저가 직관적으로 N·I·A를 칠 수 있게). 슬롯 라벨A/B/C는 공통 (Q/W/E 바인딩). - Loop 84 — keyhelp + hint i18n:
?오버레이 그리드를KEY_ORDER+keyhelp.*로 동적 재구성..hint본문은hint.body.<locale>+hint.wave+hint.shortcuts세 줄 조합. - Loop 85 — 초기
applyAllI18n순서 버그:app.innerHTML이전에 호출되어 셔플 버튼이 한국어로 남아있던 문제.app.innerHTML직후로 이동.
i18n 커버리지 확장
- Loop 86 — 라이트 테마 top-tools 일관성: 다크/라이트 모두 TAP/Quantize/Lang/Advanced가 배경 대비 선명.
- Loop 87 — 투어 1스텝 EN 본문 교정: EN 본문에 남아있던
ㅜ ㅣ ㅏ제거,N I A + A B C로. - Loop 88 — Advanced 컨트롤 버튼 i18n:
play-all,play-oiia,rec,metro,make-clip,replay-btn,loop-btn,share,share-x,reset,export. 상태 class (.on,.recording) 감지해 active 상태 텍스트는 덮어쓰지 않음. - Loop 89 — 모든
toast()i18n: 15개 토스트 +{n},{name},{sec}변수 보간.replay.window,loop.play,celebrate등. - Loop 90 — confirm·prompt·aria-label: 리셋 확인, BPM 입력, 공유 폴백 prompt,
dj-filterplaceholder, 모든 플로팅 버튼aria-label. - Loop 91 — 34개 DJ 이펙트 설명:
dj.desc.<id>모두 번역.djDesc(id)헬퍼로renderDjSlots에서curr.desc대신 호출. 검색 필터도 번역 기준. - Loop 92 — 세션 통계 + 세그먼트 에디터:
stats.*,seg.play,seg.colorTitle. - Loop 93 — inline title tooltips:
applyAllI18n이titleMap테이블을 스윕해 tooltip 일괄 처리.shuffle-dj·reset-dj·tap·metro·quantize·replay-btn·loop-speed·master-vol·dj-shuffle-inline. - Loop 94 — theme-toggle · DJ-slots 헤더 · load-error: 남아있던 작은 조각들.
- Loop 95 — 최종 런타임 감사: Advanced
#quantize버튼만 한글로 남아있던 걸applyAllI18n에서 덮어씀. Playwright가 DOM 전체 순회해[가-힣]0개 확인.
UX 개선
- Loop 96 — 키보드 a11y:
#start-hint-btn로드 직후focus(),:focus-visible링(파랑 2px / 시작 버튼 흰색 3px) 전역 지정. - Loop 97 — ≤ 380px 모바일 헤더: iPhone SE 320px에서 top-tools 좌측 clipping 되던 문제. 패딩/폰트/간격 축소로 해결.
- Loop 98 — 투어 중 언어 전환:
onLocaleChange에서render()재호출. 사용자가 KO/EN 토글하면 현재 스텝이 즉시 번역됨. - Loop 99 — 최종 감사 + devlog: KO·EN × DJ·Advanced 4조합 전수 확인. 모두 Korean leftover 0개. 본 devlog 작성.
내 생각
"새 기능 금지"가 가장 큰 제약이자 가장 큰 선물이다. 전 블록(loops 61–80)은 기능을 쌓느라 디테일이 섬세하지 못했다. 이번 블록에서 기능 추가를 제약하니 "어디가 덜 매끄러운가"만 보면 되는 탐색 공간이 된다. 결과적으로 19 loops를 자율로 돌려도 리듬이 끊기지 않았음.
t(key, vars)를 작게 직접 구현한 게 정답이었다. i18next 같은 라이브러리를 안 붙이길 잘했음. 11KB 추가 없이 navigator.language + 단순 dict로 필요한 모든 것을 해결. 변수 보간 {n}·{name}·{sec} 정도만. onLocaleChange 구독자 패턴으로 toggle 즉시 반영.
KEY_LAYOUT 로케일 분기가 생각보다 깊이 퍼진 개념이다. KEY_ORDER를 let으로 두고 locale 변경 시 재할당하는 동시에 renderKeys·renderKeyhelp·renderDjSlots·applyAllI18n·bind 텍스트·hint 본문까지 연쇄적으로 다시 그려야 한다. 처음 설계할 때 "키는 상수"로 가정했던 흔적이 곳곳에 있어서 하나씩 풀었다. segments는 여전히 locale-unaware (jamo·latin 필드는 그대로) — 그래야 localStorage 호환 + share 링크가 안 깨짐. 데이터 vs 표현 분리를 명시적으로 유지.
applyAllI18n 순서 실수 (loop 85)가 보여주는 교훈: "런타임 DOM 조작은 DOM이 존재할 때만 의미가 있다." 당연한 말이지만, 모듈 top-level에서 setup*(), let KEY_ORDER = ..., app.innerHTML = ..., 렌더러 호출이 섞여 있으면 순서가 쉽게 어긋난다. 이번 블록에선 두 번 TDZ 유사 버그를 냈고, 스크린샷으로만 발견됨. "정적 감사 → 스크린샷 감사 → Playwright 감사" 세 층을 병행하는 루틴이 정착됐다.
applyAllI18n을 함수로 뽑은 뒤부터 일관성이 생겼다. 로케일 변경 시, 초기 로드 시, 토큰 추가 시 모두 같은 함수 하나만 호출하면 됨. titleMap 같은 작은 데이터 테이블 구조가 코드 양을 크게 줄였다.
EN 로케일에서 jamo key 라벨을 n i a (lowercase)로 한 판단. 유저가 "ㅏ → a"라고 써서 그대로 따랐는데, 결과적으로 슬롯 A B C (uppercase)와 시각적으로 분리되어 좋은 선택이 됐다. 같은 A라도 소문자와 대문자가 역할이 다르다는 신호가 자연스럽게 전달됨.
모바일 320px까지 대응한 건 거의 오버엔지니어링. iPhone SE 사용자가 있긴 하지만 현실적으로 드물다. 다만 padding-right: 44px 같은 한 줄 조치로 커버되니 cost/benefit 이점. "우측 상단에 몰린 5개 버튼"이 뷰포트에 맞는 최대 조밀도임을 확인한 것이 가치.
어떤 걸 i18n 하지 않았나: DJ 이펙트 name 필드 (DISTORT·REVERSE 등). 이건 브랜드·약어이자 영어 약어가 국제 표준이라 의도적으로 고정. 세그먼트 id (o·i·a·ka·kb·kc) 역시 내부 키라서 고정.
느낌 / 셀프평가
- Viral: 95% 유지.
- DJ: 99% 유지 — 내부 동작은 변화 없음, 정확히 같은 것을 영어로도 사용할 수 있게 된 것.
- Mobile: 92% → 96% — 320px까지 대응, i18n 반영 시 오버플로 없음, 포커스 링까지.
- Onboarding: 99% → 99.5% — 투어·힌트·키도움말이 KO/EN 둘 다 완전.
- i18n/a11y: 0% → 90% (새 축). 영어 유저 접근성 + aria-label + 포커스 관리 기반 완성.
다음에 하고 싶은 것
- 3번째 언어 (일본어) — 현재 구조가 쉽게 확장 가능.
DICT.ja = {...}. - 실기기 iOS/Android 모바일 검증 — 여전히 숙제. 특히 햅틱, 포커스 관리, 음성 리더.
- DJ 이펙트
name다국어 변형 실험 — 지금은 고정. 유저가 원할 경우 옵션화. - Advanced 모드 모바일 레이아웃 — 지금 버튼 11개가 하단에 쌓여있음. DJ 모드처럼 top-tools 패턴 확장?
- Storybook-style 다중 스크린샷 자동화 — 현재 수동으로 KO/EN × DJ/Advanced × 좁은/넓은 모바일 조합을 찍음. 한 스크립트로 일괄 캡처.
메모
renderHint가innerHTML을 써서 XSS 위험은 없지만 (문구가 i18n dict에서만 옴) 향후 문구에 사용자 입력이 들어가면 escape 필요.KEY_LAYOUT.ko[1].code === 'KeyL'은 Korean keyboard layout 물리 배치 가정 (쿼티에 한글이 찍혀 있음). ANSI Korean 키보드엔 유효, Dvorak/Colemak 쓰는 한국어 사용자에겐 안 맞음. 유저 수가 적어서 패스.aria-label은 플로팅 버튼에만..key·.dj-slot-wrap는 시각적 라벨이 명확해 생략.:focus-visible은 포인터 클릭 시 나타나지 않음 — 키보드 유저에게만 노출되는 것이 맞음.applyAllI18n이renderKeyhelp·renderHint·renderDjSlots을 호출 → 내부적으로innerHTML재설정 → 이미.firing같은 과도기 상태가 있는 요소는 애니메이션이 중단될 수 있음. 실제 상황에선 로케일 토글 빈도가 낮아 무시해도 됨.- DJ 이펙트 desc 34개를 번역할 때 기술 용어 (LFO, 밴드패스, 크레셴도 등)는 영어 원어 그대로 두고 한국어만 번역. 영어 문장에서도 'low-pass', 'band-pass' 같은 표준 용어 유지.
누적: 99 loops / 34 DJ effects / 7 devlogs / 99+ commits. KO·EN × DJ·Advanced × 320/390/1280 — 모든 조합 Playwright 녹색.