한국어(한글), 중국어, 일본어와 같이 문자 수가 많거나 글자 조합이 필요한 언어를 리눅스1에서 입력하기 위해서는 특수한 입력기가 필요합니다. 윈도우나 macOS에서는 이러한 입력기가 운영체제에 기본적으로 통합되어 있어 사용자가 별도로 신경 쓸 필요가 없습니다. 반면, 리눅스에서는 사용하는 배포판에 따라 입력기를 별도로 설치하고 설정해야 하는 경우가 많습니다.

리눅스 환경에서 비 서구권 언어를 입력할 때 입력기와 관련된 문제가 발생하는 경우가 종종 있습니다. 개발자들이 이러한 문제를 효과적으로 해결하기 위해서는 리눅스의 입력기 시스템에 대한 이해가 필요합니다.

이 글에서는 리눅스의 입력기 시스템을 구성하는 다양한 요소들의 역할 및 상호작용 방식, 동작 과정을 자세히 살펴보겠습니다.

1. 입력기와 입력기 프레임워크

보통 입력기는 입력기 프레임워크 위에서 동작합니다. 입력기 프레임워크는 다양한 입력기를 관리하고 통합하는 상위 소프트웨어로, 대표적으로 IBusFcitx가 있습니다.

입력기 자체는 일반적으로 입력기 프레임워크와 별도로 설치해야 합니다. 예를 들어, Fcitx 기반의 한글 입력기는 fcitx5-hangul, IBus 기반의 일본어 입력기는 ibus-mozc 등의 패키지를 설치해야 합니다.

입력기 프레임워크는 사용자의 설정에 따라 애플리케이션의 키 입력을 활성화된 입력기가 처리하도록 중개하는 역할을 합니다. 이 글에서는 편의를 위해 입력기 프레임워크를 따로 구분하지 않고 입력기라고 지칭하겠습니다.

입력기 프레임워크와 입력기

입력기와 입력기 프레임워크

2. 통신 프로토콜 및 애플리케이션

입력기는 기본적으로 클라이언트-서버 구조로 설계되어 있습니다. 애플리케이션은 입력기 데몬과 다양한 IPC(프로세스 간 통신) 방식을 통해 통신하며 입력을 처리할 수 있습니다. 이 과정에서 사용되는 프로토콜은 두 종류로 나눌 수 있는데, 하나는 입력기와 독립적으로 작동하는 표준 프로토콜이고, 다른 하나는 입력기 개발자가 독자적으로 개발한 고유 프로토콜입니다. 애플리케이션은 이러한 통신 방식들 중 하나를 선택하여 작동합니다.

표준 프로토콜

클라이언트와 입력기 간의 통신에 사용되는 표준 프로토콜들은 다음과 같습니다.

X Input Method (XIM)

XIM은 1990년대에 X Consortium이 제정한 프로토콜로, X11 서버를 통해 입력기와 클라이언트가 통신하는 방법을 정의한 것입니다. 만들어진 지 30년이 넘은 낡은 프로토콜이기 때문에 최근에는 잘 사용되지 않지만, 호환성을 위해 대부분의 입력기가 지원하고 있습니다. X11 기반이지만 Wayland 환경에서도 X11 과의 호환성 레이어인 XWayland를 사용하는 경우 XIM 프로토콜을 사용할 수 있습니다.

입력기를 설치할 때 자주 설정하는 다음 환경변수는 XIM 프로토콜에서 사용할 입력기를 지정합니다.

export XMODIFIERS=@im=ibus

Wayland

X11을 대체하기 위해 설계된 디스플레이 프로토콜인 Wayland 또한 입력기를 위한 프로토콜을 포함하고 있습니다. Wayland의 입력기 프로토콜은 2번이나 변경되었으며, 최신 버전인 text-input-unstable-v3도 아직 unstable이 붙어있는 상태입니다. 하지만 현 시점 text-input-unstable-v3는 사실상 Wayland의 입력 프로토콜 표준에 가깝게 사용되고 있는 것으로 보입니다. 특이한 점으로는 XIM이 그저 X11 서버를 통해 애플리케이션과 입력기가 통신하는 방식이라면, text-input-unstable-v3는 중간에서 Wayland 컴포지터가 직접 관여하는 구조입니다.

