# 손버릇 통계 — 키 입력을 모아 분석할 것인가 > **상태: 구현 (2026-10-23, 기본 꺼짐)** — 데몬 엔진 카운터 · DB v6 `key_stats` · > `kmd keymap stats` · dojo 결과 카드의 "실전 7일" 줄. 2026-10-24 여러 기기 합쳐 보기(§6) 추가. > 관련: [10_dojo_plan.md](10_dojo_plan.md)(도장 안의 기록), [16_keymap_ergonomics.md](16_keymap_ergonomics.md)(배치 근거), > [13_capslock_trigger.md](13_capslock_trigger.md)(트리거·300ms) ## 1. 결론 — 모은다. 단, 키 입력이 아니라 "수(手)의 횟수"만 키맵을 고치는 근거는 지금까지 설계 원칙(docs/16)과 사용자의 느낌뿐이었다. 실제로 어느 수를 쓰는지, 레이어를 두고도 손이 화살표로 가는지, 트리거를 얼마나 쥐는지는 아무도 몰랐다. 이것을 알면 세 가지를 할 수 있다. 1. **나를 본다** — 도장(dojo)은 도장 안의 실력만 잰다. 실전에서 레이어가 몸에 붙었는지는 맨 화살표 비율로 드러난다("화살귀에게 지고 있다"). 2. **배치를 고친다** — 한 번도 안 쓰는 수는 빼거나 익히고, 많이 쓰는 수는 센 손가락에 둔다. docs/16 의 원칙을 숫자로 검증한다. 3. **타이밍을 맞춘다** — 트리거 홀드 시간 분포와 "쥐었다 그냥 놓음" 횟수는 tap-hold 판정 시간 (`tap_hold_ms`)·탭 동작(`tap_action`)을 정하는 근거가 된다. 하지만 데몬은 손쉬운 사용(접근성) 권한으로 **모든 키**를 본다. 글자를 모으는 순간 키로거다 — 비밀번호, 메시지, 검색어가 남는다. 그래서 원칙을 하나로 못 박는다. > **글자는 세지 않는다.** 레이어가 실행한 수, 레이어 밖에서 누른 맨 이동 키, 트리거를 쥔 시간만 > 센다. 순서·시각은 남기지 않고 날짜별 합계만 둔다. 기본은 꺼짐. 사용자가 공유 폴더(§6)를 > 정하지 않으면 이 기기 밖으로 나가지 않는다. ## 2. 모으는 것 | 지표 | 이름 (`key_stats.metric`) | 언제 센다 | 왜 | |---|---|---|---| | 레이어 수 | `move:<레이어>:<키>` | 레이어 매핑이 실행될 때 (마우스 레이어 포함, 오토리피트는 1회) | 많이 쓰는 수 / 안 쓰는 수 | | 맨 이동 키 | `raw:<키>` | 레이어가 꺼진 채 화살표·Home·End·PgUp·PgDn 을 누를 때 | 홈로우 이탈("화살귀") | | 트리거 홀드 | `hold:<0~10>` | 레이어 트리거를 놓을 때, 쥔 시간 100ms 칸 (10 = 1초 이상) | tap-hold 판정 근거 | | 스침 | `idle:<레이어>` | 트리거를 쥐었다가 아무 수도 안 쓰고 놓을 때 (탭 동작을 낸 탭은 빼고) | 오발·망설임 | | 탭 동작 | `tap:<레이어>` | 트리거를 짧게 쳐서 `tap_action`(예: RAlt 탭 = 한영)을 냈을 때. 홀드 분포에도 넣지 않는다 | 스침과 가르기 | | 실행 지연 | `perf:<단계>:<칸>` | 단축키로 런처를 띄울 때 단계별 (§7) | 기기별 성능 | | 느린 훅 | `perf:hookslow` | 키 훅 콜백이 20ms 넘게 걸렸을 때 (Windows) | 입력 끈적임 | 모으지 않는 것: 글자·숫자·기호 키, 레이어 밖의 일반 키, 키 순서, 시각, 앱 이름, 창 제목. 키맵이 꺼져 있을 때(`toggle_keymap`)는 아무것도 세지 않는다. `raw` 는 화살표류 8개뿐이다. 맨 Backspace/Delete 는 타이핑의 일부라 세지 않는다. ## 3. 구조 ```text OS 훅 ─▶ EngineState::process_key ─▶ 결정 (기존과 같음) │ config.stats 일 때만 ▼ KeyStats (HashMap, 엔진 Mutex 안) │ 1분마다 / 데몬 종료 시 (공유 폴더는 5분마다) ▼ keybind::take_key_stats() ─▶ Database::key_stats_add() ─▶ key_stats(day, metric, count) │ kmd keymap stats ◀── KeyReport::build(행, effective_keymap) ──┤ dojo 결과 카드 ◀──────────────────────────────────────────┘ ``` - **훅 경로 부담** — 결정이 난 뒤 해시맵 한 칸을 올릴 뿐이다. 문자열은 만들지 않는다 (`Metric` 은 `Copy` enum, 이름은 꺼낼 때 만든다). 통계가 꺼져 있으면 분기 하나로 끝난다. - **결정 불변** — 통계는 결정을 바꾸지 않는다. `손버릇_통계는_결정을_바꾸지_않는다` 테스트가 같은 입력에 켬/끔 결정 열이 같은지 본다. - **락** — 저장 스레드는 엔진 락을 해시맵을 바꿔 끼우는 동안만 쥔다(같은 용량으로 새 맵 — 훅 경로가 다시 키우지 않게). 지표 이름(문자열)은 락을 놓은 뒤 `StatsBatch::named` 가 만든다. poison 이면 훅과 같은 복구(`lock_engine` + 정리)를 거친다. - **잃지 않기** — DB 쓰기에 실패하면 꺼낸 횟수를 메모리에 두었다가 다음 저장 때 다시 더한다. 데몬이 신호로 끝나도 잃는 것은 마지막 1분 안이다. - **플랫폼** — macOS·Windows 는 데몬 엔진이 센다. Linux 는 kanata 가 키를 처리하므로 아직 없다. - **DB** — v6 마이그레이션이 `key_stats(day TEXT, metric TEXT, count INTEGER, PRIMARY KEY(day, metric))` 를 만든다. 날짜는 SQLite `date('now','localtime')`. ## 4. 쓰는 법 ```bash kmd keymap stats on # 켠다 (config.toml [launcher.keymap] stats = true) → kmd daemon restart kmd keymap stats # 최근 7일 보고서 kmd keymap stats --days 30 kmd keymap stats off # 끈다 (모인 것은 남는다) kmd keymap stats clear # 모두 지운다 ``` 보고서는 많이 쓴 레이어 수(배치상 대상과 함께), 레이어 밖 맨 이동 키, 트리거 홀드 분포, 그리고 사부의 해석을 보여 준다. - 맨 화살표 비율 = 맨 이동 키 ÷ (맨 이동 키 + 이동 대상 레이어 수). 5% 미만이면 "홈로우를 떠나지 않는다". - 홀드 20번 이상에서 스침이 15% 넘으면 알린다. - 레이어 수가 50번 넘게 모였는데 nav 에서 한 번도 안 쓴 수가 있으면 나열한다. dojo 결과 카드(`kmd dojo card`, 도장 안 C)는 모인 것이 있으면 "실전 7일 레이어 수 N번 · 맨 화살표 P%" 한 줄을 더한다. ## 5. 다음 후보 (아직 안 함) - ~~여러 기기 합쳐 보기~~ → §6 으로 구현. - **S19 배치 진단과 잇기** — "안 쓰는 수" 를 진단 화면에 올리고, 많이 쓰는 수가 약한 손가락(새끼)에 있으면 卷二·三 규칙으로 짚는다. - **더블탭 간격 분포** — 줄 처음/끝(2탭 300ms)의 두 번째 탭 간격. vim-nav.kbd(200ms)와 엔진(300ms)의 차이를 정할 근거가 된다. - **dojo 오늘의 수련 출제** — 실전에서 안 쓰는 수를 오늘의 수련에 더 자주 낸다. - **Linux(kanata)** — kanata 는 로그 외에 사용 통계를 내지 않는다. TCP 서버 이벤트로 레이어 전환은 받을 수 있지만 키별 횟수는 어렵다. ## 6. 여러 기기 합쳐 보기 — 맥과 윈도우의 손을 나란히 로컬에만 있으면 한 기기의 버릇밖에 못 본다. 맥과 윈도우를 함께 쓰면 **같은 배치가 두 OS 에서 같은 일을 하는지**, 차이가 배치 탓인지 환경 탓인지가 개선의 핵심 질문이 된다. 서버는 두지 않는다 — 사용자가 이미 쓰는 동기화 폴더(iCloud Drive·OneDrive·Dropbox·Syncthing·git 저장소)에 기기마다 파일 하나를 쓴다. ```text 맥 데몬 ──5분마다──▶ <공유 폴더>/kmd-keystats-MAC.json ┐ 윈도우 데몬 ─5분마다─▶ <공유 폴더>/kmd-keystats-WORK-PC.json ┤ 동기화 도구가 옮김 ▼ kmd keymap stats compare (아무 기기에서나) → 마크다운 비교표 + 해석 ``` ### 6.1 스냅숏 `kmd-keystats-<기기>-.json` = `{version, device, id, os, exported, mapped, rows}`. - `rows` — 최근 180일의 `(날짜, 지표, 횟수)`. §2 의 지표 그대로라 **글자는 여기에도 없다**. - `mapped` — 그 기기의 배치 `(레이어, 키, 대상)`. 맥은 `Alt+Left`, 윈도우는 `Ctrl+Left` 처럼 OS 마다 대상이 달라도 **같은 물리 키(레이어, 키)를 축**으로 맞대어 본다. - 기기마다 파일을 통째로 다시 쓴다(임시 파일 → 이름 바꾸기). 더하지 않으므로 두 번 세지 않고, 동기화 충돌도 기기별 파일이라 생기지 않는다. - 기기 이름은 호스트 이름(윈도우 `COMPUTERNAME`, 유닉스 `gethostname`의 첫 마디). 호스트 이름이 같은 두 기기가 서로 덮어쓰지 않게 설치마다 임의 id(`<데이터 폴더>/device-id`, 8자리)를 붙인다. - `launch:` 대상은 `launch` 로 가린다 — 경로나 명령이 담길 수 있어서다. - `sync` 의 경로는 절대 경로로 풀어 저장한다(데몬의 작업 폴더는 셸과 다르다). 풀리지 않은 `%USERPROFILE%`·`$HOME` 은 거절한다. ### 6.2 쓰는 법 ```bash # 각 기기에서 한 번 kmd keymap stats on kmd keymap stats sync ~/Library/Mobile\ Documents/com~apple~CloudDocs/kmd # 맥 예 kmd keymap stats sync "$env:USERPROFILE\OneDrive\kmd" # 윈도우 PowerShell 예 kmd daemon restart # 아무 기기에서나 kmd keymap stats compare # 이 기기(DB, 가장 새로움) + 공유 폴더의 다른 기기들 kmd keymap stats compare --days 30 kmd keymap stats compare a.json b/ # 폴더 대신 파일·폴더를 직접 kmd keymap stats export [경로|-] # 수동 내보내기 (USB·메신저로 옮길 때) kmd keymap stats sync off ``` 두 기기가 같은 동기화 서비스를 쓰지 않으면 `export` 로 파일을 만들어 아무 방법으로 옮긴 뒤 `compare <파일>` 하면 된다. ### 6.3 비교 보고서가 읽는 것 `Compare::insights` 는 판단 하한(기기마다 3일·레이어 수 100번) 아래의 기기는 "보류"로 빼고 읽는다. | 신호 | 해석 | 다음 행동 | |---|---|---| | 맨 화살표 비율이 기기 사이 10%p 이상 차이 | 배치가 아니라 **환경** — 그 기기에서 레이어가 안 먹는 앱(원격 데스크톱·게임·관리자 권한 창)이나 외장 키보드 | 그 기기에서 언제 화살표로 가는지 관찰, 앱 예외·권한 확인 | | 어디서나 많이 쓰는 수 | 배치의 첫 기준 — 검지·중지 자리에 있어야 함 | 새끼손가락 자리(`;` `'` `/` `A` `Q` `Z` `P` `[` `]`)면 옮길 후보 | | 한 기기 3% 이상 vs 다른 기기 0.5% 미만 | 대상이 같으면 그 OS 앱에서 안 먹음, 다르면 그 OS 대상이 틀림 | 대상 열을 보고 OS별 매핑 수정 (예: 윈도우 단어 이동이 앱에서 안 먹음) | | 어느 기기에서도 안 쓴 nav 수 | 몰라서 안 쓰는지, 필요 없는지 | 도장에서 익히거나(오늘의 수련) 빼서 자리 확보 | | 200ms 안 홀드 비율 15%p 이상 차이 | 기기마다 손·키보드가 다름 | `tap_hold_ms` 를 기기별로 (트리거 탭 동작이 생길 때 특히) | | 그냥 놓음 15% 이상 | 트리거 오발·망설임 | 트리거 자리, `tap_action` 점검 | | 기기마다 대상이 다른 수 | 대개 ⌥/Ctrl 관례 — 정상 | 같은 손짓이 같은 일을 하는지만 확인 | ### 6.4 개선 루프 1. **2주 모은다** — 두 기기 모두 하한(3일·100번)을 넘길 때까지는 판단하지 않는다. 2. **`compare --days 14` 를 읽는다** — 표와 해석은 마크다운이라 그대로 붙여 넣어 함께 분석할 수 있다. 3. **한 번에 하나만 바꾼다** — 매핑 하나, 판정 시간 하나. 바꾼 날을 기록해 둔다(배치 변경 커밋). 4. **다시 2주** — 같은 지표가 움직였는지 본다. 맨 화살표 비율·새끼손가락 수 비율·그냥 놓음 비율이 주 지표다. 5. 바뀐 배치가 두 기기 모두에서 나아지면 프리셋(vim-nav)에 올리고, 한 OS 에서만 나아지면 OS별 프리셋 분기로 둔다. ### 6.5 지키는 것 - 공유 폴더에 가는 것도 §2 의 횟수와 배치뿐이다. 클라우드 동기화 폴더를 고르는 것은 사용자다 — `sync` 를 정하지 않으면 아무 데도 쓰지 않는다. - 비교에서 이 기기 것은 공유 폴더의 파일 대신 DB 에서 바로 읽는다(가장 새롭다). ## 7. 실행 지연 — "윈도우에서 CapsLock+Space 가 굼뜨다" 키 엔진은 맥과 윈도우가 같다. 그런데 단축키를 누르고 런처를 쓸 수 있을 때까지는 엔진 밖의 단계가 더 길다. 단계를 나눠 재야 "노트북 탓인지, 설정 탓인지" 를 가를 수 있다. ```text Space down ──(트리거 대기)──▶ 데몬 Launch ──(프로세스 생성)──▶ 런처 main() ├─(창 준비)──▶ 창 id └─(검색 준비)─▶ 첫 검색 엔진 단축키 → 사용 가능 = 프로세스 생성 + max(창 준비, 검색 준비) (트리거 대기는 따로) ``` | 단계 | 지표 | 재는 곳 | 늦으면 | |---|---|---|---| | 트리거 대기 | `perf:wait` | 데몬 엔진 — Launch 키 down → 실행 결정 | 수정자 트리거라 해제까지 미룸 | | 프로세스 생성 | `perf:spawn` | 데몬이 넘긴 `KMD_LAUNCH_AT_MS` → 런처 `main()` | 백신 실시간 검사·디스크·절전 | | 창 준비 | `perf:window` | 런처 `main()` → 창 id (iced/wgpu/DirectComposition) | GPU 초기화 — `renderer = "software"` 와 비교 | | 검색 준비 | `perf:engine` | 런처 `main()` → 첫 엔진 적용 | 인덱스 크기 | | 단축키 → 사용 가능 | `perf:total` | 위의 합 | | 칸은 `<25 <50 <100 <150 <200 <300 <500 <800 <1200 <2000 2초+` (ms). 보고서는 칸으로 중앙값·90%를 읽는다. 런처는 단축키로 떴을 때(환경 변수가 있을 때)만 기록하고, 통계가 꺼져 있어도 desktop.log 에 `실행 지연:` 한 줄은 남긴다. 환경 변수는 읽자마자 지워 런처가 띄우는 앱에 물려주지 않는다. `compare` 는 기기 사이 "단축키 → 사용 가능" 중앙값이 두 칸 이상 벌어지면 차이가 가장 큰 단계를 짚는다 — 프로세스 생성·창 준비가 벌어졌으면 엔진이 아니라 기기(성능·GPU·백신) 쪽이다. ### 7.1 함께 고친 것 — CapsLock 트리거는 해제를 기다리지 않는다 레이어 Launch 는 트리거 키를 뗄 때까지 미뤄졌다(0.5.0 — Alt 트리거가 눌린 채 새 프로세스가 뜨면 포커스/IME 조합에 간섭). CapsLock 은 수정자가 아니고 데몬이 억제해 OS 에 보이지 않으므로 이 이유가 없다. 이제 **수정자 트리거일 때만** 미루고, CapsLock 트리거는 Space 를 누르는 순간 띄운다 — 트리거를 떼는 시간(대개 100~200ms)만큼 빨라진다. macOS 의 45ms 지연(첫 한글 조합 보호)은 그대로다. ### 7.2 남은 처방 (재 보고 정한다) - **상주 모드** — 지금은 단축키마다 런처 프로세스를 새로 띄우고 닫으면 끝낸다(싱글턴 토글). 프로세스 생성·창 준비가 크게 나오면, 런처를 숨겨 두었다가 보여 주는 상주 모드가 근본 처방이다 (메모리 상주 비용과 맞바꿈). - **Windows 백신 예외** — 프로세스 생성이 유독 크면 Defender 실시간 검사가 매번 실행 파일을 훑는 것이 흔한 원인이다. - **renderer** — 창 준비가 크면 `general.renderer = "software"` 로 하루 재 보고 비교한다. ### 7.3 단축키 경로 기록 — 키를 누른 순간부터 2026-11-07 분석: 사용자가 맥·윈도우에서 "하루 첫 실행이 5초 넘게 걸린다"고 했지만, 맥 기록 380회에서 `main()`→창 준비가 1초를 넘은 건 한 번뿐이었다. 지연은 측정 밖에 있었다. - **키→데몬 구간이 비어 있었다.** 이제 훅이 물리 keyDown 마다 시각과 OS 타임스탬프 기준 지연을 남기고, Launch 가 `KMD_LAUNCH_TRACE` 로 자식에게 넘긴다. 런처는 desktop.log 에 `단축키 경로: 키→훅 2ms · 훅→실행 1ms · 전경 앱 조회 0ms · 프로세스 생성 15ms (창을 띄움)` 을 남긴다. macOS 는 `CGEventTimestamp`(Apple Silicon 에선 mach tick 으로 오기도 해 둘 다 해석), Windows 는 `KBDLLHOOKSTRUCT.time`(GetTickCount) 를 쓴다. - **창을 차지한 실행만 기록됐다.** 실행이 막혀 여러 번 누르면 늦게 누른 빠른 값만 남았다. 기존 창을 닫고 끝나는 실행도 같은 줄을 `(기존 창을 닫음)` 으로 남긴다. - **첫 프레임이 없었다.** iced 는 창을 숨긴 채 만들고 첫 UI 를 지은 뒤 보이게 한다. `첫 프레임 Nms` 를 남긴다 (`window::frames()` 를 첫 프레임까지만 구독). - **데몬 로그가 없었다.** 데몬은 stdout 으로만 로그를 냈고 launchd 아래에서는 버려졌다(2026-09 이후 daemon.log 정지). 이제 `start` 는 `daemon.log` 에 직접 쓴다(백그라운드 스레드, 5MB 넘으면 시작 때 `.log.1` 로). 느려지면 그 시각 근처의 `단축키 경로:` 줄을 본다. 줄이 아예 없으면 키가 데몬에 닿지 않은 것이다(보안 입력 등). ### 7.4 런처 예열 새 바이너리를 처음 실행하면 macOS 가 서명 검사(amfid)와 Gatekeeper 스캔을 한다 — 실측 0.4~0.6초, 이후 5~14ms. 배포 다음 날 아침 첫 실행에서 실제로 걸렸다. Windows 는 백신 검사가 같은 자리다. 데몬이 시작할 때(업데이트 직후 포함)와 절전에서 깬 직후 `kmd-desktop --version` 을 한 번 띄워 그 비용을 미리 치른다. 절전은 절전을 포함하는 시계와 빼는 시계의 차이로 안다(macOS·Linux: 벽시계 대 `Instant`, Windows: `GetTickCount64` 대 `QueryUnbiasedInterruptTime`). 폰트·Metal 같은 창 쪽 콜드 비용은 그대로다 — 캐시를 비운 실측 약 1초.