🤖 이 글은 Claude Code(AI)가 작성합니다. | 시리즈 목차 | 이전: [23편] AppKit과 SwiftUI 통합
Accessibility API란?
macOS Accessibility API(접근성 API)는 원래 시각 장애인을 위한 스크린 리더(VoiceOver) 같은 보조 기술을 위해 만들어졌습니다. 그런데 이 API는 단순히 접근성 지원 이상의 능력을 갖고 있습니다.
- 다른 앱의 UI 요소 읽기: 다른 앱에 있는 버튼 텍스트, 입력창 내용 등을 코드로 읽을 수 있습니다.
- 다른 앱의 UI 조작: 버튼 클릭, 텍스트 입력 등을 코드로 수행할 수 있습니다.
- 포커스된 앱 감지: 현재 어떤 앱이 활성화되어 있는지, 어떤 요소가 선택됐는지 알 수 있습니다.
이런 이유로 자동화 도구, 생산성 앱, AI 어시스턴트 같은 앱들이 Accessibility API를 활용합니다.
권한 요청
Accessibility API를 사용하려면 반드시 사용자에게 권한을 받아야 합니다. 권한 없이 사용하면 빈 데이터가 반환됩니다.
import AppKit
// 현재 접근성 권한 확인
func checkAccessibilityPermission() -> Bool {
let options = [kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String: true]
return AXIsProcessTrustedWithOptions(options as CFDictionary)
}
// 호출하면 "시스템 설정 > 개인 정보 보호 > 손쉬운 사용"으로 유도
if !checkAccessibilityPermission() {
print("접근성 권한이 필요합니다. 시스템 설정을 확인하세요.")
}
kAXTrustedCheckOptionPrompt: true를 넣으면 권한이 없을 때 자동으로 시스템 설정 다이얼로그가 뜹니다.
AXUIElement — UI 요소 탐색
import AppKit
// 현재 포커스된 앱의 접근성 요소 가져오기
func getFocusedAppElement() -> AXUIElement? {
guard let frontApp = NSWorkspace.shared.frontmostApplication else { return nil }
let pid = frontApp.processIdentifier
return AXUIElementCreateApplication(pid)
}
// 특정 속성 읽기
func getAttribute(_ element: AXUIElement, _ attribute: String) -> AnyObject? {
var value: AnyObject?
AXUIElementCopyAttributeValue(element, attribute as CFString, &value)
return value
}
// 사용 예시
if let appElement = getFocusedAppElement() {
// 앱 이름
let appName = getAttribute(appElement, kAXTitleAttribute as String) as? String
// 포커스된 창
let focusedWindow = getAttribute(appElement, kAXFocusedWindowAttribute as String)
// 포커스된 UI 요소 (현재 선택된 버튼, 입력창 등)
let focusedElement = getAttribute(appElement, kAXFocusedUIElementAttribute as String)
print("앱: \(appName ?? "알 수 없음")")
}
주요 AX 속성
| 상수 | 의미 |
|---|---|
| kAXTitleAttribute | 요소의 제목/이름 |
| kAXValueAttribute | 현재 값 (입력창의 텍스트 등) |
| kAXRoleAttribute | 요소 역할 (button, textField 등) |
| kAXChildrenAttribute | 자식 요소 목록 |
| kAXFocusedWindowAttribute | 포커스된 창 |
| kAXFocusedUIElementAttribute | 포커스된 UI 요소 |
| kAXSelectedTextAttribute | 현재 선택된 텍스트 |
| kAXPositionAttribute | 화면상 위치 |
선택된 텍스트 읽기
func getSelectedText() -> String? {
guard let appElement = getFocusedAppElement() else { return nil }
// 포커스된 UI 요소
var focusedElement: AnyObject?
AXUIElementCopyAttributeValue(
appElement,
kAXFocusedUIElementAttribute as CFString,
&focusedElement
)
guard let element = focusedElement else { return nil }
// 선택된 텍스트
var selectedText: AnyObject?
AXUIElementCopyAttributeValue(
element as! AXUIElement,
kAXSelectedTextAttribute as CFString,
&selectedText
)
return selectedText as? String
}
전역 키보드 단축키
앱이 포커스되지 않아도 동작하는 전역 단축키는 유틸리티 앱의 핵심 기능입니다. macOS에서는 두 가지 방법이 있습니다.
1. NSEvent 글로벌 모니터 (앞 편에서 소개)
class GlobalHotkeyMonitor {
private var monitor: Any?
func start(handler: @escaping () -> Void) {
monitor = NSEvent.addGlobalMonitorForEvents(matching: .keyDown) { event in
// Cmd + Shift + Space
let isCmdShiftSpace = event.modifierFlags.contains([.command, .shift])
&& event.keyCode == 49 // 스페이스바 키코드
if isCmdShiftSpace {
DispatchQueue.main.async { handler() }
}
}
}
func stop() {
if let monitor = monitor {
NSEvent.removeMonitor(monitor)
}
}
}
단, addGlobalMonitorForEvents는 Accessibility 권한 없이도 키코드를 받을 수 있지만, 일부 보안 환경에서는 제한될 수 있습니다.
2. Carbon HotKey API (더 안정적)
import Carbon
class HotKeyManager {
private var hotKeyRef: EventHotKeyRef?
private var eventHandler: EventHandlerRef?
func register(keyCode: UInt32, modifiers: UInt32, handler: @escaping () -> Void) {
// 핫키 ID
let hotKeyID = EventHotKeyID(signature: OSType("htky".utf8.reduce(0) { ($0 << 8) | UInt32($1) }),
id: 1)
// 이벤트 핸들러 설치
var eventSpec = EventTypeSpec(eventClass: UInt32(kEventClassKeyboard),
eventKind: UInt32(kEventHotKeyPressed))
InstallEventHandler(
GetApplicationEventTarget(),
{ (_, event, _) -> OSStatus in
NotificationCenter.default.post(name: .globalHotkeyPressed, object: nil)
return noErr
},
1, &eventSpec, nil, &eventHandler
)
// 핫키 등록 (Cmd+Shift+Space 예시)
RegisterEventHotKey(keyCode, modifiers, hotKeyID, GetApplicationEventTarget(), 0, &hotKeyRef)
// 알림 수신
NotificationCenter.default.addObserver(forName: .globalHotkeyPressed, object: nil, queue: .main) { _ in
handler()
}
}
func unregister() {
if let hotKeyRef = hotKeyRef {
UnregisterEventHotKey(hotKeyRef)
}
}
}
extension Notification.Name {
static let globalHotkeyPressed = Notification.Name("globalHotkeyPressed")
}
NSWorkspace — 실행 중인 앱 감지
import AppKit
class AppWatcher {
private var observers: [NSObjectProtocol] = []
func startWatching() {
// 앱 활성화 감지 (다른 앱으로 전환할 때)
observers.append(
NSWorkspace.shared.notificationCenter.addObserver(
forName: NSWorkspace.didActivateApplicationNotification,
object: nil,
queue: .main
) { notification in
let app = notification.userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication
print("활성화된 앱: \(app?.localizedName ?? "알 수 없음")")
print("번들 ID: \(app?.bundleIdentifier ?? "")")
}
)
// 앱 실행 감지
observers.append(
NSWorkspace.shared.notificationCenter.addObserver(
forName: NSWorkspace.didLaunchApplicationNotification,
object: nil,
queue: .main
) { notification in
let app = notification.userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication
print("새로 실행된 앱: \(app?.localizedName ?? "")")
}
)
}
func stopWatching() {
observers.forEach { NSWorkspace.shared.notificationCenter.removeObserver($0) }
}
}
특정 앱이 터미널인지 확인
let terminalBundleIDs = [
"com.apple.Terminal",
"com.googlecode.iterm2",
"dev.warp.Warp-Stable",
"com.github.wez.wezterm"
]
func isFrontmostAppTerminal() -> Bool {
guard let frontApp = NSWorkspace.shared.frontmostApplication,
let bundleID = frontApp.bundleIdentifier else { return false }
return terminalBundleIDs.contains(bundleID)
}
SwiftUI에서 Accessibility 지원
내 앱의 접근성을 높이는 것도 중요합니다.
struct AccessibleButton: View {
var body: some View {
Button(action: doSomething) {
Image(systemName: "trash")
}
// VoiceOver가 읽을 레이블
.accessibilityLabel("삭제")
.accessibilityHint("선택한 항목을 삭제합니다")
// 접근성 요소 숨기기 (장식용 이미지)
Image(systemName: "star.fill")
.accessibilityHidden(true)
// 슬라이더에 값 설명 추가
Slider(value: $volume, in: 0...100)
.accessibilityValue("\(Int(volume))%")
}
func doSomething() { }
@State private var volume = 50.0
}
핵심 요약
- Accessibility API: 다른 앱의 UI 요소를 읽고 조작하는 macOS 시스템 API
- AXIsProcessTrustedWithOptions: 접근성 권한 확인 및 요청
- AXUIElement: UI 계층 탐색의 기본 단위. kAX* 상수로 속성 읽기
- NSEvent.addGlobalMonitorForEvents: 전역 키보드 이벤트 감지
- Carbon HotKey API: 더 안정적인 전역 단축키 등록
- NSWorkspace: 실행 중인 앱 목록, 앱 활성화 이벤트 감지
- SwiftUI의
.accessibilityLabel,.accessibilityHint로 내 앱의 접근성도 챙길 것
4부 macOS 앱 개발이 완결됩니다. 다음 편부터는 5부 Foundation과 데이터 저장으로, Codable/JSON, UserDefaults, FileManager, SQLite를 다룹니다.
🤖 Generated with Claude Code