독자적인 프로토콜

IBus, Fcitx, Uim 등 대부분의 입력기는 표준 프로토콜 이외에도 독자적인 통신 프로토콜을 구현하고 있습니다. IBus와 Fcitx의 독자 프로토콜은 D-Bus라는 리눅스의 IPC 시스템 위에서 통신하며, Uim은 직접 Socket을 사용합니다. 이러한 독자 프로토콜 및 클라이언트는 주로 후술할 IM 모듈을 위해 사용됩니다.

그렇다면 표준 프로토콜이 존재하는데도 불구하고 왜 독자 프로토콜이 만들어졌는지 의문이 생깁니다. 이에 대해 추측해보자면, 아마 다음과 같은 과정이 있었을 것입니다.

  1. 1990년대에 개발된 XIM은 기본적인 입력 기능만 지원했고, 현대적인 입력 방식(주변 텍스트 참조 등)을 지원하지 않았음
  2. 각 입력기 개발 팀들(IBus, Fcitx, Uim 등)은 XIM의 한계를 극복하기 위해 독자적인 프로토콜을 개발했음
  3. 이후 Wayland가 등장하고, 입력 프로토콜이 3개나 생성됨
  4. 결과적으로 여러 프로토콜이 난립하게 되었고, 애플리케이션 개발자들은 다양한 프로토콜을 지원해야 하는 부담이 생기게 됨

입력기를 사용하는 애플리케이션 개발

애플리케이션 개발자가 입력기와 관련된 고려를 해야 하는 경우는 많지 않습니다. 대부분의 경우 GTK나 Qt 같은 위젯 툴킷이 입력기와의 연동을 뒤에서 자동으로 처리하기 때문입니다. 그래서 위젯 툴킷에서 GtkEntryQLineEdit 같은 입력 필드를 사용할 때, 개발자는 입력기와 관련된 세부사항에 신경쓰지 않아도 됩니다.

위젯 툴킷은 보통 XIM 이나 text-input-unstable-v3 같은 표준 프로토콜을 자체적으로 지원하며, 입력기의 독자적인 프로토콜을 구현한 코드는 보통 모듈/플러그인 방식으로 불러옵니다. 이를 IM 모듈이라고 부릅니다. 입력기를 설치해본 적이 있다면 다음과 같이 위젯 툴킷이 사용할 IM 모듈을 지정하는 환경변수를 설정해본 경험이 있을 겁니다.

export GTK_IM_MODULE=fcitx
export QT_IM_MODULE=fcitx

자체적으로 UI를 구현한 애플리케이션의 경우

모든 애플리케이션이 위젯 툴킷에서 제공하는 입력 필드를 사용하지는 않습니다. 웹 브라우저와 같이 자체적으로 UI를 구현하고 렌더링하는 애플리케이션은 표준 입력기 프로토콜을 직접 구현하거나, 위젯 툴킷에서 제공하는 Gtk.IMContext, QInputMethod 같은 입력기를 추상화한 인터페이스/클래스를 이용하여 자체 UI 요소의 입력을 처리합니다.

요약

앞서 설명한 입력기의 통신 프로토콜 및 애플리케이션의 구조를 한눈에 보면 다음과 같습니다.

입력기 시스템 통신구조 한눈에 보기

입력기 시스템 통신구조 한눈에 보기

3. 입력기의 대략적인 동작 과정

입력기가 실제 키 입력부터 화면 출력까지의 전체 과정에서 어떻게 동작하는지 프로그래밍 관점에서 살펴보겠습니다. 이 과정은 보통 GTK, Qt 같은 위젯 툴킷에 의해 수행됩니다.

