요세미티에서 자바스크립트로 자동화 하기

alex guyot

Alex Guyot

Twitter: @the_axx | Email: guyot@macstories.net

Alex는 MacStories에 iOS 앱에 대한 가이드를 기고하고 있다. 또한 개인 블로그인 Unapologetic에서 iOS 자동화 워크플로우에 관한 내용도 다루고 있다.

Alex Guyot가 MacStories에 기고한 Getting Started with JavaScript for Automation on Yosemite를 번역한 글입니다.

나는 지난 달에 MacStories에 OS X 요세미티의 확장가능성과 자동화에 관한 변경사항을 작성하면서 요세미티가 지원하는 자바스크립트를 이용한 자동화(JXA, JavaScript for Automation)를 간단히 살펴봤다. JXA 릴리스 정보WWDC의 세션 영상을 단순 요약하는 글을 쓰고 싶지 않았기 때문에, 지난 번 글을 쓰면서 JXA의 기본적인 부분을 공부했다.
JXA는 새롭게 지원되는 기능이라 관련 정보를 찾아보기가 어려웠다. 나는 AppleScript를 다뤄본 적이 없기 때문에 AppleScript를 이용한 OS X 자동화에 관한 글은 도움이 되지 않았다. JXA의 기본적인 부분을 파악하는데 상당한 시간이 걸렸다. 이번 글을 통해서 이 과정에서 알게된 점과 JXA의 기본적인 부분을 설명하려고 한다.

이 글에서 다루는 것과 다루지 않는 것

이 글을 읽고 나면 아래처럼 JXA의 기본적인 사용법을 알 수 있다.

  • 메소드 호출을 위한 문법
  • 앱에 따라 다른 메소드 호출의 종류
  • UI를 이용한 자동화를 위해 UI 요소를 찾는 방법

나는 이 글을 기본적인 Javascript 지식을 갖추고 있고, 이를 응용해 맥용 앱에 쓸 스크립트를 만들어보고 싶은 사람을 위해 작성했다. 따라서 JavaScript에 대해 자세히 설명하지 않는다.
이 글은 JXA의 모든 측면을 설명하지도 않는다. 나는 Objective-C는 모르기 때문에, JXA의 가장 강력한 부분이라는 JXA Objective-C 브릿지에 대해서 다루지 않는다는 점도 함께 알려둔다.

자바스크립트를 이용한 자동화(JXA) 시작하기

JXA 스크립트는 스크립트 편집기(Script Editor) 앱으로 작성할 수 있다. 모든 맥에는 스크립트 편집기가 기본 설치되어 있다. 응용프로그램/유틸리티 폴더에서 찾아볼 수 있다.
스크립트 편집기는 코드 편집을 위한 도구로는 부족한 점이 많다. 문법 강조표시 기능이 실시간으로 지원되지 않고, 자동완성도 찾아볼 수 없다.
그렇지만 이런 단점을 무시하게 해주는 장점이 있다. 실행(Play) 버튼을 누르면 바로 JXA 스크립트를 실행할 수 있고, 아래 쪽의 작업창에서 바로 결과를 확인할 수 있다. 설치한 모든 앱의 스크립트 라이브러리에 접근해서 해당 앱이 스크립트에 지원하는 기능을 확인할 수 있다. JXA를 쓰기 위해 각자가 사용하는 편집기를 써도 좋지만, 이 글에서는 스크립트 편집기를 이용했다.
스크립트 편집기
스크립트 편집기를 실행하면 창이 위 아래로 나누어져 있다. 위의 넓은 영역에 코드를 작성할 수 있고, 아래의 작은 영역은 실행 시에 스크립트에 관한 정보를 표시한다. 이 작은 영역에 노출하는 내용은 설명, 결과, 이벤트 로그 중 하나로 선택할 수 있다. 기본값은 설명이고, 스크립트를 실행하면 결과로 바뀐다. 그렇지만 설명이나 결과보다는 이벤트 로그가 가장 유용하다. 스크립트 실행시 발생하는 모든 이벤트를 볼 수 있기 때문이다. 이벤트 로그를 보려면 위의 스크린샷처럼 좌측 하단의 세가지 버튼 중에 세번째 버튼을 누르면 된다. 박스 안에 줄이 쳐져있는 버튼이다. 이 버튼을 선택하고 위쪽에서 이벤트 탭도 선택하자.
코드 영역에는 네 개의 버튼이 있다. 기본적인 스크립트를 작성한다면 첫번째를 제외한 나머지 세 버튼만 사용하게 될 것이다.

  • 중지 버튼

    스크립트 실행을 중지할 수 있다. 반복문을 멈추게 할 때 특히 유용하다.

  • 실행 버튼

    스크립트를 실행한다.

  • 컴파일 버튼

    스크립트를 컴파일한다. 코드를 실행하기 전에 문법 오류를 확인할 수 있고, 마지막 실행이나 컴파일 이후에 작성한 새로운 코드에 문법 강조를 적용할 수 있다. 앞서 단점에서 말한 것처럼 문법 강조가 실시간이 아니지만, 이렇게 컴파일을 하고 나면 문법 강조가 적용된다.

