Outsider's Dev Story

Stay Hungry. Stay Foolish. Don't Be Satisfied.
RetroTech 팟캐스트 44BITS 팟캐스트

Keyboard Maestro와 JXA로 특정 WiFi에서 Dropbox 앱 제어하기

오랫동안 15인치 맥북 프로를 주력으로 사용하다가 올해 서브 맥북으로 12인치 뉴 맥북을 장만하고 개인 맥북으로 코딩할 시간이 많지 않은 평일에는 맥북을 가지고 다니고 카페에 오래 있는 주말에는 맥북 프로를 사용하고 있다. 맥북 프로를 가지고 다닐 때는 HHKB BT를 가지고 다니므로 편하고 평일에는 가방이 가벼워서 너무 좋다.(키감은 수개월이 지나도 전혀 적응이 안 되지만...)

맥북을 2개를 바꿔가면서 사용하다 보니 두 맥북의 동기화가 문제가 되었다. 대부분의 설정이나 이런 부분은 Dropbox를 이용해서 이미 동기화를 하고 있었고 안된 부분은 추가로 처리했다. 가장 큰 건 보통 개발을 하는 프로젝트 폴더였는데 오랫동안 쌓인 많은 프로젝트도 있고 여기에 많은 모듈도 설치되어 있으므로 용량이 12GB 정도 되었다. 이 프로젝트 폴더 전체를 동기화하는 방법을 여러 가지로 고민했는데 최종적으로는 기존의 사용하던 Dropbox를 유료로 결제해서 사용하고 있다. 초기에 두 맥북 간에 동기화를 하는데 아주 오래 걸렸지만, 지금은 작업한 내용만 동기화하므로 많은 시간이 걸리지는 않았다.(파일이 많아서 인덱싱에는 많은 시간이 걸리지만...)

그동안 잘 사용하고 있었는데 하나의 맥북을 사용할 때는 파일을 이리저리 계속 수정하고 모듈도 설치했다 지웠다 하는데 이게 바로바로 동기화되니까 외부에서 내 모바일 데이터가 낭비되는 기분이 들었다.(실제로도 그러고 있고..)

그래서 사실 두 맥북을 바꿔 쓸 때만 동기화하면 되므로 외부에서는 동기화되지 않다가 집에 왔을 때만 동기화가 되었으면 좋겠다는 생각을 했다. 내가 외부에 오면 동기화를 끄고 집에 오면 키면 좋겠지만, 이는 매우 귀찮은 일이므로 잘 될 리가 없었고 이게 자동화되었으면 좋겠다는 생각을 했다.

특정 WiFi에서만 동작하는 방법이 있는지 SNS에 묻자 AppleScript로 WiFi를 확인하는 방법과 ControlPlaneKeyboard Maestro를 추천받았다. AppleScript로 작성하는 방법은 결국 크론탭 같은 거로 계속 확인해야 하므로 실제로는 WiFi에 접속할 때 특정 액션을 호출하는 프로그램이 필요했는데 ControlPlane은 작년에 릴리스 이후 릴리스가 없어서(GitHub에 커밋은 되는 것 같았지만...) 불안했고 이미 사서 가지고 있던 Keyboard Maestro에 관심이 갔다.

Keyboard Maestro

Keyboard Maestro는 특정 상황이나 단축키를 통해서 원하는 액션을 자유롭게 실행할 수 있는 macOS 유료 앱이다.

Keyboard Maestro의 매크로

Keyboard Maestro에서 새로운 매크로를 만들면 트리거로 Wireless Network Trigger를 선택할 수 있는 것을 볼 수 있다.

Keyboard Maestro의 WiFi 트리거

Wireless Network Trigger를 선택하면 WiFi 이름을 체크해서 연결과 연결 해지에 따라 특정 액션을 선택할 수 있다는 것을 알게 되었다.(Keyboard Maestro를 산 지 꽤 오래되었지만 잘 사용하지는 못했다.. 이번에 7로 유료 업그레이드를 했지만..)

