사이드프로젝트 slecs

키 리매퍼에 GUI와 매크로 타이머 기능 추가

목차

cfg 파일을 손으로 편집하고 재기동하는 방식은 사실 한동안 큰 불편 없이 쓸 수 있었다. 키 매핑 몇 개 바꾸는 건 에디터 켜고 저장하면 끝이었으니까. 근데 매크로 타이머 기능이 들어오면서 상황이 달라졌다. 타이머 간격, 토글 모드, 시퀀스 키 목록까지 cfg에 때려 넣으면 사람이 읽기도 쓰기도 힘들어진다. 거기다 실수로 잘못 입력하면 재기동해서 확인하는 사이클이 너무 느렸다. GUI를 붙이기로 한 건 그냥 필연이었음.

빌드 환경부터 정리한 이유

처음엔 단순 Makefile 하나로 굴렸다. 소스 파일 몇 개에 링크 플래그 몇 줄이면 충분했으니까. 근데 GUI 라이브러리가 붙으면서 링크 체인이 급격히 늘어났다. 정적 링크인지 동적 링크인지, 어떤 순서로 넣는지에 따라 빌드가 튀고, 윈도우 타겟에서 MinGW 경로 처리가 Makefile에서는 생각보다 지저분해진다. 결국 CMake로 갈아탔다.

cmake -S . -B build -G "MinGW Makefiles"
cmake --build build --config Release

두 줄짜리 build.bat으로 묶어두니 빌드 재현이 확실해졌다. 어떤 머신에서 클론해도 MinGW 경로만 잡혀 있으면 이 배치 파일 하나로 끝난다. CMake가 처음엔 학습 비용이 있어 보이지만, 의존성이 세 개만 넘어가도 Makefile 직접 관리보다 훨씬 낫다.

초반에 빌드 산출물을 실수로 커밋해버린 것도 이번에 한 번 갈아엎었다. build/, *.exe, *.o.gitignore에 처음부터 박아두는 건 당연한 거지만, 급하게 시작한 사이드 프로젝트에선 이게 의외로 잘 빠진다. 결국 히스토리 지저분해지고 리셋하는 비용이 더 크다. 빌드 산출물은 반드시 첫 커밋 전에 잡아두는 게 맞다.

기능 세 개 붙이면서 생긴 일들

이번에 새로 들어간 기능은 크게 세 가지다.

기능 동작 방식 비고
키 캡처 후킹으로 누른 키 코드 실시간 표시 매핑 등록 UX 개선
GUI 리매핑 드롭다운으로 매핑 편집, cfg 자동 저장 재기동 불필요
매크로 타이머 일정 간격으로 키 시퀀스 반복 ms 단위 설정

키 캡처가 가장 골치였다. 후킹 콜백은 별도 스레드에서 돌아오는데, 거기서 GUI 위젯을 직접 건드리니 데드락이 걸렸다. 윈도우 저수준 훅은 콜백이 메인 스레드가 아닌 훅을 등록한 스레드에서 호출되고, GUI 프레임워크 대부분은 위젯 업데이트를 메인 스레드에서만 허용한다. 전형적인 크로스 스레드 UI 접근 문제다. 메시지 큐를 거쳐 메인 스레드로 키 코드를 넘기는 방식으로 해결했다. 콜백에서는 큐에 이벤트 넣는 것만 하고, 메인 루프에서 큐를 폴링해서 위젯 갱신한다.

매크로 타이머 정밀도 문제도 예상보다 까다로웠다. 보통 Sleep()이나 std::this_thread::sleep_for()를 쓰면 윈도우 기본 타이머 해상도가 15ms 안팎이라 간격이 그만큼 흔들린다. 타이핑 매크로처럼 빠른 입력이 필요한 케이스에선 ±15ms 오차가 체감된다. timeBeginPeriod(1)로 시스템 타이머 해상도를 1ms로 고정하니 안정됐다. 단, 이건 전역 시스템 설정이라 프로세스 종료 시 timeEndPeriod(1)로 반드시 원복해야 한다.

시스템 키 처리는 화이트리스트 방식으로 풀었다. 캡처 중에 Alt+Tab, Win+L 같은 OS 레벨 키까지 가로채면 시스템 동작이 이상해진다. 이런 키 조합은 훅에서 잡되 전달은 그냥 통과시키는 화이트리스트 목록을 뒀다. 후킹 기반 도구에서 흔한 실수 중 하나가 OS 시스템 키를 너무 공격적으로 잡는 것인데, 사용자 입장에서 운영체제가 이상하게 동작하는 건 프로그램 버그라고 느끼기 딱 좋다.

cfg 포맷을 뜯어고친 배경

기존 포맷은 단순했다.

capslock=esc
f1=ctrl+c

한 줄에 source=target 하나씩. 읽기도 쉽고 고치기도 쉬웠다. 근데 매크로 타이머가 붙으면서 한 항목에 담아야 할 정보가 늘어났다. 간격이 얼마인지, 홀드 중에만 동작하는지 토글인지, 시퀀스가 무엇인지. 기존 포맷에 이걸 욱여넣으면 파싱 코드가 지저분해지고 사람이 읽을 수 없는 형식이 된다.

섹션 기반 INI 구조로 전환했다.

[remap]
capslock=esc

[macro]
key=f1
interval=50
mode=toggle
sequence=q,w,e,r

섹션 구분이 생기니 파서도 깔끔해지고, 나중에 섹션 하나 더 추가하는 확장성도 생긴다. 프로파일 기능 붙일 때 [profile:game] 같은 네이밍 컨벤션 쓰면 기존 파서 구조 크게 안 바꿔도 될 것 같다.

기존 cfg는 마이그레이션 스크립트 하나로 변환했다. 포맷 변경할 때 이전 버전 설정 파일을 손으로 고치게 만드는 건 사용자 경험상 좋지 않다. 변환 스크립트를 도구에 번들해서 구버전 cfg 감지하면 자동으로 새 포맷으로 바꿔주도록 했다.

MinGW 빌드 바이너리에서 위젯 폰트 깨지는 문제는 매니페스트 파일 추가로 잡았다. 윈도우에서 애플리케이션 매니페스트에 DPI 인식 설정과 Common Controls 버전을 명시하지 않으면 기본 레거시 스타일로 렌더링되는데, 이게 폰트 렌더링 이슈로 나타난다. <dependency> 블록에 Microsoft.Windows.Common-Controls 추가하면 해결된다. 처음엔 코드 문제인 줄 알고 한참 삽질했다.

GUI가 붙고 나니 이전 방식으로 못 돌아가겠다. 다음 단계는 프로파일 전환, 게임용이랑 타이핑용 설정을 즉시 스왑하는 기능이랑, 트레이 아이콘 상주로 백그라운드에서 조용히 돌아가도록 하는 것. 트레이 아이콘은 윈도우 메시지 루프 건드는 부분이 좀 있어서 다음 작업 때 따로 정리할 예정.

댓글 0

첫 댓글 달아줘.