버튼 아래에는 드롭다운 메뉴가 있다. 기본값은 AppleScript인데, JavaScript로 바꾸자. 기본값을 변경하려면 스크립트 편집기의 설정을 JavaScript(1.0)로 수정할 수 있다. 이제 기본적인 준비가 끝났다. 자동화할 앱을 고르고 스크립트를 작성해 보자.
OS X에서 앱을 자동화하는 법은 크게 두가지가 있다. 하나는 앱 개발자가 준비해놓은 메소드를 호출하는 방법이고, 다른 하나는 앱의 UI를 이용한 자동화이다. 그 중에서도 메소드를 이용하는 방법이 훨씬 쉽고 간단하다.
특정 앱이 지원하는 메소드는 스크립트 사전에서 확인할 수 있다. 스크립트 사전은 스크립트 편집기에서 윈도우 > 라이브러리를 확인하거나, 파일 > 사전 열기에서 확인할 수 있다. ‘사전 열기’를 이용하면 스크립트를 적용할 수 있는 모든 앱을 확인할 수 있는데, 다만 특정 앱의 사전을 열면 사전 열기 창이 없어진다.
반면에 라이브러리 창은 좀 더 목록이 적지만 다른 앱의 사전을 열어도 닫히지 않기 때문에, 여러 개의 사전을 열어놓고 스크립트를 작성해야 할 때 유용하다. 작은 창의 + 버튼을 누르면 라이브러리에 앱을 추가할 수 있다. 자동화하려는 앱의 사전을 열면 해당 앱이 지원하는 모든 메소드와 속성 목록을 확인할 수 있다. 메소드 호출을 이용한 스크립트를 작성할 때 필수적인 도구다.

자동화 스크립트 예제

릴리스 정보나 스크립트 사전에 정보가 잘 구성되어 있지만, 반면에 예제 코드는 매우 부족하기 때문에 JXA를 시작하기가 쉽지 않았다. 나는 JXA 코드를 좀 더 쉽게 이해할 수 있게 두 개의 스크립트를 만들었다. JXA 릴리스 정보와 함께 내가 작성한 스크립트를 함께 보면 자동화 스크립트를 만드는 대략적인 흐름을 이해할 수 있고, 코드를 좀 더 큰 그림에서 바라볼 수 있다.
나는 이 글에서 iTunes를 자동화했는데, 대부분 설치한 앱이기도 하고, 전부는 아니지만 상당히 많은 스크립트 라이브러리를 지원하기 때문에 예제로 적합하다고 판단했다.
iTunes 12의 사용자 인터페이스는 형편없다. 그렇지만 여기서 작성한 스크립트를 이용해 내가 매일 iTunes를 쓰면서 겪었던 고통을 줄일 수 있었다.
내가 iTunes에 가장 많이 사용하는 기능은 당연하겠지만 음악 재생이다. 난 즐겨듣는 100곡을 하나의 재생목록으로 만들어서 무작위 재생을 한다. 이 재생목록을 종종 업데이트하기도 한다. 대체로 이 재생목록으로 음악을 듣는 편이다. 그렇지만 iTunes의 인터페이스는 재생목록을 기본적으로 열어놓을 수 없고, 재생목록으로 접근해서 재생하는 과정이 번거롭다. 이 과정을 스크립트로 자동화하고 싶었다.
첫번째 스크립트에서 이 기능을 구현했다. iTunes의 재생목록의 이름을 기준으로 해서, 재생목록을 재생하는 과정을 JXA 메소드를 이용해 자동화했다. 이 스크립트는 무작위재생을 하게 만든 것이 아니기 때문에, 마지막으로 직접 선택한 반복 옵션을 사용한다. 그러니까 나처럼 무작위 재생을 쓰는 사람이 아니더라도 이 스크립트를 사용할 수 있다.
스크립트를 실행하면 먼저 iTunes 플레이어의 상태를 확인한다. ‘정지’, ‘재생중’, ‘일시정지’, ‘빨리감기’, ‘뒤로감기’이다. 빨리감기나 뒤로감기를 하고 있을 때는 스크립트를 실행할 이유를 찾을 수 없었기 때문에, 이 부분은 고려하지 않기로 했다.
대신 iTunes의 상태가 재생중이면 재생되고 있는 곡의 정보를 보여주고, 일시정지, 재생목록 처음부터 듣기, 취소, 아무 것도 하지 않기 같은 옵션을 만들었다. 일지정지 상태이면 나머지는 그대로이고 일시정지 옵션이 재생으로 바뀐다. 정지되어 있으면 재생과 취소의 두가지 옵션만 보여준다.
그럼 코드를 통해 직접 살펴보자.

내장 메소드를 이용한 자동화

처음으로 살펴볼 JXA 메소드는 Application() 메소드이다. 이 메소드는 응용프로그램 폴더의 이름을 이용해 앱을 반환하는데, 이렇게 하면 해당 앱이 지원하는 메소드를 사용할 수 있다. iTunes라면 이렇게 작성할 수 있다.

iTunes = Application('iTunes')

이제 iTunes 객체를 이용해서 iTunes 스크립트 사전에서 확인한 메소드를 호출할 수 있다.
가장 먼저 스크립트에 필요한 것은 iTunes 플레이어의 상태를 받아서 띄워야할 창을 정하게 하는 것이다. 윈도우 > 라이브러리 > iTunes를 더블클릭해서 스크립트 사전을 열자. 사전의 상단으로 세 개의 창이 보인다.
가장 왼쪽의 창을 보면 도구 모음(Suite) 목록을 확인할 수 있다. 각각의 도구 모음은 iTunes 객체에서 접근할 수 있는 다양한 속성과 메소드를 담고 있다. 표준 도구 모음(Standard Suite)는 거의 대부분의 앱에서 확인할 수 있는데, 동일한 속성과 iTunes 인쇄 설정같은 별로 유용하지 않는 메소드를 담고 있다.
우리에게 필요한 것은 iTunes 도구 모음(iTunes Suite)이다. iTunes 도구 모음을 선택하면, 중앙의 창에 iTunes 앱에서 사용할 수 있는 객체와 메소드를 확인할 수 있다. 목록마다 아이콘을 볼 수 있다. 둥근 파란색 C 아이콘은 호출할 수 있는 메소드이고, 보라색 사각형 C 아이콘은 객체이다.
객체를 선택하면 우측의 창에서 해당 객체의 요소(Element)와 속성(Property)을 확인할 수 있다. 노란색 E 아이콘이 요소, 보라색 P 아이콘이 속성이다.
세 창 아래를 보면 선택한 객체나 메소드에 대한 상세한 정보를 볼 수 있다. 원하는 것을 찾기 위해 스크롤을 해도 좋고, 상단에서 필요한 아이템을 골라 선택하면 더 쉽게 필요한 정보를 얻을 수 있다. 가장 유용한 것은 Application 객체이다. 스크립트 사전에 있는 개별 앱의 도구 모음에서 항상 찾아볼 수 있는 객체이기도 하다. iTunes의 Application 객체를 살펴보자.
iTunes 사전
Application 객체의 속성 목록 중에서 우리가 필요한 playerState 속성을 찾을 수 있다.
아래에서 playerState 속성의 자세한 정보를 확인할 수 있다.

  • “stopped” (정지)
  • ‌”playing” (재생중)
  • “paused” (일시정지)
  • ‌”fast forwarding” (빨리감기)
  • “rewinding” (뒤로감기)

