[Linux/KDE Wayland] Lutris + GE-Proton 월드 오브 워크래프트(WoW) 한글 입력 완벽 해결

리눅스(CachyOS / Arch Linux 등) + KDE Plasma 6 (Wayland) + Fcitx5 환경에서 Lutris (GE-Proton / Wine)로 배틀넷과 월드 오브 워크래프트(WoW)를 구동할 때 겪는 고질적인 문제 3가지를 완벽하게 해결하는 가이드.
채팅창을 닫았는데도 입력이 한글 상태면 WASD 이동 등 키가 인식이 안 되는 문제
채팅 후 한/영 키를 안 바꾸고 채팅창을 닫으면, WASD를 눌렀을 때 캐릭터가 움직이지 않고 한글(ㅈ, ㅁ, ㄴ, ㅇ)이 조합되는 창이 뜨면서 와우에서 인식해야할 키가 인식이 안되는 현상 해결
채팅 입력 시 거슬리는 떠다니는 사전편집(Preedit) 팝업창 제거
한글 입력 시 화면에 별도 팝업 박스가 뜨지 않고, 윈도우 환경처럼 게임 채팅창 커서 위치에서 바로 글자가 조합되도록 설정
앱 메뉴에서 아이콘 한 번만 누르면 배틀넷이 자동 로그인 후 와우를 바로 띄우고 스스로 종료되도록 구성
TIP
이 방식은 Wine이나 게임 실행 파일(Wow.exe, winex11.so)을 직접 바이너리 패치하지 않고,
표준 LD_PRELOAD 공유 라이브러리(libwow_ime_fix.so)로 동작하므로 Proton/Wine 버전을 업데이트해도 깨지지 않고 그대로 재사용 가능
1단계 : Fcitx5 사전편집(인라인 입력) 설정
한글 조합 시 떠다니는 팝업창을 없애고 게임 채팅창 안에 직접 글자가 조합되게 하려면 일반 설정과 XIM 부가기능 설정 두 곳을 모두 설정해야 함.
- 일반 설정 (응용프로그램에서 사전편집 표시 / PreeditEnabledByDefault):
원문은 “Show preedit in application”. 조합 중인 글자(사전편집)를 별도 팝업창이 아니라 응용프로그램 입력칸 내부에 직접 표시할지 묻는 옵션.
이 옵션을 체크(True)해야 팝업창이 뜨지 않고 인라인으로 입력 됨. (체크를 해제하면 오히려 팝업창이 표시) - XIM 부가기능 설정 (스팟 스타일에서 사용 / UseOnTheSpot):
Wine/Proton 게임은 X11의 구형 XIM(X Input Method) 프로토콜을 사용한다.
Fcitx5의 XIM 프론트엔드는 기본값이 UseOnTheSpot=False로 되어 있어, 일반 설정을 켜 두어도 Wine 게임에서는 강제로 팝업창을 띄우게 됨.
이걸 True로 켜야 Wine(winex11.so)의 XIMPreeditCallbacks가 활성화 됨.
설정 적용 방법 (GUI 또는 설정 파일)
- KDE 시스템 설정 → 입력기(Fcitx5) → 전역 옵션 클릭 → 응용프로그램에서 사전편집 표시 체크
- 입력기(Fcitx5) → 부가기능(Addons) 탭 → 프론트엔드 → X 입력기 프론트엔드 우측 톱니바퀴 클릭 → 스팟 스타일에서 사용(다시 시작 필요) 체크
KDE Wayland에서 Fcitx5 재시작하기
KDE 시스템 설정 → 가상 키보드에서 없음을 눌렀다가 다시 Fcitx 5를 선택해 적용으로 재시작.