이 트리거에 연결할 수 있는 액션으로 수많은 액션이 있지만 내가 원하는 것은 Dropbox의 동기화를 키거나 끄는 기능이므로 결국은 스크립트를 실행하도록 해야 했다. Keyboard Maestro에서 실행할 수 있는 스크립트는 AppleScript, Shell Script, Swift Script, JavaScript 등 다양하게 있는데 내가 선택한 건 내가 익숙한 JavaScript이다. 트리거가 되는 건 확인했으므로 원하는 동작을 하는 스크립트만 작성하면 되는 것이었다.

JavaScript for Automation

macOS는 AppleScript를 내장하고 있지만, 요세미티부터 Automation에 JavaScript를 사용할 수 있는 JXA(JavaScript for Automation)를 지원하고 있다. Node.js와 상관없이 macOS 네이티브로 JavaScript를 사용할 수 있다는 의미이다.(물론 완전히 같지 않으므로 여기에는 수많은 삽질이 필요하고 문서 상태도 엉망이다.)

이 글은 JXA의 사용법을 설명하는 것은 아니므로 필요한 부분만 설명하겠다. 기본적으로 JXA는 macOS의 오토메이터에서 실행해 주는 것이므로 JavaScript면 실행이 가능하지만 오토메이터에서 매번 테스트하는 것은 귀찮으므로 파일 상단에 #!/usr/bin/env osascript -l JavaScript를 지정하면 터미널에서 바로 실행해 볼 수 있다.(실제 오토메이터 등으로 실행할 때는 이 부분이 필요 없다.)

#!/usr/bin/env osascript -l JavaScript

console.log('Hello World');

위 파일이 test.js라고 하면 이 파일에 실행 권한을 주고 실행하면 Node.js와 비슷하게 바로 실행할 수 있다.

./test.js
Hello World

AppleScript를 알면 좀 더 작성하기 쉽긴 한데 AppleScript와 비슷하게 JXA로도 Mac의 애플리케이션에 접근할 수 있다.

#!/usr/bin/env osascript -l JavaScript

const itunes = Application('iTunes');

console.log( itunes.id() ); // -> com.apple.iTunes
console.log( itunes.running() ); // -> false

iTunes 앱을 가져와서 id를 확인하고 현재 실행 중인지를 확인할 수 있다. 여기서 itunes.play(), itunes.pause(), itunes.stop()같은 메서드로 아이튠스를 코드로 제어할 수 있고 비슷한 작업을 Dropbox 앱으로도 할 수 있다.

#!/usr/bin/env osascript -l JavaScript

const dropbox = Application('Dropbox'); 

console.log(dropbox.id()); // com.getdropbox.dropbox
console.log(dropbox.running()); // true


JXA로 Dropbox를 제어하기 위한 삽질

잘 동작하는 것 같았지만 Dropbox에 어떤 메서드가 노출되어 있는지 알 수가 없었다. Object.keys(dropbox)같은 거로 돌려봐도 아무것도 나오지 않았고 코드 실행으로는 이 내용을 테스트해보기가 어려웠다. 메서드 혹은 속성이 있는지 확인해 보려고 출력하면 function () { [native code] }라고 나오지만 실행하면 Error: 메시지를 이해할 수 없습니다. (-1708)같은 오류만 나와서 실행을 잘못한 건지 파라미터가 잘못된 건지 도저히 알 수가 없었다.

찾다 보니 다행히 JXA를 repl형태로 실행해 볼 수 있는 프로젝트가 있어서 테스트를 해보았다.(Node.js의 repl을 osascript -l JavaScript로 실행한 것이다.)

> const itunes = Application('iTunes');
undefined

> itunes.running()
true

> itunes.running
[object JXAReference => function () {
    [native code]
}]

> itunes.asdfzxcv
[object JXAReference => function () {
    [native code]
}]

> itunes.asdfzxcv()
0:49: execution error: Error on line 1: Error: 메시지를 이해할 수 없습니다. (-1708)
undefined

JXA repl로 일단 많이 알려진 iTunes를 테스트해본 것인데 running이라는 존재하는 함수와 도저히 없을 것 같은 asdfzxcv가 모두 function() {}으로 출력된다. 내부 구조는 모르지만 내 추측으로는 모든 속성은 JXAReference라는 객체가 받게 되고 이를 macOS 앱으로 전달하는 것으로 보인다. 그래서 실행해 보기 전에는 존재 여부를 알 수 없고 실행을 했는데 문제없으면 결과가 나오지만 이러한 메서드가 없다면 오류가 나므로 그 결과를 보여주는 것이다.