괄호 안의 따옴표로 작성한 값은 playerState가 반환할 수 있는 값이고, r/o는 playerState가 읽기 전용 속성이라는 점을 알려준다. iTunes가 어떤 상태인지 확인하는 것은 가능하지만, 이 값을 바꿀 수는 없다. 따라서 play()pause()같은 메소드를 이용해서 상태를 변경해야 한다. 괄호 뒤에는 속성의 의미를 간단히 설명한다. 어떤 앱을 보더라도 이런 형식으로 사전이 정리되어 있어서, 객체에 접근하면 어떤 값을 반환하는지 알 수 있다.
스크립트로 돌아와서, 변수에 플레이어의 상태를 저장하자.

state = iTunes.playerState()

다음으로 if/else문을 이용해서 플레이어의 상태를 확인하고, 상태에 맞게 적절한 창을 띄우게 만들자. 앞에서 설명한 것처럼 “fast forwarding”이나 “rewinding” 같은 상태를 반환하는 경우는 고려하지 않기로 한다. 그러면 이렇게 세 개의 상태가 필요하다.

if (state == 'playing') {
    // 재생중일 때 알림창
} else if (state == 'paused') {
    // 일시정지일 때 알림창
} else {
    // 정지일 때 알림창
}

이제 알림창을 띄워야 한다. 여기서부터는 조금 더 복잡해진다. 기본적으로는 개별 앱에서는 시스템 창을 띄울 수 없다. 알림창을 띄우기 위해서는 StandardAdditions 패키지를 사용해야 한다.
StandardAdditions는 시스템 수준에서 Mac OS X와 상호작용하는 특별한 메소드 라이브러리이다. StandardAdditions에는 다양하고 유용한 메소드를 찾아볼 수 있다. ASCII 문자열 변환, 클립보드 접근, 파일 읽기 및 쓰기, 사용자 인터랙션을 위한 다양한 미디어를 갖추고 있다. 우리에게 필요한 알림창 메소드도 여기에서 찾을 수 있다.
StandardAdditions는 모든 앱의 스크립트 사전에서 확인할 수 있지만, 앱은 아니다. 따라서 다른 앱처럼 메소드를 이용해서 Application('StandardAdditions') 같은 방법으로 사용할 수는 없다. 대신에 기존 앱에서 includeStandardAdditions 속성을 true로 변경해서 이 메소드를 사용할 수 있다. 모드 앱은 이 속성을 가지고 있는데 기본값은 false이다. 이렇게 설명하면 iTunes에 includeStandardAdditions = true를 보내줘야 할 것으로 생각하겠지만, 그렇게 하면 원하지 않는 부작용이 발생한다.
지금 우리는 iTunes에서 음악 재생을 할 때 버튼을 반복해서 누르는 과정을 대신해주는 스크립트를 만들고 있다. 재생을 위해 iTunes 인터페이스를 볼 필요도 없다면 훨씬 더 편리할 것이다. 이것이 가능한 이유는 JXA Script를 AppleScript처럼 워크플로우 자동화 도구로 사용할 수 있기 때문이다. 키보드 단축키에 이 스크립트를 연결해서 어떤 앱에서든 기능을 실행할 수 있다. Keyboard Maestro 같은 앱을 사용하는 사람은 자동화 도구 없이도 이런 기능을 쓸 수 있겠지만, 별도로 이런 앱을 설치하지 않아도 자동화도구를 이용하면 시스템 설정을 이용해 키보드 단축키를 사용할 수 있다.
키보드 단축키를 눌렀을 때 iTunes 창이 화면 앞으로 나오는 것은 우리가 원하지 않는 기능이다. OS X에서는 앱이 알림을 띄우면 해당 앱을 확인할 때까지 Dock에서 깜빡이고, 창은 화면의 맨 앞으로 위치해서 알림창의 옵션을 선택할 것을 요구한다. 이 스크립트에서는 이런 과정을 피하기 위해서 알림창을 iTunes나 특정 앱에 연결하지 않기로 했다.
Application() 메소드를 이용하면 이런 문제를 해결할 수 있다. Application.currentApplication()를 이용하면, 메소드를 호출했을 때 사용하고 있는 앱을 반환한다. 이렇게 해서 현재 사용중인 앱에서 알림창을 띄우면, iTunes가 Dock에서 확인을 요하거나 창을 띄우는 일이 없게 할 수 있다.
현재 사용하는 앱에서 객체를 만들고, includeStardardAdditionstrue로 하면 된다. 아래 코드를 살펴보자.

currentApp = Application.currentApplication()
currentApp.includeStandardAdditions = true