이해를 돕기 위해 Javascript 기반의 의사 코드를 사용했습니다.

Context 생성

Context는 하나의 텍스트 입력 상태(조합 중인 텍스트, 커서 위치 등)를 나타냅니다. 입력기는 Context를 이용하여 여러 입력 필드, 애플리케이션의 입력 상태를 별도의 객체로 유지합니다.

// Pseudocode
const inputContext = new InputContext();

커서 위치 설정

입력 후보 리스트

입력 후보 리스트

일본어나 중국어를 입력하거나 한국어에서 한자 키를 사용한다면 위와 같이 입력 텍스트 후보가 표시됩니다. 이 UI는 입력기에서 표시하는 것으로, 애플리케이션은 입력기가 이를 적절한 위치에 띄우도록 하기 위해 현재 입력 커서의 화면상 좌표를 전송할 필요가 있습니다. 또한 창 이동 등의 사유로 인해 입력 필드의 위치가 이동될 때 마다 갱신이 필요합니다.

// Pseudocode
inputContext.setCursorLocation(x, y, width, height);

키 입력 전송

입력기는 일반적으로 직접 디스플레이 서버로부터 키 입력을 받지 않기 때문에, 애플리케이션은 입력기에 키 입력 이벤트를 전송해줄 필요가 있습니다.2 입력기는 키 입력을 전송받고 해당 키의 처리 여부를 반환합니다. 보통 조합이 필요없는 키는 false 값이 반환됩니다. 예를 들어 한글 입력기에서 숫자, 기호 등 조합 없이 직접 입력 가능한 키가 이에 해당합니다. 이러한 경우 일반적으로 애플리케이션은 원본 키가 그대로 입력되도록 처리합니다.

// Pseudocode
function onKeyPress(keyCode) {
  const isKeyProcessed = inputContext.processKey(keyCode);
  // ...
}

Preedit 이벤트

Preedit 텍스트 예제

Preedit 텍스트 예제

텍스트를 입력하다 보면, 아직 조합중인 글자 아래에 밑줄이 표시되는 것을 관찰할 수 있습니다. 이와 같이 아직 입력중이지만 확정되지 않은 텍스트를 Preedit 텍스트라고 부릅니다. 입력기는 Preedit 텍스트가 업데이트 되면 애플리케이션에 이벤트를 전송하며, 이를 통해 사용자는 아직 조합되지 않은 텍스트를 실시간으로 보면서 편집할 수 있습니다.

// Pseudocode
function onPreeditTextUpdate(preeditText) {
  // ...
}

// Add an event listener
inputContext.setOnPreeditTextUpdate(onPreeditTextUpdate);

Commit 이벤트

Commit 예제

Commit 예제

사용자가 입력 중이던 Preedit 텍스트를 최종 확정하여 애플리케이션에 전달하는 것을 커밋(Commit) 이라고 합니다. 커밋된 텍스트는 더 이상 입력기가 관리하는 특수 상태가 아니게 되며 밑줄이 사라집니다. 커밋이 발생하는 조건은 입력하는 언어의 특성 및 사용하는 입력기에 따라 다르지만 일반적으로는 Enter 키를 눌렀을 때, 마우스로 창을 클릭했을 때, 커서를 이동했을 때 발생합니다.

// Pseudocode
function onCommit(commitText) {
  // ...
}

// Add an event listener
inputContext.setOnCommit(onCommit);

테스트 해보기

아래 웹 사이트를 통해 직접 Preedit 업데이트, Commit을 확인해 볼 수 있습니다.

참고


  1. 일반적인 X11, Wayland, Freedesktop 등의 기술을 사용하는 GNU/Linux 배포판들 ↩︎

  2. 다만 Wayland의 text-input-unstable-v3 프로토콜은 컴포지터 측에서 키 입력을 입력기에 전송하고, 조합 결과만 애플리케이션에 보내주는 방식을 사용하기 때문에 애플리케이션의 키 입력 전송이 필요하지 않습니다. ↩︎