2단계 : 채팅창 닫힘 시 WASD 한글 씹힘 방지 라이브러리 (libwow_ime_fix.so) 만들기
동작 원리
와우는 채팅창을 닫을 때 Windows API ImmAssociateContext(hwnd, NULL)를 호출해 입력 컨텍스트(HIMC)를 제거함.
하지만 기본 Wine/Proton(winex11.so)은 이 신호를 리눅스 Fcitx5(XIM)로 전달하지 않아 Fcitx5가 계속 한글을 조합하려고 키 입력을 가로채서 팝업(사전편집)을 띄움.
libwow_ime_fix.so는 win32u.so의 드라이버 테이블 등록 함수(__wine_set_user_driver)와 libX11.so.6의 XIM 함수들을 후킹하여,
HIMC == 0(채팅창 닫힘)일 때는 Fcitx5 포커스를 해제하고 키 입력을 영문 Raw Key로 즉시 통과시켜서 와우가 키 입력을 알아먹도록 함.
소스 코드 작성 (wow_ime_fix.c)
배틀넷 프리픽스 폴더(예: ~/Games/battlenet/wow_ime_fix.c)에 아래 파일을 작성.
#define _GNU_SOURCE
#include <dlfcn.h>
#include <link.h>
#include <stdint.h>
#include <string.h>
typedef void *HWND;
typedef void *HIMC;
typedef void *XIC;
typedef void *XIM;
typedef void *Display;
typedef unsigned long Window;
typedef unsigned long KeySym;
typedef int Bool;
typedef int Status;
typedef union _XEvent {
int type;
long pad[24];
} XEvent;
/* Function pointer types */
typedef unsigned int (*pfn_wine_set_user_driver)(void *funcs, unsigned int version);
typedef uintptr_t (*pfn_NtUserCallHwnd)(HWND hwnd, unsigned int code);
typedef HWND (*pfn_NtUserGetForegroundWindow)(void);
typedef void (*pfn_EnableIMEContext)(HWND hwnd, int enable);
typedef void (*pfn_XSetICFocus)(XIC ic);
typedef void (*pfn_XUnsetICFocus)(XIC ic);
typedef Bool (*pfn_XFilterEvent)(XEvent *event, Window w);
typedef int (*pfn_XmbLookupString)(XIC ic, XEvent *event, char *buffer, int bytes, KeySym *keysym, Status *status);
typedef int (*pfn_XLookupString)(XEvent *event, char *buffer, int bytes, KeySym *keysym, void *compose);
typedef char *(*pfn_XmbResetIC)(XIC ic);
typedef int (*pfn_XFree)(void *data);
typedef XIM (*pfn_XIMOfIC)(XIC ic);
typedef Display *(*pfn_XDisplayOfIM)(XIM im);
typedef int (*pfn_XFlush)(Display *display);
static void *g_win32u_handle = NULL;
static void *g_x11_handle = NULL;
static pfn_wine_set_user_driver real_wine_set_user_driver = NULL;
static pfn_NtUserCallHwnd real_NtUserCallHwnd = NULL;
static pfn_NtUserGetForegroundWindow real_NtUserGetForegroundWindow = NULL;
static pfn_EnableIMEContext orig_EnableIMEContext = NULL;
static pfn_XSetICFocus real_XSetICFocus = NULL;
static pfn_XUnsetICFocus real_XUnsetICFocus = NULL;
static pfn_XFilterEvent real_XFilterEvent = NULL;
static pfn_XmbLookupString real_XmbLookupString = NULL;
static pfn_XLookupString real_XLookupString = NULL;
static pfn_XmbResetIC real_XmbResetIC = NULL;
static pfn_XFree real_XFree = NULL;
static pfn_XIMOfIC real_XIMOfIC = NULL;
static pfn_XDisplayOfIM real_XDisplayOfIM = NULL;
static pfn_XFlush real_XFlush = NULL;
static XIC g_last_xic = NULL;
static HWND g_tracked_hwnd = NULL;
#define NTUSER_CALL_HWND_GET_WINDOW_INPUT_CONTEXT 7
#define USER_DRIVER_FUNCS_SIZE 0x4d8
#define OFFSET_ENABLE_IME_CONTEXT 0x338
/* Status values for XmbLookupString */
#define XLookupNone 1
#define XLookupChars 2
#define XLookupKeySym 3
#define XLookupBoth 4
struct phdr_search_ctx {
const char *suffix;
void *handle;
};
static int phdr_callback(struct dl_phdr_info *info, size_t size, void *data)
{
(void)size;
struct phdr_search_ctx *ctx = (struct phdr_search_ctx *)data;
if (!info->dlpi_name || !info->dlpi_name[0])
return 0;
size_t name_len = strlen(info->dlpi_name);
size_t suffix_len = strlen(ctx->suffix);
if (name_len >= suffix_len &&
strcmp(info->dlpi_name + name_len - suffix_len, ctx->suffix) == 0) {
void *h = dlopen(info->dlpi_name, RTLD_NOLOAD | RTLD_LAZY);
if (h) {
ctx->handle = h;
return 1;
}
}
return 0;
}
static void *find_loaded_module(const char *suffix)
{
struct phdr_search_ctx ctx = { suffix, NULL };
dl_iterate_phdr(phdr_callback, &ctx);
return ctx.handle;
}
static void resolve_win32u_symbols(void)
{
if (!g_win32u_handle)
g_win32u_handle = find_loaded_module("/win32u.so");
if (!g_win32u_handle)
return;
if (!real_wine_set_user_driver)
real_wine_set_user_driver = (pfn_wine_set_user_driver)dlsym(g_win32u_handle, "__wine_set_user_driver");
if (!real_NtUserCallHwnd)
real_NtUserCallHwnd = (pfn_NtUserCallHwnd)dlsym(g_win32u_handle, "NtUserCallHwnd");
if (!real_NtUserGetForegroundWindow)
real_NtUserGetForegroundWindow = (pfn_NtUserGetForegroundWindow)dlsym(g_win32u_handle, "NtUserGetForegroundWindow");
}
static void resolve_x11_symbols(void)
{
if (!g_x11_handle)
g_x11_handle = find_loaded_module("/libX11.so.6");
if (!g_x11_handle)
g_x11_handle = dlopen("libX11.so.6", RTLD_NOLOAD | RTLD_LAZY);
if (!g_x11_handle)
g_x11_handle = dlopen("libX11.so.6", RTLD_LAZY);
if (!g_x11_handle)
return;
if (!real_XFilterEvent)
real_XFilterEvent = (pfn_XFilterEvent)dlsym(g_x11_handle, "XFilterEvent");
if (!real_XSetICFocus)
real_XSetICFocus = (pfn_XSetICFocus)dlsym(g_x11_handle, "XSetICFocus");
if (!real_XUnsetICFocus)
real_XUnsetICFocus = (pfn_XUnsetICFocus)dlsym(g_x11_handle, "XUnsetICFocus");
if (!real_XmbLookupString)
real_XmbLookupString = (pfn_XmbLookupString)dlsym(g_x11_handle, "XmbLookupString");
if (!real_XLookupString)
real_XLookupString = (pfn_XLookupString)dlsym(g_x11_handle, "XLookupString");
if (!real_XmbResetIC)
real_XmbResetIC = (pfn_XmbResetIC)dlsym(g_x11_handle, "XmbResetIC");
if (!real_XFree)
real_XFree = (pfn_XFree)dlsym(g_x11_handle, "XFree");
if (!real_XIMOfIC)
real_XIMOfIC = (pfn_XIMOfIC)dlsym(g_x11_handle, "XIMOfIC");
if (!real_XDisplayOfIM)
real_XDisplayOfIM = (pfn_XDisplayOfIM)dlsym(g_x11_handle, "XDisplayOfIM");
if (!real_XFlush)
real_XFlush = (pfn_XFlush)dlsym(g_x11_handle, "XFlush");
}
static void resolve_symbols(void)
{
resolve_win32u_symbols();
resolve_x11_symbols();
}
static int is_ime_enabled(void)
{
resolve_symbols();
if (!real_NtUserCallHwnd)
return 1;
HWND hwnd = NULL;
if (real_NtUserGetForegroundWindow)
hwnd = real_NtUserGetForegroundWindow();
if (!hwnd)
hwnd = g_tracked_hwnd;
if (!hwnd)
return 1;
uintptr_t imc = real_NtUserCallHwnd(hwnd, NTUSER_CALL_HWND_GET_WINDOW_INPUT_CONTEXT);
return (imc != 0);
}
static void flush_xic_display(XIC ic)
{
if (!ic || !real_XIMOfIC || !real_XDisplayOfIM || !real_XFlush)
return;
XIM im = real_XIMOfIC(ic);
if (!im)
return;
Display *dpy = real_XDisplayOfIM(im);
if (dpy)
real_XFlush(dpy);
}
static void hooked_EnableIMEContext(HWND hwnd, int enable)
{
resolve_symbols();
if (hwnd)
g_tracked_hwnd = hwnd;
if (orig_EnableIMEContext)
orig_EnableIMEContext(hwnd, enable);
XIC ic = g_last_xic;
if (!ic)
return;
if (enable) {
if (real_XSetICFocus)
real_XSetICFocus(ic);
} else {
if (real_XmbResetIC) {
char *leftover = real_XmbResetIC(ic);
if (leftover && real_XFree)
real_XFree(leftover);
}
if (real_XUnsetICFocus)
real_XUnsetICFocus(ic);
}
flush_xic_display(ic);
}
unsigned int __wine_set_user_driver(void *funcs, unsigned int version)
{
resolve_symbols();
if (!real_wine_set_user_driver)
real_wine_set_user_driver = (pfn_wine_set_user_driver)dlsym(RTLD_NEXT, "__wine_set_user_driver");
if (!real_wine_set_user_driver)
return 0;
if (funcs && version == 0x6f) {
static unsigned char patched_funcs[USER_DRIVER_FUNCS_SIZE];
memcpy(patched_funcs, funcs, USER_DRIVER_FUNCS_SIZE);
pfn_EnableIMEContext *slot = (pfn_EnableIMEContext *)(patched_funcs + OFFSET_ENABLE_IME_CONTEXT);
if (*slot == NULL) {
orig_EnableIMEContext = NULL;
*slot = hooked_EnableIMEContext;
return real_wine_set_user_driver(patched_funcs, version);
}
}
return real_wine_set_user_driver(funcs, version);
}
void XSetICFocus(XIC ic)
{
resolve_symbols();
if (ic)
g_last_xic = ic;
if (!is_ime_enabled()) {
if (real_XUnsetICFocus && ic)
real_XUnsetICFocus(ic);
return;
}
if (real_XSetICFocus && ic)
real_XSetICFocus(ic);
}
void XUnsetICFocus(XIC ic)
{
resolve_symbols();
if (ic)
g_last_xic = ic;
if (real_XUnsetICFocus && ic)
real_XUnsetICFocus(ic);
}
Bool XFilterEvent(XEvent *event, Window w)
{
resolve_symbols();
if (event && (event->type == 2 || event->type == 3)) { /* KeyPress or KeyRelease */
if (!is_ime_enabled()) {
if (g_last_xic && real_XUnsetICFocus)
real_XUnsetICFocus(g_last_xic);
return 0; /* Do not let XIM/Fcitx5 swallow WASD movement keys */
}
}
return real_XFilterEvent ? real_XFilterEvent(event, w) : 0;
}
int XmbLookupString(XIC ic, XEvent *event, char *buffer, int bytes, KeySym *keysym, Status *status)
{
resolve_symbols();
if (ic)
g_last_xic = ic;
if (!is_ime_enabled() && real_XLookupString) {
KeySym sym = 0;
int len = real_XLookupString(event, buffer, bytes, &sym, NULL);
if (keysym)
*keysym = sym;
if (status) {
if (len > 0 && sym != 0)
*status = XLookupBoth;
else if (len > 0)
*status = XLookupChars;
else if (sym != 0)
*status = XLookupKeySym;
else
*status = XLookupNone;
}
return len;
}
return real_XmbLookupString ? real_XmbLookupString(ic, event, buffer, bytes, keysym, status) : 0;
}
컴파일 명령어
gcc -shared -fPIC -O2 -Wall -Wextra \
~/Games/battlenet/wow_ime_fix.c \
-o ~/Games/battlenet/libwow_ime_fix.so -ldl
3단계 : Lutris 게임 설정 (YAML)
Lutris YAML 예시 (~/.local/share/lutris/games/battlenet-*.yml):
game:
######## 생략 ########
game_slug: battlenet
name: Battle.net
slug: battlenet-standard
system:
env:
DXVK_HUD: compiler
DXVK_STATE_CACHE_PATH: /home/user/Games/battlenet
LD_PRELOAD: /home/user/Games/battlenet/libwow_ime_fix.so #여기에 본인 유저디렉토리로 시작하는 작성한 libwow_ime_fix.so 파일 경로를 적는다.
STAGING_SHARED_MEMORY: '1'
######## 생략 ########
year: 1996

이렇게 와우 채팅창에 잘 입력 된다.