여기까지 스크립트에 각각 iTunes와 현재 사용중인 앱을 위한 객체를 만들었다. 현재 앱에는 StandardAdditions를 활성화했고, iTunes의 재생 상태는 변수에 담았다. 이제 알림창만 만들면 된다. 우선 StandardAdditions 사전에서 문법을 살펴보자. 사용자 인터랙션 도구모음(User Interaction Suite)에 있는 displayAlert 메소드를 살펴봐야 한다.
displayAlert
이 메소드에는 매개변수가 있기 때문에 내용이 훨씬 복잡해보인다. 이 메소드의 JXA 문법은 아래에서 살펴볼 수 있다. 괄호 안에는 첫번째 매개변수가 있고, 이 뒤에는 중괄호로 묶은 매개변수 객체 리터럴이 나온다. 모든 매개변수에 값을 넘겨줄 필요는 없고, 필요한 값만 작성한다. 이 스크립트에서는 제목, 메세지, 버튼 세 개가 필요하다. 기본값 버튼과 취소 버튼을 만들어 놓으면 유용하다. 기본값 버튼은 Enter키를 눌렀을 때 작동하고, 취소 버튼은 Esc키를 누를 때 작동한다. 알림창을 만들기 전에 어떤 메세지를 표시할지 정해야 한다.
지난 몇 주 동안 스크립트를 만져보면서 가장 적절한 메세지를 표현하는 법을 찾아봤다. 재생중인 곡명, 가수, 남은 시간을 분/초 단위로 나타내게 했다. 알림 메세지로 전달하기 전에 몇가지 설정이 필요하다. 우선 iTunes 객체에서 재생중인 곡명과 가수에 관한 정보를 받아보자.

track = iTunes.currentTrack.name()
artist = iTunes.currentTrack.artist()

이렇게 해서 trackartist 변수에 곡명과 가수를 담았다. 남은 재생시간을 확인하는 것은 조금 더 복잡하다. iTunes는 재생한 시간을 제공하고, 이 값은 초만 확인할 수 있다. 분/초 단위로 변경하려면 코드를 몇줄 작성해야 한다. iTunes에서 곡의 전체 재생시간과 재생한 시간을 받아오고, 이 값을 계산해서 분/초 단위로 변경한다. 재생과 일시정지 알림창에 반복해서 쓰기 위해 함수로 만들어서 호출하기로 한다.

function calculatePlayerPosition() {
    playerPosition = iTunes.playerPosition()
    duration = iTunes.currentTrack.duration()
    secRemainder = (duration - playerPosition)
    minRemainder = Math.floor(secRemainder/60)
    secRemainder = Math.round(Math.abs(secRemainder) % 60)
    remainder = minRemainder + ":" + (secRemainder < 10 ? "0" + secRemainder : secRemainder)
    return remainder
}

이제 알림창을 만들 준비가 끝났다. playerState(플레이어 상태)가 playing(재생중)인 경우라면 이렇게 코드를 넣어볼 수 있다.

action = currentApp.displayAlert('iTunes Playback Options', {
    buttons: ['Playlist','Pause','Cancel'],
    message: 'There is currently ' + calculatePlayerPosition() + ' left in \'' + track + '\' by ' + artist,
    defaultButton: 1,
    cancelButton: 3
})

이 알림창을 호출하면 아래와 같은 화면을 확인할 수 있다.
Alert
paused(일시정지) 알림창에 쓰는 코드도 크게 다르지 않다. stopped(정지) 알림창은 메세지가 더 간단하다. 재생중인 곡이 없기 때문에 곡명, 가수, 재생시간을 표시하지 않아도 되기 때문이다. 바로 재생목록을 선택하기 때문에 재생이나 일시정지 버튼도 필요없다.
이제 거의 다 완성했다. 알림창에 응답하는 부분을 추가하면 끝이다. 알림창은 AlertReply 객체를 반환한다. 이 객체는 buttonReturned 속성을 가지고 있다. buttonReturned는 알림창에서 선택한 버튼의 텍스트로 구성된 문자열 객체이다. action 변수에 응답을 담았고, 이 값을 기준으로 어떤 수행을 할지 정한다. 아래의 코드를 이용하자.

playlist = 'Good Songs'
if (action.buttonReturned != 'Cancel') {
    if (action.buttonReturned == 'Playlist') {
        iTunes.playlists.play()
    } else {
        iTunes.playpause()
    }
}

이 코드는 먼저 취소(Cancel)를 눌렀는지 확인한다. 취소를 눌렀으면 아무 수행을 하지 않고, 취소를 누르지 않았다면 재생목록(Playlist) 버튼을 눌렀는지 확인한다. 재생목록 버튼은 playlist 변수에 play() 메소드를 실행한다. 위 예제에서는 playlist 변수를 if/else문 위에 적었지만, 실제 파일에서는 파일의 최상단에 작성해서 원하는 재생목록을 쉽게 수정할 수 있게 했다. 이 부분은 각자 재생하려고 하는 재생목록의 이름으로 변경해야 한다. 재생목록이나 취소를 선택하지 않았다면, 재생(Play)이나 일시정지(Pause)를 선택한 것이기 때문에, playpause() 명령을 실행한다. 이 메소드는 재생과 일시정지를 설정/해제하는 명령이다.
드디어 완성이다. 완성한 코드를 아래에서 확인하자. Github에서도 다운로드 할 수 있다.

playlist = 'Good Songs'
currentApp = Application.currentApplication()
currentApp.includeStandardAdditions = true
iTunes = Application('iTunes')
state = iTunes.playerState()
if (state == 'playing') {
  track = iTunes.currentTrack.name()
  artist = iTunes.currentTrack.artist()
  action = currentApp.displayAlert('iTunes Playback Options', {
    buttons: ['Playlist','Pause','Cancel'],
    message: 'There is currently ' + calculatePlayerPosition() + ' left in \'' + track + '\' by ' + artist,
    defaultButton: 1,
    cancelButton: 3
  })
} else if (state == 'paused') {
  track = iTunes.currentTrack.name()
  artist = iTunes.currentTrack.artist()
  action = currentApp.displayAlert('Play music in iTunes?', {
    buttons: ['Playlist','Play','Cancel'],
    message: 'Currently \'' + track + '\' by ' + artist + ' is paused with ' + calculatePlayerPosition() + ' remaining',
    defaultButton: 1,
    cancelButton: 3
  })
} else {
  action = currentApp.displayAlert('Play music in iTunes?', {
    buttons: ['Playlist','Cancel'],
    message: 'Currently nothing is playing.',
    defaultButton: 1,
    cancelButton: 2
  })
}
if (action.buttonReturned != 'Cancel') {
  if (action.buttonReturned == 'Playlist') {
    iTunes.playlists.play() // 1행에 작성한 재생목록을 재생한다.
  } else {
    iTunes.playpause()
  }
}
function calculatePlayerPosition() {
  playerPosition = iTunes.playerPosition()
  duration = iTunes.currentTrack.duration()
  secRemainder = (duration - playerPosition)
  minRemainder = Math.floor(secRemainder/60)
  secRemainder = Math.round(Math.abs(secRemainder) % 60)
  remainder = minRemainder + ":" + (secRemainder < 10 ? "0" + secRemainder : secRemainder)
  return remainder
}

