본문 바로가기
C.W.K.
Stream
Lesson 01 of 05 · published

접근성은 사용자가 열어주는 문이야

~11 min · accessibility, ax-tree, trust, permission

Level 0식은 부싯돌
0 XP0/34 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"스크린 리더가 화면을 읽어주게 하는 그 문이, Flint 가 내 글을 읽게 해주는 문이기도 해. 그 문은 내가 열어줘야만 열리고."

접근성이 대체 뭔데

macOS 엔 접근성 API 라는 게 통째로 있어. 다들 AX 라고 부르지. 스크린 리더나 스위치 컨트롤, 음성 제어 같은 보조 도구가 다른 앱 화면을 읽고 조작할 수 있게 만들어둔 거야. 생각해보면 대단한 권한이지. 앱 하나가 다른 앱 화면을 들여다보고 움직이는 거니까. Flint 는 스크린 리더가 아니지만 딱 그 힘이 좁은 목적으로 필요해. 지금 앞에 있는 앱에서 내가 긁어놓은 글을 읽어야 하거든. 그래서 보조 기술이 쓰는 바로 그 API 를 같은 권한으로 써.

요소로 이루어진 나무

앱은 저마다 자기 화면을 AXUIElement 라는 마디들의 나무로 내놔. 창, 버튼, 입력 칸 하나하나가 마디고, 마디마다 물어볼 수 있는 게 있어. 무슨 역할인지, 값이 뭔지, 지금 포커스를 쥐고 있는지, 안에 뭐가 긁혀 있는지. 시스템 전역 요소에서 출발해 포커스된 요소로 걸어가고, 거기다 물어보는 거야. 긁어놓은 글을 읽는 일이 결국 그 나무의 마디 하나한테 속성 하나를 물어보는 일이지. 이게 좋은 건 어디서나 똑같이 생겼다는 점이야. 제대로 구현한 네이티브 앱이면 같은 질문 몇 개가 전부 통해.

AXUIElementCreateSystemWide()
   -> kAXFocusedUIElementAttribute   (아무 앱에서든 지금 포커스 쥔 것)
        -> kAXSelectedTextAttribute       (긁어놓은 글)
        -> kAXSelectedTextRangeAttribute  (어디서 어디까지인지)
        -> kAXRoleAttribute / kAXSubroleAttribute  (어떤 종류 칸인지)

나무 하나, 질문 몇 개. 읽는 건 이게 전부다.

권한이 전부야

AX 로 아무 앱이나 읽고 조종할 수 있으니까 macOS 는 이걸 꽉 잠가놨어. 사용자가 시스템 설정에서 대놓고 접근성 권한을 주기 전엔 Flint 는 아무것도 못 해. AXIsProcessTrusted() 가 그 권한이 있는지 알려주고, 뭘 읽으려 들기 전에 그것부터 봐야 하지. 사용자가 문을 열어주기 전까지는 모든 읽기가 실패해. 그게 맞고. 권한은 돌아가야 할 장애물이 아니라 이 기능 전체가 딛고 선 신뢰거든.

받은 권한은 딱 그 용도만큼만 써. 접근성은 Flint 한테 아무 앱이나 읽고 조종할 힘을 줘. 필요한 것보다 한참 넘치게. 그러니 그 힘을 최소한만 쓰는 게 규율이야. 포커스된 자리에서 긁어놓은 글을 읽고, 결과 하나를 넣고, 그 외엔 아무것도. 넓은 권한을 쥔 앱은 그 안에서 좁게 움직일 의무가 있어. 사용자는 기술적으로 가능한 모든 곳에 그 힘을 쓰지 않을 거라고 믿고 문을 열어준 거니까.
보조 기술이 쓰는 바로 그 문이야. 그래서 이 문이 존재하고, 원칙적으로 믿을 만해. AX API 는 편법이 아니라 macOS 의 당당한 일부고 문서도 다 나와 있어. 안전한 이유가 힘이 작아서는 아니야. 힘은 커. 사용자가 대놓고 허락해야 열리고, 좋은 앱이라면 그 안에서 좁게 쓰니까 안전한 거지.

Code

권한부터 본다. 없으면 아무것도 안 읽는다·swift
import ApplicationServices

// 접근성 권한이 있나? 뭘 읽기 전에 이것부터.
func hasAccessibility() -> Bool {
    AXIsProcessTrusted()
}

// 없으면 요청한다. 사용자한테 물어보고 시스템 설정 화면을 열어준다.
func requestAccessibility() {
    let opts = [kAXTrustedCheckOptionPrompt.takeUnretainedValue(): true]
    _ = AXIsProcessTrustedWithOptions(opts as CFDictionary)
}

// 모든 읽기의 출발점: 시스템 전역 요소.
let systemWide = AXUIElementCreateSystemWide()
// 여기서 포커스된 요소로 걸어간다 (다음 레슨).

External links

Exercise

접근성 권한은 앱 하나한테 이 컴퓨터의 다른 모든 앱을 읽고 조종할 힘을 줘. Flint 가 기술적으로는 할 수 있지만 절대 하면 안 되는 일 세 가지를 적어봐. 그리고 넓은 권한을 받은 앱을 좁게 붙들어두는 원칙이 뭔지도. 그다음, 권한을 아직 안 준 사용자가 처음으로 매크로를 쳤을 때 뭐가 일어나야 하는지 말해봐.
Hint
기술적으로는 뒤에 떠 있는 창의 글을 훑거나, 사용자가 건드리지도 않은 문서를 읽거나, 읽은 걸 어딘가로 보낼 수도 있어. 전부 좁은 허락을 배신하는 짓이지. 붙들어두는 원칙은 최소한만 쓰는 거야. 넓은 힘을 쥐되 사용자가 이해한 그 기능 안에서만 움직이기. 권한 없이 처음 쳤을 땐 아무것도 쓰면 안 돼. 권한이 없다는 걸 먼저 알아채고, 설명하고, 설정 화면을 열어줘야지 조용히 실패하면 안 되고.

Progress

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

댓글 0

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

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