Tauri 2로 클립보드 관리자를 만들면 네이티브 코드는 얼마나 남을까?
지난 글에서 저는 클립보드 관리자를 Tauri로 만들면 가장 어려운 부분이 결국 네이티브 코드로 남는다고 썼습니다. 그 뒤 실제로 Tauri 2로 CopyFocus라는 클립보드 관리자를 만들었습니다. 결과부터 말하면 예상은 절반만 맞았습니다. 직접 짠 네이티브 코드는 생각보다 훨씬 적었고, 대신 예상하지 못한 곳에서 운영체제를 다시 만났습니다.
크레이트가 메워 준 두 부분
지난 글의 비교표에서 Tauri 열에는 "Rust로 직접"이 세 번 들어 있었습니다. 변경 감지, 임의 타입 읽기, 포커스를 뺏지 않는 패널입니다. 공식 clipboard-manager 플러그인의 함수 목록만 보고 내린 판단이었고, 커뮤니티 크레이트는 찾아보지 않았습니다. 그게 첫 번째 실수였습니다.
clipboard-rs는 macOS에서 changeCount를 폴링하는 감시기와 available_formats(), get_buffer(format)를 줍니다. 덕분에 비밀번호 관리자가 붙이는 org.nspasteboard.ConcealedType 같은 표식을 이름으로 확인하는 일이 Rust 몇 줄로 끝났습니다. Windows에서는 같은 API 뒤에서 AddClipboardFormatListener로 알림을 받고, 기록에서 빼 달라는 ExcludeClipboardContentFromMonitorProcessing 형식도 같은 방법으로 확인합니다. 감시 간격은 기본값이 500ms인데, 복사하자마자 패널을 열어도 목록에 이미 있도록 250ms로 줄였습니다.
포커스를 뺏지 않는 패널은 tauri-nspanel이 맡았습니다. Tauri 창을 NSPanel로 바꾸고 NonactivatingPanel 스타일을 더하는 크레이트이고, 클립보드 관리자인 EcoPaste도 이걸 씁니다. 패널이 떠 있는 동안에도 원래 앱이 활성 상태로 남기 때문에, 패널을 숨기기만 하면 다음 ⌘V가 그 앱으로 갑니다.
직접 짠 것은 함수 여섯 개
결국 손으로 쓴 macOS 코드는 ⌘V를 보내는 부분이 거의 전부였습니다. CoreGraphics의 CGPreflightPostEventAccess, CGRequestPostEventAccess, CGEventSourceCreate, CGEventCreateKeyboardEvent, CGEventSetFlags, CGEventPost 여섯 개를 extern으로 선언하고, 키 코드 9(V)에 Command 플래그를 붙여 보냅니다. 이 정도면 바인딩 크레이트를 하나 더 들일 분량이 아니라고 봤습니다.
패널을 숨긴 뒤 60ms를 기다렸다가 키를 보냅니다. 키 코드는 물리 키 위치라서 한글 입력 상태에서도 ⌘V로 제대로 갑니다. 반대로 드보락처럼 V 자리가 다른 자판에서는 다른 키가 눌립니다. 아직 고치지 않은 부분입니다.
Windows는 SendInput으로 Ctrl+V를 보냅니다. Windows에는 비활성 패널에 해당하는 창 종류가 없어서, 패널을 띄우기 직전의 전경 창을 기억해 두었다가 숨길 때 SetForegroundWindow로 되돌리는 방식으로 갈랐습니다.
맥에서 Windows 빌드를 확인할 수 없던 이유
Windows 코드는 cfg(windows) 안에 있어서 맥에서는 컴파일조차 되지 않습니다. 그래서 cargo check --target x86_64-pc-windows-msvc로 확인하려 했는데 실패했습니다. 원인은 제 코드가 아니라 히스토리 저장에 쓴 rusqlite의 bundled 기능이었습니다. SQLite C 소스를 컴파일하는 단계에서 Windows SDK의 stdlib.h를 찾지 못했습니다.
결국 GitHub Actions에 macOS와 Windows 러너를 두고 cargo clippy -- -D warnings를 돌리게 했습니다. Windows 쪽 코드가 처음으로 컴파일된 곳이 CI였고, 다행히 한 번에 통과했습니다. 다만 컴파일이 통과한 것과 동작하는 것은 다릅니다. 포커스 되돌리기와 Ctrl+V 전송은 아직 실제 Windows 기기에서 돌려 보지 않았습니다.
입력란 읽기에서 다시 만난 접근성 트리
만들다 보니 기능이 하나 늘었습니다. 복사하지 않아도 지금 포커스된 입력란의 글을 단축키 한 번으로 담는 기능입니다. 이름의 Focus도 여기서 왔습니다. macOS 손쉬운 사용 API로 시스템 전체 요소에서 AXFocusedUIElement를 얻고, AXSelectedText가 있으면 그것을, 없으면 AXValue를 읽습니다. AXSecureTextField인 비밀번호 칸은 건너뜁니다.
처음 눌러 본 곳에서 바로 실패했습니다. 화면에는 "포커스된 입력란이 없어요"가 떴습니다. 그 앱은 Orca라는 Electron 앱이었습니다. Electron과 Chromium 앱은 보조 기술이 요청하기 전까지 접근성 트리를 만들지 않는다는 것을 알고 있었기 때문에, 앱 요소에 AXManualAccessibility를 켜 달라고 요청한 뒤 다시 읽게 고쳤습니다.
그런데 앱 요소를 얻는 단계에서부터 막혔습니다. 시스템 전체 요소의 AXFocusedApplication이 비어 있었습니다. 그래서 맨 앞 앱의 pid로 AXUIElementCreateApplication을 불러 앱 요소를 직접 만들었습니다. 속성 설정은 성공했지만 포커스 요소는 여전히 없었습니다.
여기서 트리를 직접 세어 봤습니다. 요소가 454개 있었습니다. 트리는 만들어져 있는데 포커스 요소만 없었던 겁니다. 속성을 켜기 전에도 트리가 있었는지는 재지 않아서 모릅니다. 알고 보니 제가 단축키를 누른 곳은 Orca 안의 터미널 창이었습니다. 터미널은 글자 칸을 직접 그리는 화면이라 읽어 올 입력란이 원래 없습니다. 고칠 버그는 pid로 앱 요소를 만드는 폴백 하나였고, 나머지는 "터미널처럼 입력란이 아닌 곳에서는 담을 수 없어요"라는 안내 문구로 끝냈습니다.
이 기능에는 대가가 있습니다. ⌘V를 보내는 권한은 이벤트 전송에만 한정된 권한이라 App Sandbox와 함께 쓸 수 있습니다. 반면 다른 앱의 입력란을 읽으려면 손쉬운 사용 전체 권한이 필요하고, 이건 샌드박스 앱에서 쓸 수 없습니다. 그래서 이 기능은 기본으로 꺼 두고 설정에서 켜는 선택 기능으로 뒀습니다.
지난 글의 판단에서 고칠 부분
Windows까지 낸다면 Tauri를 고르겠다는 결론은 그대로입니다. 고칠 것은 이유입니다. 가장 어려운 부분이 네이티브로 남는 게 아니었습니다. 공식 플러그인 밖의 커뮤니티 크레이트가 그 부분을 상당히 메워 줬습니다. 화면 쪽은 React 하나로 끝났고, 설정 화면까지 넣고 빌드한 JavaScript는 gzip으로 73KB였습니다.
다만 크레이트가 운영체제의 동작까지 감춰 주지는 않았습니다. 접근성 트리처럼 앱마다 다르게 동작하는 영역은 결국 직접 재 보고 맞춰야 했고, 이번에 시간을 가장 많이 쓴 곳도 거기였습니다. 남은 일은 Windows 실기기 확인, V 자리가 다른 자판, Windows의 입력란 읽기(UI Automation)입니다.
함께 읽기
- 데스크톱 앱 코드 서명 정리: macOS 공증부터 Windows SmartScreen까지직접 빌드한 .dmg를 다른 맥에 보내면 "확인되지 않은 개발자"라는 창이 뜨고 앱이 열리지 않습니다. 같은 앱의 Windows 설치 파일은 파란 "Windows의 PC 보호" 창에 막힙니다. 두 경고 모두 코드 서명이 없어서 생깁니다.
- macOS 클립보드 관리자, Swift·Tauri·Electron 중 무엇으로 만들까?클립보드 관리자를 만들겠다고 하면 첫 질문은 대개 "UI를 뭘로 짤까"입니다. React가 익숙하면 Electron이나 Tauri, 네이티브가 좋으면 SwiftUI를 떠올립니다. 그런데 이 앱의 화면은 검색창 하나와 목록 하나가 거의 전부입니다. 나머지는 운영체제와 주고받는 일입니다.
- GitHub 개인 계정과 조직: 어떤 리포를 조직으로 옮길까?기능 모듈을 패키지로 내려다가 리포 하나를 개인 계정에서 조직으로 옮겼습니다. 옮기기 직전까지 저는 이 일이 귀찮을 거라고 생각했습니다. 러너를 다시 붙이고, 시크릿을 다시 넣고, 문서마다 주소를 고쳐야 할 것 같았습니다. 실제로는 API 호출 한 번과 원격 주소 한 줄로 끝났습니다. 그 과정에서 어떤 리포를 조직에 두…
- 새 맥북 개발 환경 세팅: git clone이 가져오지 않는 것들맥북을 새로 받고 하던 프로젝트를 이어서 하려 했습니다. 리포를 클론하고 npm install 을 돌린 다음 개발 서버를 띄웠는데, 브라우저에 이게 떴습니다.
- xcode-select와 DEVELOPER_DIR: 어느 쪽이 정석일까?Expo 앱을 iOS 시뮬레이터에 띄우려고 도구부터 점검했는데, 결과가 앞뒤가 안 맞았습니다.