앱 UI를 이용한 자동화

두번째로 앱 UI를 이용한 자동화(이하 UI 자동화)를 다루는 스크립트를 살펴보자. UI 자동화는 해당 앱이 메소드로 지원하지 않는 기능도 자동화할 수 있다. 맥용 앱에 대부분 포함되어 있는 접근성 프레임워크(Accessibility framework)를 사용해서 앱에서 마우스로 클릭하는 동작을 흉내내는 방법이다. 이 방식의 자동화는 어렵지는 않지만, 추측하고 확인하는 식으로 프로그래밍해야 하기 때문에 시간이 많이 소요된다. 그렇기는 해도 어떤 앱에나 적용할 수 있기 때문에 시도해볼 가치는 있다.
UI 자동화의 첫 단계는 시스템 이벤트(System Events) 객체를 생성하는 것이다. 시스템 이벤트는 앞에서 살펴본 StandardAdditions와 비슷하다. 특정 앱이 아닌 시스템 전체에 쓸 수 있는 메소드 라이브러리이다. UI 자동화를 할 때는 개별 앱 객체를 쓰지 않고 시스템 이벤트 객체만 사용한다. 접근성 프레임워크를 조작하려면 개별 앱이 아니라 시스템 자체를 조작해야하기 때문이다. 시스템은 개별 앱을 프로세스(Processe)로 본다. 따라서 iTunes의 사용자 인터페이스를 조작하려면, iTunes 프로세스를 다루는 시스템 이벤트 객체를 생성해야 한다.

system = Application('System Events')
iTunesController = system.processes['iTunes']

위처럼 iTunes 프로세스를 다루는 객체를 만들고 여기에 이벤트를 보낼 수 있다. 프로세스 도구모음(Processes Suite)의 시스템 이벤트 스크립트 사전을 보면, 호출할 수 있는 메소드와 사용할 수 있는 객체를 살펴볼 수 있다.
대표적인 메소드는 click()이다. 앱에서 특정 지점을 클릭했을 때의 변화를 시뮬레이션 한다. 스크립트에 적용하려면 앱의 인터페이스에서 클릭해야하는 부분에 맞는 객체를 찾아내야 한다.
앱 인터페이스는 다양한 요소로 구성되어 있다. 프로세스 도구 모음에서 모든 요소를 찾아볼 수 있다. UIElement 객체를 구성하는 요소를 확인해보자. 이 요소들은 우리는 볼 수 없지만 접근성 프레임워크는 확인할 수 있는 계층에 존재한다. 인터페이스에서 객체를 클릭하려면, 인터페이스 계층을 타고 들어가서 객체의 위치를 확인해야 한다. 그 후에 click() 메소드를 실행한다. 나는 이런 계층 구조를 파일 시스템처럼 생각하는 것이 편했다. 어떤 경로에서 다른 경로로 이동하려면 폴더를 여러번 열어 들어가고 파일을 더블 클릭해서 여는 것과 비슷하다. 폴더 대신에 uiElements에서 필요한 요소를 찾아내는 것이다.1
사용자 이벤트 사전의 프로세스 도구 모음을 살펴보면 사용자 인터페이스를 잘 이해할 수 있다. Application 객체는 UI 자동화를 통해 조작하는 객체 중 최상위 단계에 있다. 스크립트 사전 상단 두번째 창에서 Application 객체를 선택한다. 아래 설명을 보면 Elements라고 된 섹션이 있다. 그 뒤로는 Application 객체를 상속하는 요소의 목록이 나온다. 여기서 uiElements를 볼 수 있고, 클릭하면 UIElement 객체를 볼 수 있다. 이렇게해서 조작가능한 모든 인터페이스를 확인할 수 있고, 원하는 객체를 검색할 수도 있다. 이 객체들은 UIElement의 Elements 목록에서 확인할 수 있다.
사실 이 목록만으로는 이해할 수 있는 것은 별로 없다. 목록만 봐서는 실제로 앱이 어떻게 구성되었는지 알 수 없기 때문이다. 대부분의 UIElement 객체가 서로를 상속할 수 있고, 파일 시스템처럼 어디에든 위치할 수 있기 때문이다. Mac의 Finder에서 데스크탑이 어디있는지 알 수 있는 것과 달리 앱 인터페이스에서는 그런 부분이 없다. 예를 들어 Group 객체가 ScrollArea 객체 안에 있을 수 있고, 반대의 경우도 가능하다. 유일한 힌트는 버튼이 클릭 이벤트를 받기 위해 마지막 계층에 있을 것이라는 점이다.2 이렇게 복잡한 앱의 UI 계층을 쉽게 살펴보려면 터미널을 사용해야 한다.
터미널에서는 전체 스크립트를 쓰지 않고도 명령창을 통해 JXA를 테스트할 수 있다. 새 터미널 창을 열고 아래와 같이 입력한다.

osascript -l JavaScript -i