결국, 속성을 찾을 수 없다는 결론에 이르고 있었는데 그럼 얼마안되는 JXA 문서에 나오는 iTunes, Mail 등 앱에 대한 JXA 예제의 코드는 어디서 나온 것인가 찾다 보니 macOS에 기본 설치된 스크립트 편집기라는 앱을 알게 되었다.

스크립트 편집기의 라이브러리

이 앱에서 [윈도우] 메뉴의 [라이브러리]를 실행하면 위 화면처럼 기본으로 포함된 앱의 목록이 나온다. 여기서 iTunes를 클릭해 보면 다음과 같이 사용할 수 있는 메서드 목록을 볼 수 있다.

스크립트 편집기의 iTunes API

앱에서 사용할 수 있는 인터페이스를 모두 볼 수 있고 AppleScript, JavaScript, Objective-C 별로 코드를 볼 수 있다. 저 라이브러리에서 +를 누르면 추가할 수도 있지만 Dropbox에 대한 스크립트 파일은 없으므로 추가할 수 없었다. 보통의 애플리케이션 개발을 생각했을 때 저 API는 앱을 개발할 때 퍼블릭으로 노출한 API일 테고 Dropbox에는 이러한 API 문서를 찾을 수가 없었고 세부 기능까지 퍼블릭으로 제공한다는 보장을 할 수도 없었다. 문서도 공개 안 한 거 보면 노출 안 했을 가능성이 훨씬 높아 보였다.

비슷한 질문은 있어도 해결책은 마땅히 없어서(아직 베타인 dbxcli가 있지만 여기선 주로 파일 제어만 한다.) 거의 포기 직전이었다. dropbox의 다른 API 문서를 보고 대충 있을 것 같은 메서드를 호출해 봤지만, 도저히 찾을 수도 없었고 단순 기능 이상의 동기화를 시작하고 멈추는 기능은 있을 것 같지 않았다.

그러다가 앞에서 id(), running()같은 일반적인 메서드는 존재했던 것처럼 launch(), quit()로 앱을 시작하고 종료할 수 있다는 것을 알게 되었다.

Keyboard Maestro와 JXA로 Dropbox 연동하기

원하는 것은 집에 들어가면 즉, 집에 있는 WiFi에 접속하면 Dropbox의 동기화를 시작하고 집에 있는 WiFi에 접속하지 않으면 Dropbox 동기화를 멈추는 것이었지만 동기화 대신 앱을 종료했다가 실행해도 결과는 똑같다는 생각이 들었다. 어차피 앱이 꺼져있으면 동기화를 안 할 것이고 앱이 시작하면 자동으로 동기화를 시작할 테니까...

그래서 Keyboard Maestro에 매크로를 2개 만들었다. 삽질한 고생에 비해서는 결과물은 아주 간단하다.

집 WiFi에서 Dropbox를 켜는 매크로

간단하므로 뻔하지만 "Home"이라는 무선 네트워크에 접속하면 하단의 JXA를 실행한다. 코드가 짧아서 그냥 매크로에 넣어버렸고 결과를 볼 필요는 없으므로 비동기로 실행했다. 코드는 Dropbox 앱을 가져와서 동작하고 있는지 확인하고 꺼져있다면 실행을 한다.

집 WiFi에서 Dropbox를 끄는 매크로

이번엔 반대의 매크로다. "Home"이라는 무선 네트워크의 접속이 끊기면 Dropbox가 실행 중인지 확인하고 실행 중이면 종료한다. 테스트해보면 아주 잘 된다. 며칠째 사용하고 있는 데 큰 문제는 없는 것으로 보이고 한두 번 동작 안된다고 큰 문제가 있는 것도 아니긴 하다.(동기화 중에 갑자기 종료해버렸을 때 문제가 생기는 건 아닌지 약간 걱정되기는 하다. 그래서 동기화 멈춤을 하고 싶었던 건데...)

결과물이 너무 간단해서 허무하지만 JXA의 맛 정도는 봤다. 조금씩 써보기 시작해야지.

2016/10/26 00:40 2016/10/26 00:40