키 배치
학습 경로가 아니라 참고 문서다. 필요할 때 들어와 찾아보는 곳.
목적
"초성 ㄱ 은 어느 키를 몇 번 누르는가" 같은 심볼 → 키열
매핑을 담는 파일 3종을 설명한다.
규칙표(rules.json)가 무엇을 출력할지를 정한다면,
키맵은 그 글자를 어떤 키로 치는지를 정한다. 생성기는 이 둘을
곱해 manipulator 를 만든다.
| 파일 | 내용 | 항목 수 |
|---|---|---|
keymap_390.json(Extensions/n3sh/core/keymap_390.json) |
세벌식 390 배열의 심볼 → 키열 | 79 |
keymap_final.json(Extensions/n3sh/core/keymap_final.json) |
세벌식 최종식 배열의 심볼 → 키열 | 79 |
keymap_final_to_390.json(Extensions/n3sh/core/keymap_final_to_390.json) |
두 배열의 산출 차이 10건만 기록한 대조표 | 10 |
심볼 표기
각 항목은
{"sym": [위치, 문자], "keys": [{key_code, modifiers}, ...]}
형태다.
위치(sym[0]) |
의미 | 문서 표기 |
|---|---|---|
cho |
초성 | 접두 없음 — ㄱ |
jung |
중성 | 접두 없음 — ㅏ |
jong |
종성 | ~ 접두 —
~ㄱ |
lit |
낱자 그대로 출력하는 항목(ex:
ㅒ 한 글자) |
— |
tail |
꼬리 — 수식키 조합·기호·spacebar 여부로 결정되는 후행 키열 | — |
keys 가 2개 이상이면 연타를 뜻한다(ex: 초성
ㄲ = k 두 번).
문서 표기 열은 산문에서 그 심볼을 지칭하는 방법이다
— JSON 값이 아니라 사람이 읽는 표기이며, SSOT 는 notation.md "자모" 다. 같은
ㄲ 이라도 초성(k 연타)과
종성(~ㄲ)이 다른 심볼이므로 접두로 가른다.
매핑 정본 — 이 표가 SSOT 다
아래 두 표가 사람 정본이다. 고친 뒤
python3 tools/keymap_build.py build 로
Extensions/n3sh/core/keymap_{390,final}.json 을 재생성하고
check 로 정합을 확인한다.
셀 표기는 프로젝트 왕복 규약을 따른다 — —=빈 문자열,
␣=공백 1칸. 키열 은 key_code 를
공백으로 잇고 shift 가 필요하면 key+shift 로 적는다(빈
키열은 —). tail 행의 다섯 칸
(⌥ ⇧ ⌘ 기호 sp)은 규칙표의 같은 이름 컬럼과 대응하며,
—(빈 값)과 -(하이픈)는 서로 다른
값이므로 구분해 적는다.
⚠️ 표 위의 <!-- data: ... --> 앵커를 지우면 그
표는 생성 대상에서 빠진다 — 도구가 fail-loud 로 멈춘다.
390 배열
| 종류 | 자모 | ⌥ | ⇧ | ⌘ | 기호 | sp | 키열 | | :--- | :- | : | : | : | :- | :- | :------------------- | | cho | ㄱ | — | — | — | — | — | k | | cho | ㄲ | — | — | — | — | — | k k | | cho | ㄴ | — | — | — | — | — | h | | cho | ㄷ | — | — | — | — | — | u | | cho | ㄸ | — | — | — | — | — | u u | | cho | ㄹ | — | — | — | — | — | y | | cho | ㅁ | — | — | — | — | — | i | | cho | ㅂ | — | — | — | — | — | semicolon | | cho | ㅅ | — | — | — | — | — | n | | cho | ㅇ | — | — | — | — | — | j | | cho | ㅈ | — | — | — | — | — | l | | cho | ㅊ | — | — | — | — | — | o | | cho | ㅋ | — | — | — | — | — | 0 | | cho | ㅌ | — | — | — | — | — | quote | | cho | ㅍ | — | — | — | — | — | p | | cho | ㅎ | — | — | — | — | — | m | | jong | ㄱ | — | — | — | — | — | x | | jong | ㄲ | — | — | — | — | — | 1+shift | | jong | ㄴ | — | — | — | — | — | s | | jong | ㄶ | — | — | — | — | — | s+shift | | jong | ㄷ | — | — | — | — | — | a+shift | | jong | ㄹ | — | — | — | — | — | w | | jong | ㄺ | — | — | — | — | — | d+shift | | jong | ㅀ | — | — | — | — | — | v+shift | | jong | ㅁ | — | — | — | — | — | z | | jong | ㅂ | — | — | — | — | — | 3 | | jong | ㅄ | — | — | — | — | — | x+shift | | jong | ㅅ | — | — | — | — | — | q | | jong | ㅆ | — | — | — | — | — | 2 | | jong | ㅇ | — | — | — | — | — | a | | jong | ㅈ | — | — | — | — | — | 1+shift | | jong | ㅊ | — | — | — | — | — | z+shift | | jong | ㅌ | — | — | — | — | — | w+shift | | jong | ㅍ | — | — | — | — | — | q+shift | | jong | ㅎ | — | — | — | — | — | 1 | | jung | ㅏ | — | — | — | — | — | f | | jung | ㅐ | — | — | — | — | — | r | | jung | ㅑ | — | — | — | — | — | 6 | | jung | ㅓ | — | — | — | — | — | t | | jung | ㅔ | — | — | — | — | — | c | | jung | ㅕ | — | — | — | — | — | e | | jung | ㅗ | — | — | — | — | — | v | | jung | ㅘ | — | — | — | — | — | slash f | | jung | ㅙ | — | — | — | — | — | slash r | | jung | ㅚ | — | — | — | — | — | slash d | | jung | ㅛ | — | — | — | — | — | 4 | | jung | ㅜ | — | — | — | — | — | b | | jung | ㅝ | — | — | — | — | — | 9 t | | jung | ㅟ | — | — | — | — | — | 9 d | | jung | ㅡ | — | — | — | — | — | g | | jung | ㅢ | — | — | — | — | — | 8 | | jung | ㅣ | — | — | — | — | — | d | | lit | , | — | — | — | — | — | comma | | lit | . | — | — | — | — | — | period | | lit | ㄲ | — | — | — | — | — | k k | | lit | ㄸ | — | — | — | — | — | u u | | lit | ㅃ | — | — | — | — | — | semicolon semicolon | | lit | ㅆ | — | — | — | — | — | n n | | lit | ㅉ | — | — | — | — | — | l l | | lit | ㅑ | — | — | — | — | — | 6 | | lit | ㅒ | — | — | — | — | — | r+shift | | lit | ㅖ | — | — | — | — | — | 7 | | lit | ㅘ | — | — | — | — | — | slash f | | lit | ㅙ | — | — | — | — | — | slash r | | lit | ㅚ | — | — | — | — | — | slash d | | lit | ㅝ | — | — | — | — | — | 9 t | | lit | ㅞ | — | — | — | — | — | 9 c | | lit | ㅟ | — | — | — | — | — | 9 d | | lit | ㅠ | — | — | — | — | — | 5 | | lit | ㅢ | — | — | — | — | — | 8 | | tail | — | — | — | — | — | — | — | | tail | — | — | — | — | . | ✔ | period spacebar | | tail | — | - | - | - | — | — | — | | tail | — | - | - | - | — | ✔ | spacebar | | tail | — | - | - | - | . | ✔ | period spacebar | | tail | — | - | - | ✔ | — | — | — | | tail | — | - | - | ✔ | — | ✔ | spacebar | | tail | — | - | - | ✔ | . | ✔ | period spacebar | | tail | — | - | ✔ | - | — | ✔ | spacebar | | tail | — | - | ✔ | - | ? | — | slash+shift spacebar | | tail | — | - | ✔ | - | ? | ✔ | slash+shift spacebar | | tail | — | - | ✔ | ✔ | ! | — | b+shift spacebar | | tail | — | - | ✔ | ✔ | ! | ✔ | b+shift spacebar | | tail | — | ✔ | - | - | — | — | — | | tail | — | ✔ | - | - | — | ✔ | spacebar |
최종식 배열
| 종류 | 자모 | ⌥ | ⇧ | ⌘ | 기호 | sp | 키열 | | :--- | :- | : | : | : | :- | :- | :------------------- | | cho | ㄱ | — | — | — | — | — | k | | cho | ㄲ | — | — | — | — | — | k k | | cho | ㄴ | — | — | — | — | — | h | | cho | ㄷ | — | — | — | — | — | u | | cho | ㄸ | — | — | — | — | — | u u | | cho | ㄹ | — | — | — | — | — | y | | cho | ㅁ | — | — | — | — | — | i | | cho | ㅂ | — | — | — | — | — | semicolon | | cho | ㅅ | — | — | — | — | — | n | | cho | ㅇ | — | — | — | — | — | j | | cho | ㅈ | — | — | — | — | — | l | | cho | ㅊ | — | — | — | — | — | o | | cho | ㅋ | — | — | — | — | — | 0 | | cho | ㅍ | — | — | — | — | — | p | | cho | ㅎ | — | — | — | — | — | m | | jong | ㄱ | — | — | — | — | — | x | | jong | ㄲ | — | — | — | — | — | 1+shift | | jong | ㄴ | — | — | — | — | — | s | | jong | ㄶ | — | — | — | — | — | s+shift | | jong | ㄷ | — | — | — | — | — | a+shift | | jong | ㄹ | — | — | — | — | — | w | | jong | ㄺ | — | — | — | — | — | 2+shift | | jong | ㅀ | — | — | — | — | — | r+shift | | jong | ㅁ | — | — | — | — | — | z | | jong | ㅂ | — | — | — | — | — | 3 | | jong | ㅄ | — | — | — | — | — | x+shift | | jong | ㅅ | — | — | — | — | — | q | | jong | ㅆ | — | — | — | — | — | 2 | | jong | ㅇ | — | — | — | — | — | a | | jong | ㅈ | — | — | — | — | — | 3+shift | | jong | ㅊ | — | — | — | — | — | z+shift | | jong | ㅌ | — | — | — | — | — | w+shift | | jong | ㅍ | — | — | — | — | — | q+shift | | jong | ㅎ | — | — | — | — | — | 1 | | jung | ㅏ | — | — | — | — | — | f | | jung | ㅐ | — | — | — | — | — | r | | jung | ㅑ | — | — | — | — | — | 6 | | jung | ㅓ | — | — | — | — | — | t | | jung | ㅔ | — | — | — | — | — | c | | jung | ㅕ | — | — | — | — | — | e | | jung | ㅗ | — | — | — | — | — | v | | jung | ㅘ | — | — | — | — | — | slash f | | jung | ㅙ | — | — | — | — | — | slash r | | jung | ㅚ | — | — | — | — | — | slash d | | jung | ㅛ | — | — | — | — | — | 4 | | jung | ㅜ | — | — | — | — | — | b | | jung | ㅝ | — | — | — | — | — | 9 t | | jung | ㅟ | — | — | — | — | — | 9 d | | jung | ㅡ | — | — | — | — | — | g | | jung | ㅢ | — | — | — | — | — | 8 | | jung | ㅣ | — | — | — | — | — | d | | lit | . | — | — | — | — | — | period | | lit | ㄲ | — | — | — | — | — | k k | | lit | ㄸ | — | — | — | — | — | u u | | lit | ㅃ | — | — | — | — | — | semicolon semicolon | | lit | ㅆ | — | — | — | — | — | n n | | lit | ㅉ | — | — | — | — | — | l l | | lit | ㅑ | — | — | — | — | — | 6 | | lit | ㅒ | — | — | — | — | — | g+shift | | lit | ㅖ | — | — | — | — | — | 7 | | lit | ㅘ | — | — | — | — | — | slash f | | lit | ㅙ | — | — | — | — | — | slash r | | lit | ㅚ | — | — | — | — | — | slash d | | lit | ㅝ | — | — | — | — | — | 9 t | | lit | ㅞ | — | — | — | — | — | 9 c | | lit | ㅟ | — | — | — | — | — | 9 d | | lit | ㅠ | — | — | — | — | — | 5 | | lit | ㅢ | — | — | — | — | — | 8 | | tail | — | — | — | — | — | — | — | | tail | — | — | — | — | . | ✔ | period spacebar | | tail | — | - | - | - | — | — | — | | tail | — | - | - | - | — | ✔ | spacebar | | tail | — | - | - | - | . | ✔ | period spacebar | | tail | — | - | - | ✔ | — | — | — | | tail | — | - | - | ✔ | — | ✔ | spacebar | | tail | — | - | - | ✔ | . | ✔ | period spacebar | | tail | — | - | ✔ | - | — | ✔ | spacebar | | tail | — | - | ✔ | - | ? | — | slash+shift spacebar | | tail | — | - | ✔ | - | ? | ✔ | slash+shift spacebar | | tail | — | - | ✔ | ✔ | ! | — | b+shift spacebar | | tail | — | - | ✔ | ✔ | ! | ✔ | b+shift spacebar | | tail | — | ✔ | - | - | — | — | — | | tail | — | ✔ | - | - | — | ✔ | spacebar |
왜 역추론에서 선언으로 바꿨나 (2026-08-06)
2026-08-06 이전에는 이 파일이 설명 문서였고, 키맵은
keymap_derive.py(tools/n3sh/keymap_derive.py) 가 원본
오라클에서 역추론했다 —
(출력 글자, 실제 키열) 쌍을 제약으로 두고 미지 심볼이
하나만 남는 레코드부터 확정해 나가는 제약 전파다.
그 구조가 막다른 길을 만들었다. 재배치 이후의 오라클이 없는
배열은 자기 코퍼스로 학습할 수 없다 — 390 을 자기 정본으로
학습시키면 5단계 받침의 1↔︎3 한 칸 밀림 때문에 14행이
다른 글자의 오라클 항목에 매칭돼 출력 21행이 바뀌고 5행이
사라진다. 그중 18행은 교집합 밖이라 오라클 대조가 잡지 못하는
조용한 회귀였다.
애초에 세벌식 자모→키 위치는 자판 정의에서 결정적으로 나오는 값이라 추론할 대상이 아니었다. 그래서 표를 정본으로 올리고, 역추론은 검증 수단으로 남긴다.
| 이전 | 지금 | |
|---|---|---|
| 정본 | 오라클(역추론 결과) | 이 문서의 표 |
keymap_derive.py |
학습기 — 매 실행 재학습 | 검증기 — 선언값과 대조 |
| 배열 분리 | 불가(오라클 부재) | 표를 배열별로 적으면 끝 |
조립 불가 레코드는 검증에서 제외된다
txt 가 빈 레코드는 cho/jung/jong 심볼 자체가 없어
개별 키로 분해되는 압축식 모델을 따르지 않으므로 역추론
검증에서 뺀다(is_forced). 현재 해당 행은
0건이다.
✅ 구 억지 컬럼은 폐지됐다(2026-08-06).
예전에는 손 축약 타건 16행(390)이 여기 걸려 오라클 literal 로만
산출됐다. 그 행들이 필요로 하던 심볼을 이 문서의 표에
선언하면서 전부 자모 조립으로 넘어갔고, 산출물은 바이트
단위로 동일했다 — 표현이 바뀌었을 뿐 동작은 그대로다.
배열 차이
(keymap_final_to_390.json)
227건 중 217건은 두 배열이 동일하고 10건만 다르다. 차이는 세 패턴으로 나뉜다.
| 패턴 | 의미 | 해당 |
|---|---|---|
| A | 트리거 조합 자체가 다름 (from 이 다름) | ㅒ — 최종 g+f ↔︎
390 f+r |
| B | ~ㅈ 키가 다름 |
늦·찾·맞·잊·갖·짖 (최종
3+shift ↔︎ 390 1+shift) |
| C | 종성
겹받침(~ㄺ·~ㅀ) 키가 다름 |
읽·잃·싫 |
짖 은 왜 뒤늦게
들어왔나
이 목록은 원래 두 오라클을 직접 대조해 만들었다.
그런데 390 오라클의 짖 에는 390 전환 때 최종식 값이 그대로
남아 있었다(3+shift). 그 결과 두 오라클이 우연히 같은 값이
되어 "차이 없음" 으로 판정됐고, 실제로는 배열 차이 항목인데 목록에서
빠졌다.
오라클에 결함이 있는 항목일수록 오히려 조용히 누락되는 구조였다는 뜻이다. 그래서 지금은 오라클을 대조하지 않고 두 배열 keymap 으로 산출한 결과를 비교해 검증한다.
python3 tools/verify_array_diff.py # 계산 ↔ 선언 대조 (불일치 시 exit 1)
python3 tools/verify_array_diff.py -v # 판정 경로(calc/oracle)까지 전건 출력
구 억지='✔' 레코드(맞·잃·읽)는 압축식 모델 밖이라 오라클
대조로 판정했다(출력의 판정=oracle). ** 이후 그런 레코드는
0건**이라 전건이 계산 경로로 판정된다. 검증기는 diff_count
= 실제 항목 수, diff_count + identical_count =
total_manipulators 산술도 함께 본다.
패턴 A 는 출력이 아니라 입력 트리거가 달라 오라클 매칭 자체가 실패하므로, 생성기가 이 파일을 배열별 트리거 예외의 SSOT 로 읽는다.
스냅샷 재현성 (2026-07-27 해소)
한동안 keymap_390.json(87 심볼)이
keymap_derive.py 재실행으로 재현되지 않았다(81 심볼).
원인은 스냅샷이 시절 산출물이었기 때문이다 — 그때는
억지 레코드까지 학습에 넣어 실제 자판과 무관한 심볼이 섞였다.
| 구분 | 구 스냅샷(87) | 현행(81) |
|---|---|---|
lit ㄲ |
k+k (억지 축약
타건에서 잘못 학습) |
1+shift (실제 키) |
cho ㄸ ·
jong
ㄱ·ㄺ·ㅄ·ㅅ ·
jung ㅟ · lit . |
있음 | 없음 → 2026-08-06 선언으로 부활(아래) |
jong ㅇ |
없음 | a (정상 학습) |
현행 코드로 만든 build 는 오라클과 전건 일치(390 227/최종식 228)하므로 생성기가 옳고 스냅샷이 낡았던 것이다. 2026-07-27 재생성으로 정합화했다.
심볼별 출처 (2026-07-29)
위 표의 "전부 억지 레코드에서만 나오던 것" 이라는 판정은 심볼마다
출처 레코드를 짚어야 재검증할 수 있다. 아래는 각 심볼을 산출하던 레코드
전체다. idx 는 rules.json 레코드 인덱스이며,
억지 열이 ✔ 인 레코드는 현행 is_forced 가
학습에서 제외한다.
| 심볼 | 산출 레코드 (idx · 트리거 ·
txt) |
억지 | 현행 판정 |
|---|---|---|---|
lit . |
idx0
period+comma → . · idx1
c+v → . |
✔ ✔ | 소거 — 정상 출처 없음 |
cho ㄸ |
idx53 u+1 →
떻 |
✔ | 소거 — 정상 출처 없음 |
jong ㄱ |
idx51 l+period →
적 |
✔ | 소거 — 정상 출처 없음 |
jong ㄺ |
idx44 j+- →
읽 |
✔ | 소거 — 정상 출처 없음 |
jong ㅄ |
idx45 j+comma →
없 |
✔ | 소거 — 정상 출처 없음 |
jong ㅅ |
idx38
h+open_bracket → 넷 · idx49
k+o → 것 |
✔ ✔ | 소거 — 정상 출처 없음 |
jung ㅟ |
idx37 9+s →
윈 |
✔ | 소거 — 정상 출처 없음 |
lit ㄲ |
idx21 k+j →
ㄲ · idx166 1.0+w →
ㄲ |
✔ · — | 유지 — 정상 출처 idx166
이 1+shift 를 준다 |
jong ㅇ |
idx229 u+0 →
등 |
— | 추가 — 처음부터 정상 레코드였고 구 스냅샷이 놓쳤다 |
읽는 법은 두 가지다.
소거 7종은 산출 레코드가 전부 억지다. 억지 레코드는
gen_rules.py가 오라클 키열을 그대로 literal override 로 쓰므로 키맵이 없어도 build 결과가 동일하다. 즉 이 심볼들은 애초에 쓰이지 않았다.✅ 2026-08-06 에 전부 되살아났다. 억지를 없애려면 그 행들이 자모 조립으로 산출돼야 하고, 그러려면 바로 이 7종이 있어야 한다. 지금은 역추론으로 배우는 것이 아니라 위 표에 선언한다 — 그래서 "정상 출처가 없다" 는 사실이 더 이상 결격 사유가 아니다. 선언값이 옳다는 근거는 build 산출물이 바이트 단위로 변하지 않았다는 것이다.
lit ㄲ은 두 출처가 공존한 유일한 사례다. 구 스냅샷은 억지 쪽(idx21,k+j를 눌러ㄲ을 강제 출력)을 학습해 키열을k+k로 잘못 유도했다. 억지를 빼면 정상 레코드 idx166 만 남아 실제 키1+shift가 나온다. 회귀의 직접 원인이 이 오염이었다.✅ 그 공존이 곧 결함이었다. 두 레코드는 서로 다른 글자를 친다 — idx21 은 초성
ㄲ(k연타), idx166 은 종성ㄲ(1+shift). 같은 심볼 이름을 나눠 쓴 것이 문제였지 어느 한쪽이 오염된 것이 아니었다. 지금은 종성 낱자를txt에~ㄲ로 적어 심볼을jong ㄲ로 가르고,lit ㄲ은 초성k k를 갖는다. 제외가 아니라 표현으로 푼 것이며, 학습에서 무엇을 빼야 하는지 고민할 필요 자체가 사라졌다.
재현 명령은 다음과 같다.
python3 - <<'PY'
import sys; sys.path.insert(0, 'tools')
from n3sh import keymap_derive as kd
targets = {('cho','ㄸ'), ('jong','ㄱ'), ('jong','ㄺ'), ('jong','ㅄ'),
('jong','ㅅ'), ('jung','ㅟ'), ('lit','.'), ('lit','ㄲ'), ('jong','ㅇ')}
for i, r in enumerate(kd.load_records):
if kd.is_disabled(r):
continue
hit = {tuple(s) for s in kd.rec_symbols(r)} & targets
if hit:
print(f"idx{i:3d} forced={'Y' if kd.is_forced(r) else 'n'} "
f"{r.get('c1n')!r}+{r.get('c2n')!r} txt={r.get('txt')!r} {sorted(hit)}")
PY
알려진 원본 결함
짖(트리거l+3) 은 390 원본에서~ㅈ이3+shift(최종식 값)로 남아 있다. 같은~ㅈ을 쓰는 다른 5건이 모두1+shift이므로 390 전환 때 남은 미변환 잔재다. 라이브는 2026-07-26 부터1+shift로 정상 동작한다.- 원본 오라클 자체는 수정 금지 백업이므로 그대로 둔다. 생성기가 390
keymap 대로
1+shift를 산출하므로 실사용에는 영향이 없다.
- 원본 오라클 자체는 수정 금지 백업이므로 그대로 둔다. 생성기가 390
keymap 대로
관련
- 규칙표: 배열별 정본 n390/rules.md·final/rules.md(
Extensions/n3sh/layout/final/rules.md) (교집합 뷰 rules_Intersection.md(Extensions/n3sh/layout/rules_Intersection.md))