이렇게 입력하면 프롬프트에 ">>"가 나온다. JXA 명령을 사용할 수 있다는 뜻이다. UI 자동화 스크립트를 쓸 때 처럼 작성한다. 스크립트를 작성하면 실제로 명령을 수행한다. 아래같은 화면을 볼 수 있다.
Terminal
이제 iTunes 프로세스에 명령을 보내서 사용자 인터페이스를 찾아낼 수 있다. UI 자동화는 현재 보고 있는 데스크탑에 열려있는 앱에만 적용할 수 있다. 나처럼 데스크탑을 여러개를 사용하고 각각 앱을 띄워놓는 경우에는, iTunes 창이 자동화하려는 데스크탑에 위치하게 해야한다. 예를 들어 지금같은 경우에는 터미널 뒤에 백그라운드로 iTunes를 실행해놓아야 한다.
iTunes 객체에 명령을 이용해 UIElement를 보낼 수 있다. 터미널에서 현재 iTunes 창에 존재하는 해당 UIElement의 모든 인스턴스를 반환한다. iTunes의 기본 창에서 자동화를 한다면 다음의 명령으로 시작해보자.

>> iTunes.windows()

그러면 아래와 같이 반환한다.

=> [Application("System Events").applicationProcesses.byName("iTunes").windows.byName("iTunes")]

이 반환값을 통해 여러가지를 알 수 있다. Application("System Events").applicationProcesses.byName("iTunes")는 iTunes 객체에 대해 알려준다. iTunes는 시스템 이벤트 어플리케이션의 요소인 어플리케이션 프로세스이다. windows.byName("iTunes")를 보면 이 객체는 단일 창 객체이고 이름이 iTunes라는 것을 알 수 있다. 창이 하나이기 때문에 자동화도 이 창에서 모두 이뤄진다. 이제 iTunes 객체를 직접 탐색하는 대신에 iTunes.windows['iTunes']를 찾아보면 된다.
다음 단계는 간단하지 않다. 기본 iTunes 창을 살펴봐야 하는데, 여기서부터는 UI가 무한의 방법으로 구성되어 있다. 다행히도 entireContents() 명령을 이용하면 좀 더 쉽게 UI 구성을 살펴볼 수 있다. 이 명령을 실행하면 해당 앱의 사용자 인터페이스 계층에 있는 모든 UIElement를 반환한다. 현재 앱의 지도를 보여준다고 할 수 있는데, 여기에서 우리가 클릭하려는 요소를 분리해낼 수 있다.
터미널에 iTunes.windows['iTunes'].entireContents()를 입력하면, 엄청난 양의 텍스트를 볼 수 있다. 이 목록 안에는 모든 요소가 쉼표와 공백으로 분리되어 있다. 복사해서 텍스트 에디터를 이용해 쉼표 ,를 공백 두줄로 바꾸면 찾기가 좀 더 쉽다.3 이렇게 하면 위 아래로 공백이 있어 iTunes 인터페이스 요소를 알아보기가 좋다.
살펴보면 알겠지만, 앱 계층 안쪽으로 갈 수록 항목은 점점 늘어난다. 아래는 내 목록에서 고른 항목 중 하나이다.

Application("System Events").applicationProcesses.byName("iTunes").windows.byName("iTunes").radioGroups.at(0).radioButtons.at(2)

시스템 이벤트 앱의 iTunes 앱 프로세스에 있는 iTunes 창, 그 안에 라디오 그룹 중에도 2번째 라디오버튼. 대단하다. 그렇지만 이 버튼이 어떤 용도인지는 설명하지 않는다. 다행히도 용도를 알 수 있게 해주는 명령어가 여러개 있다.
시스템 이벤트 스크립트 사전에 UIElement 항목을 보자. 속성 목록(Properties List)가 있는데, 여기에는 UIElement에 첨부된 모든 속성을 확인할 수 있다. 앱 개발자가 이 속성을 사용했는지와는 상관없이, 대부분의 요소가 name(이름)을 찾으면 null을 반환한다.
다행히도 내가 살펴본 UIElement에는 모두 description(설명)이 선언되어 있었다. 위에서 찾아본 라디오버튼이 어떤 작동을 하는지 알고 싶다면 .description()을 끝에 붙여주면 된다.
이 부분은 JavaScript에서 객체를 참조하는 것처럼 바꿀 수 있다. 먼저 Application("System Events").applicationProcesses.byName("iTunes")는 앞에서 선언한 iTunesController로 바꿔서 쓸 수 있다. .byName()이나 .at()['iTunes'][0]로 바꿔쓰면 된다. 결과적으로 아래처럼 쓸 수 있다.

>> iTunesController.windows['iTunes'].radioGroups[0].radioButtons[2].description()
=> "TV Shows"

iTunes 인터페이스로 돌아가자. 버튼 중에 "TV Shows"라고 적혀진 버튼이 있다. 위의 값이 가리키는 것이 바로 이 버튼이다. 이 버튼을 클릭하게 하려면 위의 경로에 click() 메소드를 붙여주면 된다.

>> iTunesController.windows['iTunes'].radioGroups[0].radioButtons[2].click()

터미널 창에서 실행해보면 iTunes 창에서 TV Shows 섹션으로 넘어가는 것을 볼 수 있다. 이제 앱의 사용자 인터페이스 계층을 탐색하는 기본적인 방법을 알게 되었다.

UI 자동화 스크립트 예제

이제 이 글을 위해 준비한 두번째 스크립트인 iTunes용 UI 자동화 스크립트를 살펴보자. 이 스크립트는 iTunes Radio에서 스테이션을 재생한다. iTunes가 제공하는 메소드로 만들기에는 적절하지 않기 때문에 UI 자동화를 사용한다. 여기서는 내가 구성한 iTunes 라디오 스테이션 중 첫번째를 자동으로 선택해서 재생하게 하려고 한다.
앞에서 처럼 iTunes 시스템 프로세스 객체를 생성하자.

