본문 바로가기
C.W.K.
Stream
Lesson 04 of 07 · published

모두의 키보드에 톨게이트를 놓지 마

~11 min · hotkey, event-tap, permissions, flagschanged

Level 0불씨 없음
0 XP0/48 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete
"키를 삼키는 이벤트 탭은 이 기계가 만드는 모든 키 입력의 톨게이트야. 그 일을 정말 맡고 싶은지 아주 확실히 해."

요구사항

Firekeeper 의 기본 활성화는 맨 Fn 홀드 야. 조합키 없이, 안 쓰는 키 하나 누르고 말하는 거. 인터랙션은 사랑스러운데 구현은 진짜 어색해. 맨 모디파이어는 평범한 단축키가 아니거든. 평범한 단축키 API 엔 'Fn 눌림' 이벤트라는 게 없어. 모디파이어는 플래그가 바뀌는 걸로 자기를 알리니까, 누르고 있는 상태를 거기서 직접 재구성해야 해.

넘어가기 쉬운 틀린 도구

뻔한 답은 CGEventTap 이야. 시스템 전역 이벤트 스트림 위에 앉아서 모든 키를 보고, 내가 가져가겠다는 건 삼켜. 되긴 돼. 그리고 그 순간 네 앱이 이 기계가 만드는 모든 키 입력의 톨게이트가 돼. 탭이 잘못 굴면 네 앱이 깨지는 게 아니야. 어디서든 타이핑 이 깨져. 가정이 아니고, 경쟁자의 삼키는 탭이 유저들 스페이스바를 먹은 게 정확히 그거였어. 그리고 권한이 경고를 안 해 줘. 키를 삼키는 탭은 삽입 때문에 이미 받아 둔 바로 그 접근성 허가로 승인되거든. 그 허가가 조용히 모든 키 입력의 길목 자리로 바뀌는 거야. (Input Monitoring 이라는 두 번째 프롬프트가 필요한 건 듣기만 하는 키보드 탭이야. 삼키는 탭은 새로 받을 게 없고, 그게 또 다른 위험이고.)

CGEventTap (거부됨):          flagsChanged + keyState (선택됨):
  모든 키 입력에 앉음           모디파이어 플래그 변화만 관찰
  시스템 전역 억제 가능         아무 키도 못 삼킴
  같은 권한, 훨씬 큰 힘         이미 가진 접근성 권한만 씀
  경쟁자 게 스페이스바 먹음     가로채지 않는 걸 못 먹음

고른 길

Firekeeper 는 대신 NSEvent flagsChanged 모니터를 봐. 그리고 플래그만으로는 못 가리는 경우를 CGEventSource.keyState 로 구분해. 특히 쌍둥이 모디파이어가, 그러니까 반대쪽 Command 키 같은 게 아직 눌려 있을 때 누름과 놓음을 갈라내는 일. 앱이 삽입 때문에 이미 받아 둔 접근성 권한만으로 돌아가고, 새 권한을 하나도 안 더하고, 애초에 스트림을 안 가로채니까 구조적으로 키 입력을 삼킬 수가 없어. 같은 인터랙션인데 폭발 반경은 조각이야.

가로채기보다 관찰을 골라. 시스템 이벤트를 알아야 할 때, 보고만 있는 게 길목에 서는 것보다 훨씬 안전해. 잘못 구는 관찰자는 틀린 값을 로그에 남길 뿐이야. 잘못 구는 가로채기는 모든 앱한테 시스템을 망가뜨리고. 이벤트를 진짜로 소비해야 할 때만 가로채기를 집어. 눌러서 말하기 키한텐 그럴 이유가 없어.
Fn 키는 🌐 키 노릇도 같이 해. 요즘 macOS 에선 Fn 키가 Globe 키이기도 해서, 맨 Fn 홀드가 받아쓰기 위에 시스템 이모지나 입력 소스 피커를 띄울 수 있어. 코드로는 못 고쳐. 유저가 시스템 설정에서 '🌐 키를 누를 때: 아무 동작 안 함' 을 골라야 해. 그래서 모든 바인딩을 바꿀 수 있게 만들었고 Setup 패널이 이 힌트를 띄우는 거야. OS 설정에 기대는 인터랙션이라면 그 설정을 유저한테 알려 줘야 하니까.

Code

모디파이어 플래그는 보기만, 스트림은 절대 안 가로챔·swift
// 플래그 변화를 봄 — 관찰하지, 절대 소비 안 함.
let fnKey = CGKeyCode(kVK_Function)

func handleFlags(_ event: NSEvent) {
    // flagsChanged 는 모든 모디파이어에서 와; Fn 키 자기 이벤트에만 반응.
    guard event.keyCode == fnKey else { return }
    // 누름과 놓음은 keyCode 가 같아; 물리 키 상태가 어느 쪽인지 알려 줘.
    // 쌍둥이 모디파이어가 아직 눌려 있으면 플래그만으론 거짓말하니까.
    if CGEventSource.keyState(.combinedSessionState, key: fnKey) {
        controller.start(mode: .dictate)
    } else {
        Task { await controller.stop() }
    }
}
// 글로벌: 다른 앱이 앞에 있을 때. 로컬: 우리 창이 앞에 있을 때.
NSEvent.addGlobalMonitorForEvents(matching: [.flagsChanged]) { handleFlags($0) }
NSEvent.addLocalMonitorForEvents(matching: [.flagsChanged]) { handleFlags($0); return $0 }
// CGEventTap 없음. 가로채지 않는 키를 삼킬 수 없음.

External links

Exercise

앱이 뭔가를 시스템 전역으로 알아야 하는 경우를 찾아봐. 키 입력이든 클립보드든 창 변화든. 그게 관찰이 필요한 일인지 가로채기가 필요한 일인지 물어봐. 각 설계에서 버그의 폭발 반경이 뭐고, 가로채기를 고르면 그게 올라탄 권한이 무슨 뜻으로 바뀌어?
Hint
관찰에서 제일 나쁜 버그는 네 앱이 이벤트를 놓치거나 잘못 읽는 거야. 피해가 네 앱 안에 갇혀 있지. 가로채기에서 제일 나쁜 버그는 OS 전체가 이벤트를 잃는 거고. 피해가 남들 타이핑이야. 게다가 가로채기는 자기가 올라탄 권한을 더 큰 걸로 만들어. macOS 에선 키를 삼키는 탭이 접근성 허가를 모든 키 입력의 길목 자리로 바꿔 놓지. 그러니 두 번 값을 치르는 셈이야. 위험이 더 크고, 그 허가가 유저 생각보다 훨씬 많은 걸 뜻하게 되고.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인 — 댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.