NavilIMEforMac
맥OS 용 한글 입력기 (Hangul Input Method for MacOS
Install / Use
/learn @navilera/NavilIMEforMacREADME
** 주의 **
이 문서의 업데이트는 코드 업데이트보다 느림.
그러므로 최신 코드와 이 문서의 설명이 다를 수 있음.
커밋 히스토리를 추적해서 문서의 내용과 코드의 변경이 어떻게 이뤄졌는지 보는것도 추천함.
지원하는 한글 자판
- 세별식 318
- 세별식 390
- 두벌식
나빌 입력기 for 맥
나는 내가 디자인한 한글 세벌식 자판을 쓰고 있다.
https://github.com/navilera/318Na_HangulKeyboard
위 링크에서 관련 정보를 찾을 수 있다. 글자판의 이름은 318Na 자판이다.

위 그림이 318Na 자판의 배치다.
이 자판을 디자인하고 나서 실제로 사용하려고 나는 리눅스와 윈도우에서 직접 입력기를 만들었다. 리눅스에서는 libhangul을 수정하는 것으로 318Na 자판을 쓸 수 있었다.
https://github.com/navilera/libhangul
위 링크에서 내가 수정한 libhangul 코드를 볼 수 있다.
윈도우에서는 위 libhangul을 이용해서 입력기를 새로 만들었다.
https://github.com/navilera/NavilIME
위 링크에서 내가 만든 윈도용 입력기 코드를 얻을 수 있다. 이름은 나빌입력기이다. 나빌입력기는 지금도 내가 아주 잘 쓰고 있다. 그리고 쓰는 사람이 몇 명 더 있다고 한다. 이 자리를 빌어 감사를 표한다.
그렇게 나는 내가 필요한 환경에서 318Na 자판을 충분히 잘 쓰고 있었다. 나는 맥북 등 액OS 계열 제품을 쓸 생각이 없었기 때문에 리눅스와 윈도우에서 쓰면 충분했다. 그리고 윈도우에서 한글 입력기를 만들면서 충분히 즐거웠다.
강제로 맥을 쓰게 되다
회사를 옮겼다. 이직한 회사에서는 업무용 랩탑으로 맥북만 쓸 수 있다. 다른 선택은 없었다. 회사에서 일하면서 한글을 쓸 일은 그리 많지 않다. 그래도 업무상 채팅 등 으로 종종 한글을 쓸 일이 생겼다. 어쩔 수 없이 맥에서는 두벌식을 쓰는데 더이상 견딜 수가 없었다.
맥에서 318Na를 쓸 수 있는 입력기를 만들어야 겠다고 결심했다.
구름 입력기를 고려해보다
맥에서 기본 한글 입력기를 제외하고 가장 많이 사용하는 입력기는 구름 입력기다. 그리고 구름 입력기는 libhangul을 사용한다. 그래서 나는 내가 리눅스와 윈도우에서 사용하는 libhangul을 쓰기만 하면 구름 입력기를 이용해서 318Na 자판을 쓸 수 있을 줄 알았다.
나는 libhangul 라이브러리의 구조를 알고 있다. 그래서 구름 입력기도 리눅스에 ibus처럼 libhangul로부터 자판 정보를 받아서 여러 한글 자판을 지원하는 줄 알았다. 구름 입력기 소스 코드를 분석해 보니 여러 한글 자판을 별도로 등록해야 하고 자판 하나당 변경 혹은 추가해야 하는 코드가 여러 군데에 있었다. 그냥 318Na 자판을 추가하려고 시도하면 분명 필요한 코드를 빼먹을 것 같았다. 잘 찾아보고 하나씩 넣어 보면서 시도하면 될 것도 같았다. 그런데 그냥 하기 싫었다.
물론 이후 나빌 입력기를 구현하는데 있어서 구름 입력기의 소스 코드를 많이 참고했다.
그냥 내가 하나 만들까
이왕 이렇게 된거. 구름 입력기 활용하기엔 공부해야 할 것이 너무 많고.. 무엇보다 구름 입력기를 한 번에 빌드하지 못 했다. 빌드를 하려면 뭔가 추가적인 작업을 더 해야 했다. 그래서 그냥 내가 직접 새로 만들기로 마음을 정했다.
컨셉은 명확했다. 순수 swift로 만들 것. 318Na 자판을 지원할 것. 단순할 것.
무엇보다 아마도 어차피 나만 쓸 것이기 때문에 다른 자판은 고려 대상이 아니었다. 그래서 libhangul 말고 오토마타도 내가 직접 구현하기로 결정했다.
나빌 입력기 for 맥
완성한 나빌 입력기는 순수 swift로만 구현했다. 전체 코드는 오토마타를 포함해서 1000줄 정도다. 공백을 포함한 코드 라인 수니까 순수 코드는 몇 백 줄일 것이다. 모든 목적을 달성했다. 무엇보다 코드가 아주 간단하다는 것이 마음에 든다.
코드 줄 수가 적으므로 이 글에서 나빌 입력기 코드를 한 줄 씩 설명하려고 한다.
나는 나빌 입력기를 오토마타와 입력기 두 부분으로 구성했다.
오토마타
오토마타를 만들면서 잠재적으로 예상되는 범용적 상황을 고려하긴 했다. 그러나 현 시점에서 나빌 입력기의 오토마타는 318Na 자판만 구현했다. 별 어려움 없이 한글 두벌식이나 다른 여러 세벌식 자판을 추가할 수 있을 것이라 생각한다.
오토마타는 다음 파일 네 개로 구현했다.
- Hangul.swift
- Keyboards.swift
- Keyboard318.swift
- Testcases.swift
실제 오토마타 엔진에 대한 구현은 Hangul, Keyboards, Keyboard318 이렇게 세 파일이다. Testcases.swift 파일은 오토마타 기능을 검증하는 테스트 케이스들이 들어 있다.
이제부터 오토마타 구현 소스를 한 줄씩 모두 설명하겠다.
Hangul.swift
오토마타 코어 구현이다. 실질적인 오토마타 상태 머신 (state machine) 구현이 있는 파일이다.
Composition
struct Composition {
var chosung:String = ""
var jungsung:String = ""
var jongsung:String = ""
var done:Bool = false
var Size:UInt {
return UInt(self.chosung.count + self.jungsung.count + self.jongsung.count)
}
}
구조체 이름이 컴포지션이다. 컴포지션을 영어 사전에서 찾으면 이런 뜻이다.
명사) 조립, 구성, 합성
한글은 초성, 중성, 종성을 조립하여 한 글자를 만든다. 그래서 Composition 구조체는 한글 한 글자를 구성하는 정보를 가진다.
- chosung : 한글 초성
- jungsung : 한글 중성
- jongsung : 한글 종성
- done : 조합 완료 표시
- Size : 컴포지션 구조체 크기
각 멤버 변수의 의미는 위와 같다. 각 멤버 변수를 어떻게 활용하는지는 이어질 코드를 보면 알게 된다.
Automata
오토마타 클래스는 말 그대로 오토마타 구현이 있다. 사실 한글 오토마타는 자판(두벌식, 세벌식 등) 마다 다르므로 자판에서 상당 부분 처리해야 한다. 그런데 생각해보면 가장 근본적인 한글 오토마타는 그대로 있고 자판에서는 오토마타에 전달해주는 입력을 결정한다. 따라서 오토마타는 한글 자판에서 명확하게 초성, 중성, 종성을 구분해서 넘겨준다는 가정을 하고 동작한다.
struct Automata {
// 현재 작업 중인 입력 시퀀스
var current:[String]
// 키보드
var keyboard:Keyboard
init(kbd:Keyboard) {
self.current = []
self.keyboard = kbd
}
스위프트에서 구조체(struct)와 클래스(class)는 상속 가능 유무 외에는 거의 차이가 없다. 더 파고들면 다른 큰차이가 있겠지만 나빌 입력기를 구현하고 코드를 이해하는 수준에서는 상속할 필요가 있으면 클래스, 상속할 필요가 없으면 구조체다.
나는 Automata를 구조체로 선언했다. 왜냐면 Automata 클래스는 다른 클래스가 상속하지 않기 때문이다. 구조체를 클래스로 바꿔도 동작은 동일하다. 이글에서는 더이상 구조체와 클래스는 구분하지 않고 그냥 클래스라고 부르겠다.
오토마타 클래스의 시작 부분은 위 코드와 같다. 클래스 변수는 아래 두 개다.
- current
- keyboard
current 변수는 오토마타가 처리해야 하는 입력이다. 한글 한 글자는 키보드에서 최소 입력 두 개에서 최대 입력 여섯 개 까지 필요하다. 그래서 키보드 입력을 아스키 코드(ascii)의 배열로 저장한다. 그러므로 나빌 입력기의 오토마타는 한 문장으로 다음과 같다.
아스키 코드의 배열을 유니코드 한글로 변환하는 변환기
keyboard 변수는 오토마타에 한글 자모를 전달하는 키보드다. 현 시점에서는 318Na 자판만 있다. 추후에 내가 기분이 좋거나 할 일이 없거나 정말 누군가 거절할 수 없는 제안을 하거나 다른 사람이 만들어 준다면 다른 자판 구현체가 이 keyboard 변수에 인스턴스로 들어갈 수 있다.
그 아래 init() 함수는 스위프트의 클래스 생성자다. 생성자이므로 클래스 변수를 초기화한다. current 변수는 빈 배열이다. keyboard 변수에는 생성자의 파라메터로 받은 자판 인스턴스를 넣는다.
func chosung(comp: inout Composition, ch:String) {
if comp.chosung == "" {
// 초성 입력이 처음이면 채움
comp.chosung = ch
} else {
// 초성 입력이 이미 있는데
if comp.jungsung != "" {
// 중성도 있으면 조합 종료
comp.done = true
} else {
// 중성은 아직 없음
if self.keyboard.chosung_proc(comp: comp, ch: ch) {
// 쌍자음이면 채움
comp.chosung += ch
} else {
// 쌍자음이 아니면 조합 종료
comp.done = true
}
}
}
}
chosung() 클래스 함수는 사용자가 초성을 입력했을 때 동작을 구현한다. 주석을 잘 써 놨으므로 주석만 봐도 코드 진행을 이해하는데 무리가 없을 것이다. 그래도 설명은 하겠다.
그냥 화면에서 한글이 조합되는 모습을 생각하면 된다. 보통은 초성을 먼저 입력한다. 모아치기를 지원하면 중성을 먼저 입력하는 것도 가능하다. 어느쪽이든 컴포지션에 초성이 없으면 초성을 채워주면 된다.
그럼 나머지는 초성이 이미 있는 상태에서 초성이 또 들어온 경우만 처리해주면 된다. 이 때도 두 가지 경우를 생각할 수 있다. 중성이 있는 경우와 중성이 없는 경우다. 중성이 있는 경우는 초성 + 중성이 완료된 상황이므로 이 시점에 초성이 또 들어온다는 것은 초성 + 중성인 글자는 조합을 완료하는 것이다.
도ᄂ
위와 같은 상황이다. 위 상황에서 앞 글자 '도'는 초성 + 중성이 이미 있다. 이 상태에서 기대하는 입력은 이중 모음을 만드는 중성이거나 종성인데 초성이 들어왔으면 '도'는 조합을 완료하고 초성 'ᄂ'을 조합 중인 상태로 해야한다. 그래서 초성 + 중성 상태에서 중성이 또 들어오면 comp.done = true로 하고 조합을 종료한다.
중성이 없는 상태에서 초성 + 초성에서 기대하는 입력은 쌍자음 뿐이다. 그래서 키보드에게 해당하는 초성 쌍자음이 있는지 검색을 요청한다. 키보드가 쌍자음이 있다고 응답하면 컴포지션 초성에 쌍자음에 해당하는 입력을 넣는다. 키보드에 해당 입력이 쌍자음으로 없으면 조합을 종료한다.
위 과정은 키보드 자판이 무엇이건간에 한글을 조합하는 기본적인 알고리즘이다. 그러므로 위 동작에서 매끄럽게 동작하는 키보드 클래스만 작성하면 아마도 나빌 입력기에서 여러 자판을 사용할 수 있을 것이다.
func jungsung(comp:inout Composition, ch:String) {
if comp.jungsung == "" {
// 중성 입력이 처음이면 채움
comp.jungsung = ch
} else {