system = Application('System Events')
iTunesController = system.processes['iTunes']

iTunes 처럼 다양한 화면을 보여주는 앱에 UI 자동화를 적용하려면, 자동화가 실행되는 시점에 어떤 화면이 노출되고 있을지 알기가 어렵다. 그래서 처음 해야할 일은 iTunes 라디오 화면이 나오게 하는 것이다. 그렇지만 지금은 TV Shows 화면으로 되어 있다. UI 자동화는 버튼이 보이지 않으면 명령을 보낼 수 없기 때문에, iTunes 라디오로 가려면 기본 음악 탭으로 돌아가야 한다. 앞에서 TV Shows 버튼을 찾은 것을 기억할 것이다. 음악 버튼도 같은 그룹에서 찾을 수 있다. 음악 탭이 첫번째에 있기 때문에 순서는 0이라고 추측할 수 있을 것이다. 그러면 여기에 click() 메소드를 쓸 수 있다.

iTunesController.windows[0].radioGroups[0].radioButtons[0].click()

다음으로 iTunes 라디오 탭을 찾아야 한다. iTunes 창의 UIElement를 살펴보면 버튼 순서만 있는게 아니라 이름도 Radio라고 있는 것을 알 수 있다.

Application("System Events").applicationProcesses.byName("iTunes").windows.byName("iTunes").radioGroups.at(1).radioButtons.byName("Radio")

그러면 스크립트에서는 아래처럼 객체 참조 형태로 바꾸고 끝에 click() 메소드를 추가한다.

iTunesController.windows['iTunes'].radioGroups[1].radioButtons['Radio'].click()

iTunes 라디오 인터페이스는 한 행에 나열된 여러개의 이미지로 되어 있다. 제일 위의 행은 특집 스테이션이고, 아래쪽 행은 커스텀 스테이션이다. 나는 내 커스텀 스테이션 중 첫번째를 선택해서 재생하고 싶다. 따라서 두번째 행에 있는 첫번째 아이템의 이미지 번호를 알아야 한다.
iTunes 라디오 화면이 아닌 상태에서 터미널에 entireContents() 메소드를 수행하면 iTunes 라디오의 요소를 확인할 수 없다. 원하는 요소가 있는 화면으로 바꾸고 메소드를 호출해야 한다.
iTunes 라디오 스테이션 버튼은 이미지이고, 따라서 버튼 요소 대신에 이미지 요소를 찾으면 된다. 이미지 객체는 두 그룹으로 나눠지는데 특집 스테이션과 커스텀 스테이션에 쓰는 이미지이다. 나는 커스텀 스테이션이 두개 뿐이기 때문에 내 커스텀 스테이션 요소는 두개라는 것을 짐작할 수 있다. 순서로 0과 1이다. 특집 스테이션은 이미지가 5개다.4
내 UIElement 목록을 살펴보니 이미지 요소가 있는 곳을 두군데서 발견했다. 하나는 다섯개의 이미지이고 다른 하나는 두개의 이미지이다. 두개의 이미지가 있는 것을 호출해보니 아래처럼 나온다.

>> iTunes.windows['iTunes'].scrollAreas[1].uiElements[0].images[0].description()
=> "Good Songs"

내가 재생하려는 iTunes 라디오 스테이션이 Good Songs이다. 제대로 맞췄다. 클릭 메소드를 실행해보자.

iTunesController.windows['iTunes'].scrollAreas[1].uiElements[0].images[0].click()

터미널에서 위 코드를 실행하면 첫번째 iTunes 라디오 스테이션이 백그라운드의 iTunes 창에서 확대되는게 보인다. 확대된 라디오 스테이션에서 재생 버튼을 찾아서 재생하게 하면 된다. 처음에 entireContents() 메소드를 실행했을 때는 이렇게 스테이션이 노출되지 않았기 때문에, 먼저 찾은 목록에서는 재생 버튼을 찾을 수 없다. 지금은 스테이션이 노출되고 있으니 iTunesController.windows['iTunes'].entireContents()를 다시 실행해서 새로운 목록을 받으면 된다.
새로운 목록은 더 많다. 이번에는 이미지가 아닌 버튼을 찾아야해서 더 복잡하다. 하지만 iTunes는 이것을 쉽게 찾아볼 수 있게 준비해 놓았다. 여기에는 곡 목록이 있는데, 재생한 곡, 더 재생하려는 곡, 재생하지 않게 한 곡들이다. 내 인터페이스를 보면 제일 위에 있는 곡은 St. Lucia의 All Eyes on You이다. 아까 정리한 요소 목록에서 all eyes를 검색했다. 재생 버튼은 이 곡 목록보다 조금 위쪽에 있다. Good Songs라는 이름을 가진 버튼 요소를 찾을 수 있었다. 스테이션의 제목 버튼이다. 그 아래에 확인되지 않은 버튼 두개가 있는데, 하나는 재생, 다른 하나는 공유 버튼이다. 첫번째 버튼에 description() 메소드를 실행했더니, => "play"를 반환한다. 이렇게 필요한 요소를 찾았다.
각자의 스테이션 화면과 상관없이 재생 버튼은 동일할 것이다. 우리가 작성한 스크립트의 마지막에는 아래처럼 원하는 스테이션을 클릭해서 재생하는 코드를 쓴다.

iTunesController.windows[0].scrollAreas[1].uiElements[0].groups[0].buttons[0].click()

마지막으로 한가지가 더 남았다. UI 자동화의 약점 때문이다. iTunes 라디오 스테이션을 클릭할 때 iTunes UI를 보면, 스테이션이 확장하면서 애니메이션이 적용되는 것을 볼 수 있다. 스크립트를 애니메이션이 재생되고 나서 재생버튼을 클릭하게 적용해야 한다. 이 짧은 사이에 확장될 스테이션의 uiElements를 접근성 프레임워크에서 접근할 수 없다. 대체로는 문제가 되지 않지만 가끔씩 창이 커지는 시간이 오래 걸려서 재생 버튼을 클릭하지 못하는 경우가 있었다. 이럴 때 쓸 수 있는 메소드가 delay()이다. 스크립트에 한줄을 더 추가해서 0.5초를 기다렸다가 재생버튼을 클릭하게 하자.
완성한 UI 자동화 스크립트를 살펴보자.

system = Application('System Events')
iTunesController = system.processes['iTunes']
iTunesController.windows[0].radioGroups[0].radioButtons[0].click()
    iTunesController.windows['iTunes'].radioGroups[1].radioButtons['Radio'].click()
iTunesController.windows['iTunes'].scrollAreas[1].uiElements[0].images[0].click()
delay(0.5)
iTunesController.windows[0].scrollAreas[1].uiElements[0].groups[0].buttons[0].click()

맺음말

여기까지 살펴본 예제가 JXA 스크립트를 작성하는데 도움이 되었으면 한다.
UI 자동화보다는 메소드를 이용한 자동화를 할 것을 추천한다. UI 자동화는 원하는 무엇이든 자동화할 수 있는 장점이 있지만, 내장 메소드를 사용하는 것처럼 확실하지는 않다. UI 자동화에는 다른 단점도 있다. UI 자동화 스크립트를 적용하기 전에 해당 앱이 실행되어 있어야 한다. 또 앱이 업데이트 되면 UI 변경으로 스크립트가 무용지물이 될 수도 있다. 위치가 바뀔 수 있기 때문이다. 메소드를 이용해서 자동화를 하면 이런 부분은 걱정하지 않아도 된다. 업데이트를 통해 메소드를 제거할 가능성은 높지 않기 때문이다.
OS X의 자동화 앱은 매우 강력하고 시간도 상당히 줄여줄 수 있다. 내가 만든 iTunes 자동화 스크립트는 편의용이지만, 이것을 응용하면 앱을 더 유용하게 만들어 줄 수도 있다. 자신의 맥 워크플로우에 대해 고민해보고, 자주 사용하는 앱의 내장 메소드를 살펴보면 JXA를 이용해 업무 효율성을 높이는 방법을 찾을 수 있을 것이다.
자동화는 더 간단한 일에도 쓸 수 있다. Finder가 지원하는 메소드는 꽤 쓸모가 있는데, 특히 여러 파일을 이름을 변경하거나 옮기는 경우처럼 단순 작업을 반복할 때 쓰면 좋다. 스크립트를 작성해서 이런 작업을 자동화해보자.
자동화 스크립트를 쓰느라 할애하는 시간이 오히려 업무에 방해된다고 생각하는 사람이 있다면 아래에 Bruno Oliveira의 그래프가 생각을 바꿔줄지도 모르겠다.
 
Geek과 반복작업
 
그럼, 즐겁게 자동화 스크립트를 만들길 바란다!
 


 

  1. 여기서 "UIElement"와 uiElements"로 나눠서 작성한데는 이유가 있다. 스크립트 사전을 보면 복수(UIElemnet)와 단수(uiElements)의 대문자 표기 방식이 다른 것을 알 수 있다. 처음에는 조금 혼동스러울 수 있지만, JXA 문법이 그렇게 구성되어 있다. 하나는 UIElement이고 여러개이면 uiElements이다. 단수의 경우 앞글자를 대문자로 표기하고, 복수의 요소이면 카멜 표기법을 따른다. 왜 이렇게 했는지는 알 수 없지만 대부분의 경우 카멜 표기법으로 작성된 복수 형태를 사용하게 된다는 것을 기억해둘 필요가 있다. 그렇지만 스크립트 사전에서는 단수 형태로 작성되어 있다. 예외적으로 객체를 설명하는 요소 섹션에는 카멜 표기법을 따른 복수형을 사용한다.
  2. 이것을 파일 시스템에 빗대어 보자면, 버튼은 파일이라고 할 수 있다. 파일 시스템에서 파일에 접근했다면 파일 경로의 마지막에 도달했다고 할 수 있다. 파일 경로에서 파일보다 더 안으로 들어갈 수는 없기 때문이다.
  3. 쉼표를 공백 두줄로 바꾸는 방법은 간단하다. 터미널에서 공백을 복사하려면 새로운 줄의 마지막 문자부터 다음줄의 첫번째 문자 전까지를 복사한다. 그 다음 이것을 에디터에서 모두 바꾸기 기능을 이용해서 쉼표 대신 넣어주면 된다.
  4. 처음에는 이것을 이해하기가 어려웠다. 특집 스테이션을 스크롤해보면 다섯개보다 더 많은 스테이션을 확인할 수 있다. 그렇지만 UI 자동화는 화면에 실제로 나타나는 것만 볼 수 있다. 나는 13인치 레티나 MacBook Pro를 사용하는데 대체로 최대 5개의 스테이션이 나왔다. 스크롤을 하면 UI 순서상 첫번째였던 스테이션이 화면 밖으로 사라져서 UI 순서에서는 첫번째가 아닌 상태가 된다. 대신에 이 상태에서 인터페이스 목록으로 봤을 때 화면 가장 좌측에 노출되는 스테이션이 UI 순서상 첫번째가 된다.
카테고리: ResearchTech

KWAK Hyunchul

웃으세요, 웃기기 전에.

1개의 댓글

곽석종 · 2014년 12월 17일 3:32 오후

jxa 와 관련한 글 잘 읽었습니다~!
관련 사이트 url 첨부합니다.
애플 jxa 릴리즈 노트: https://developer.apple.com/library/mac/releasenotes/InterapplicationCommunication/RN-JavaScriptForAutomation/index.html#//apple_ref/doc/uid/TP40014508
공식 위키는 아니지만, 도움이 된 사이트: https://github.com/dtinth/JXA-Cookbook/wiki

답글 남기기

아바타 플레이스홀더

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다