아이패드에서 로컬 웹 서버 돌리기

고쳐짐 2023-09-27: 시간이 흐르고 흘러 World Wide Web이라는 쉽고 간편한 앱이 나왔습니다. 그냥 그거 씁니다.

아이패드와 맥북사이의 간극을 줄여봅시다. 이번에는 아이패드에서 localhost 웹서버를 돌려보겠습니다.

이제 아이패드에도 Files 앱의 open-in-place 기능을 활용한 코드편집기들이 제법 있습니다. 예를 들어, Kodex 를 이용하면 Files 에 들어있는 텍스트 파일을 복사본 없이 직접 수정할 수 있습니다.

파일과 폴더로 만들어진 웹 페이지를 웹 브라우저에서 미리보기 하고 싶을 때, 맥이라면 터미널을 실행하고 python3 -m http.server 를 돌린 다음 브라우저로 localhost:8000 을 열면 됩니다. 그런데 아이패드에서는?

이런 간편한 웹서버를 아이패드에서도 사용하고 싶은데, 어쩐지 쉬운 방법을 찾을 수가 없어서, 거의 에잇 내가 앱을 하나 만들자 단계의 직전까지 갔다가 다행스럽게도 iSH 를 이용한 해결책을 발견할 수 있었습니다.

우선 iSH 를 설치하고 (안타깝게도 앱스토어 배포가 힘들어서 TestFlight 를 이용해야 합니다),

apk add python3

python3 를 더해줍시다.

mount -t ios . ./www
  • mount -t ios : iSH 에서 UIDocumentPickerViewController 를 이용하여 Files 에 있는 폴더를 선택할 수 있습니다.
  • . : 이 인자는 무엇을 넣어도 무시됩니다.
  • ./www : 도큐먼트 픽커에서 선택한 폴더가 www 에 마운트 됩니다.

이제 www 폴더로 이동하여 python3 -m http.server 를 돌리면 일단 iSH 가 foreground 에 있을 때는 로컬 웹 서버가 작동합니다. 그러나 iSH 가 백그라운드로 들어가면 금방 멈춥니다.

iSH 는 백그라운드에서도 계속 실행되기 위한 방법으로 위치정보 요청하기를 사용합니다.

cat /dev/location > /dev/null &

이것을 python3 를 굴리기 전에 돌려줍니다.

그저 무기한으로 백그라운드에서 동작하기 위해서 무려 위치정보를 요청해야 한다는 사실이 마음 아프지만 (내 배터리…), 이 정도가 TestFlight 로 배포하는 iOS 앱이 할 수 있는 최선이겠죠.

python3 를 종료하고 용건이 끝나면 www 를 언마운트합니다.

umount ./www

이것으로 아이패드 사파리에서 localhost:8000 을 미리보기 할 수 있습니다.


NSSecureCoding으로 (un)archive

iOS 앱 개발을 하다보면 가끔 겉보기로는 별것 아닌 일에 막대한 시간을 들여 강도 높은 삽질을 하곤 하는데, 기록 차원에서 여기에 좀 남겨야겠다.

Metapho 3.2.4에서 최소 요구사항을 iOS 12로 올렸더니 NSSecureCoding과 관련된 많은 메소드들이 iOS 12에서 deprecated 되었다고 Xcode가 경고한다. WWDC2018 영상을 찾아보니 이것은 NSSecureCoding을 좀 더 권장하기 위한 결정이었다.

핵심은 archive할 때 어떤 클래스가 들어 있었는지 unarchive할 때 지정해 주라는 것.

그러나 하라는데로 했는데 DataNSArray로 바꾸는 것이 영 안 된다.

let array = [CLLocation(latitude: 0, longitude: 0)]
let data = try! NSKeyedArchiver.archivedData(withRootObject: array, requiringSecureCoding: true)

let unarchivedArray = try! NSKeyedUnarchiver.unarchivedObject(ofClass: NSArray.self, from: data)
// Fatal error

한참을 삽질하다가 애플 개발자 포럼의 포스트 하나를 보고 대오각성. CLLocation같은 object들이 들어있는 Collection (Array, Dictionary, Set)을 unarchive할 때는 unarchiveObject(ofClass:from:)이 아니라 unarchiveObejct(ofClasses:from:)에서 ofClasses에 모든 원소들의 클래스를 때려 넣어야 하는 것이었다.

let unarchivedArray = try! NSKeyedUnarchiver.unarchivedObject(ofClasses: [NSArray.self, CLLocation.self], from: data)

애플 개발자 포럼이 Stack overflow보다 도움이 된 흔치 않은 경험.

그런데, unarchiveTopLevelObjectWithData(_:) throws를 사용하면 클래스지정 없이도 멀쩡하게 unarchive된다. 그러나 쓰기 찜찜한 부분이… 알수없게도 iOS 12를 기준으로 NSKeyedArchiver.h 헤더를 보면 Objective-C에서는 +unarchiveTopLevelObjectWithData:error:가 deprecated 인데, Swift에서는 아니다? 오묘한 ObjC interoperability의 세계…


App Store 5.1 Policy Notification 해결하기

SyncScore는 2011년에 첫 출시한 클래식 음악 앱 시리즈이다. 이 유료앱들이 기적적으로 제법 팔려준 덕분에 진인웍스가 설립 초기에 홀딱 망하지 않고 버틸 수 있었다.

마지막 업데이트는 3년 전. 그런데 얼마 전 애플에서 메일이 하나 왔다(좋은 소식, 나쁜 소식, 관계없는 소식 가리지 않고 애플에서 메일이 오면 일단 앱 개발자의 심장은 살짝 덜컥 한다). 앱이 더 이상 앱 스토어 리뷰 가이드라인에 부합하지 않으니 확인하라고.

App Store Issue screenshot with 5.1 Policy Notification

마지막 업데이트가 너무 오래 전이라서 앱스토어 청소의 대상이 된 것이다. 일한다 앱 리뷰팀! 침착하게 3년 만에 SyncScore 프로젝트를 열어서, 업데이트 하는 김에 11인치, 12.9인치 iPad Pro 화면 지원까지 넣어서 리뷰 신청을 하고, 무사히 업데이트 되었다.

교훈: 앱이 돌아는 간다고 업데이트 안 하고 게으름 피우면, 정리 대상이 될 수 있다.


다시 유선으로

라즈베리 파이를 이용한 타임 캡슐을 한동안 돌렸으나, 여러 문제점이 있었다.

우선, 너무나 느리다. 라즈베리 파이에 랜선으로 연결해도 최대 초당 8메가바이트 정도의 몹시 느긋한 백업 속도를 보여준다. 그런데 아무것도 안 하고 있는 맥을 한 시간 마다 백업할 때도 타임머신은 100-200메가바이트 정도를 디스크에 쓴다. (무엇을 쓰는 지는 짐작이 가지 않는다.) 여기에 굼벵이 속도가 결합하면 맥이 돌아가는 거의 모든 시간 동안 타임 머신 백업이 진행중이라는 어쩐지 마음이 편치 않은 일이 벌어진다.

게다가 라즈베리 파이가 열심히 디스크를 쓰는 동안 만약 네트워크 연결이 불안정해지면 곧바로 백업 데이타에 오염이 생길 수 있다. 실제로 몇 번인가 백업에 사용되는 Sparse bundle이 맛이 가서 전체 백업을 처음부터 다시 시작해보니 이 장치가 미워지기 시작했다.

결국 긴 케이블을 하나 구입하여 타임머신 하드디스크를 멀리 두는 방법으로 돌아왔다. 재미는 없지만 빠르고 조용하다.


Hammerspoon으로 삽질하기

iOSDevWeekly를 통해서 Hammerspoon이라는 오픈소스 앱을 알게 되었습니다. 이름이 재미난 이 앱은 맥에서 항상 돌아가면서 여러 이벤트나 키보드 단축키에 반응하여 Lua 코드로 작성된 기능들을 실행합니다. 설치하면 멋진 아이콘과 함께 맥 자동화의 방대한 가능성이 열리고 너무나 할 수 있는 것이 많아서 어디서부터 시작해야 하나 막막해집니다. 그 때 생각난 것이

AirPods. iOS와는 잘 붙고 잘 떨어지는데 어째서인지 맥과의 연결이 매끄럽지 못합니다. Toothfairy라는 앱을 써보았지만 별로 도움이 되지 않습니다. 가장 신뢰성 있게 맥과 에어팓을 연결하는 방법은 메뉴바 > 블루투스 아이콘 > AirPods > connect인데, 이거 차라리 유선 이어폰을 꼽고 말지, 라는 생각이 들어서 괴로워집니다. 블루투스는 사람들에게 고통을 주기 위해서 악마가 만들어낸 표준이 아닐까요?

Hammerspoon과 BluetoothConnector로 괴로움을 조금 줄일 수 있습니다. 단축키 하나에 hs.execute()로 BluetoothConnector가 동작하도록 지정해 두는 것이죠.

casc = {"cmd", "alt", "shift", "ctrl"}
hs.hotkey.bind(casc, "A", function()
	hs.execute("swift <path to BluetoothConnector.swift> <AirPods Address>")
end)

casc라는 괴상한 키조합은 Ergodox키보드의 Hyper키를 이용합니다. 이 정도 시점에서 Keyboard Maestro 같은 것을 이용해서 클릭과 메뉴선택을 자동화하면 되는거 아냐? 라는 생각이 슬며시 들지만, 스위프트 스크립트를 돌리는 쪽이 어쩐지 멋지지요.


라즈베리 파이로 타임 캡슐

나의 백업 설정

최후의 방어선 온라인 백업은 Backblaze입니다. 만약 집에 불이 나거나 역대급 천재지변으로 많은 것이 사라져도 작업을 부활시킬 수 있다고 생각하면 1년에 50달러는 아깝지 않습니다. 주로 쓰는 맥은 생각날 때마다 사나흘에 한 번, Super Duper!로 클론을 만들어 둡니다.

디스크 클론이나 온라인 백업은 사실 마음의 평화를 위하는 측면이 더 크고, 실제로 가장 꺼내쓰기 좋은 백업 방식은 역시 Time Machine입니다. 한 시간 단위의 자동 백업 덕분에 절망의 구렁텅이에서 건져 올려진 경험이 다들 한 번 쯤 있을 것입니다.

하지만 무선 주변장치의 시대에 USB 하드디스크를 직접 연결해야 하는 Time Machine은 우아하지 못합니다. 무선으로 우아한 Time Capsule은 출시가 너무나 오래 전이라서 붙어있는 공유기의 성능이 의심스러운 가운데, 급기야 단종되었습니다 (잠시 묵념). 와이파이에 연결된 하드디스크를 하나 만들어서 타임 캡슐 대용으로 써먹을 수 있지 않을까, 연구를 좀 해보았습니다.

Pime Capsule

연구 결과는 라즈베리 파이에 3TB 하드디스크를 연결하여 타임 캡슐로 쓰는 것입니다. 불현듯 Pime Capsule이라는 이름이 떠올라서 흡족했는데 검색을 해보니 역시 누군가가 먼저 생각해낸 이름입니다. 그러니 이 이름은 쓰지 않습니다.

이런, 저런, 고대의 기록들을 보면서 진행하다보니 어마어마한 시행착오가 있었습니다. 가장 친절하고 최신인 포스팅은 How-To Geek의 Justin Pot이 썼습니다.

나의 재료

하나만 빼고 How-To Geek에서 시키는 대로 하면 됩니다. netatalk 디펜던시 설치 부분에서 libmysqlclient-dev는 찾을 수 없다고 나오니 이것만 슬쩍 빼고

sudo aptitude install build-essential libevent-dev libssl-dev libgcrypt11-dev libkrb5-dev libpam0g-dev libwrap0-dev libdb-dev libtdb-dev avahi-daemon libavahi-client-dev libacl1-dev libldap2-dev libcrack2-dev systemtap-sdt-dev libdbus-1-dev libdbus-glib-1-dev libglib2.0-dev libio-socket-inet6-perl tracker libtracker-sparql-1.0-dev libtracker-miner-1.0-dev

이렇게 설치하면 놀랍게도 별 문제 없이 진행됩니다. 뭔가 수상한 것이 가득 섞인 3대째 내려오는 육수 같은 느낌이 들지만 netatalk 설치에 대해서 공부하고 싶진 않으니 그냥 넘어갑니다.

라즈베리 파이가 재부팅 된 경우 간혹 외장하드가 읽기전용(ro)으로 마운트 되어 타임머신 백업이 작동하지 않는 경우가 있는데 이럴 때는

fsck.hfsplus -f /dev/sda2

검사를 한 번 해주고 umount 하고, 다시 마운트 해주면 읽기쓰기(rw)로 제대로 마운트됩니다. (jaysonlorenzen.wordpress.com)

백업 속도

DIY 타임 캡슐 만들기는 라즈베리 파이 3B 모델로 먼저 시작했습니다. 그러나 2.4GHz 와이파이로는 백업 속도가 너무 느려서 맥이 거의 항상 백업 중인 상태가 됩니다. 3B+에 랜선을 꽂아서 쓰면 2-3배 속도 향상이 있습니다. 만, 어라? 무선이 아니네? 음…

외장하드 불면증 해소

용건이 끝나면 바로 잠드는 맥에 직접 연결된 외장하드와 달리, 라즈베리 파이에 연결된 외장하드는 24시간 돌아갑니다. Caleb Wood가 제안한 hdparm은 도시바 하드에서 어쩐지 동작하지 않습니다. hd-idle이라는 프로그램도 시도해보았으나 소용 없습니다. 어지간히 잠자기 싫어하는 도시바 하드를 재우기 위해서 결국 스크립트를 하나 만듭니다.

sudo apt install hdparm sysstat python3

이것을 설치하면 iostat으로 외장하드의 읽기/쓰기 상태를 확인할 수 있습니다. 60초 주기로 iostat을 돌리고 읽기/쓰기 활동이 없으면 외장하드에 잠자기를 먹이는 파이썬 스크립트입니다.

import subprocess

def issue_standby():
    subprocess.call("sudo hdparm -y /dev/sda", shell=True)

process = subprocess.Popen("iostat -y 60 sda", shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
while process.poll() is None:
    outstring = process.stdout.readline()
    if "sda" in str(outstring):
        components = outstring.decode("utf-8").split()
        byteread = int(components[4])
        bytewritten = int(components[5])
        if byteread + bytewritten == 0:
            issue_standby()

/home/pi/hdsleep/hdsleep.py에 담고, crontab -e로 crontab에 아래 라인을 추가하여, 부팅하면 위 스크립트가 돌도록 해줍니다.

@reboot python3 /home/pi/hdsleep/hdsleep.py > /dev/null

교훈

장대한 삽질을 거치면서 어째서 똑똑한 사람들은 간편한 문제 해결을 위해서 돈을 아끼지 않는지, 혹은 애플은 어째서 타임 캡슐을 접었는지에 대하여 희미한 힌트를 얻을 수 있었습니다.



수정

2018-06-14 netatalk 버전:
힘들게 직접 컴파일해서 최신 버전의 netatalk를 돌리니 알 수 없는 이유로 몇 번의 백업 이후에 자꾸만 파일 시스템 오류가 나면서 Disk Utility 조차 디스크를 포기해 버립니다. apt install netatalk로 설치하는 오래된 버전을 사용하니 오히려 문제가 안 발생하는데 운 때문인지는 잘 모르겠습니다.


MoonBurst 업데이트

Vapor 2를 이용하는 MoonBurst를 만든 것이 얼마 안 된 것 같은데 Vapor 3.0이 릴리즈 되었습니다. 근본적으로 많이 바뀌어서 Controller를 완전히 새로 작성해야 했습니다. 최근에 나온 멋진 것을 쓰려면 어느정도의 양계질은 피할 수 없나봅니다.

Vapor 3에는 SwiftNIO? Future? 생소한 것들이 많이 도입되어 단순한 블로그 엔진을 업데이트하는 데에도 상당히 애를 먹었습니다. 굉장히 강력한 기능을 가진 다목적 웹 후레임웤을 그저 라우팅하고 HTTPResponse 만들어 돌려주는 데에 쓰고 있자니 핵폭탄으로 파리를 잡는 기분이 들고 뭐 나쁘지 않습니다.



WebtoonCutter 업데이트

macOS에서 긴 웹툰 JPEG 파일을 짧게 잘라서 나눠주는, 웹툰 편집자의 필수 앱! … WebtoonCutter가 0.3으로 업데이트 되었습니다.

3월에 공개한 WebtoonCutter 0.2가 어째서 웹툰 세계에서 돌풍을 일으키지 못하였나? 라는 고민을 하다가, 그래 앱 아이콘이 없어서 그럴거야! 라는 (아마도 잘못된) 결론을 얻었습니다.

iOS 앱 아이콘은 Sketch로 만듭니다만, macOS 아이콘은 어떻게 만들면 좋을까요. Bjango 존잘님의 포스트를 보니 아이콘을 만들 때 3D 렌더링을 하기도 하는군요. 전에 구체적인 목표 없이 Blender를 배우다가 포기한 생각이 나서 이번에는 확고한 목적을 가지고 다시 배워보았습니다.

Blender는 참으로 맥스럽지 않은 UI를 갖고 있어서 아주 기초적인 사용법에 익숙해지는 데도 상당히 오래 걸립니다. 운좋게도, 맨땅에 Blender Tutorial로 검색하여 Blender Guru라는 훌륭한 튜토리얼을 발견하였습니다. 우선, 그가 손쉽게 도너츠나 모루를 만드는 것을 침을 흘리면서 구경했습니다. 그다음, 내가 만들면 왜 어딘가 이상할까 약간의 좌절을 거치면서 튜토리얼을 따라해 봅니다. 무수한 양계질 끝에 구상했던 작두 모양 아이콘을 만들었습니다. 좀 더 잘 만들고 싶지만 이미 비이성적으로 많은 시간을 아이콘 제작에 써버렸습니다. 모자란 아이콘이라도 없는 것보다는 낫겠지요.

WebtoonCutter 다운로드


JSON Feed 발행 시작

피드를 발행하는 웹사이트가 좋다. 특히 비정기적으로 새로운 글이 올라오는 개인 블로그의 경우에는 피드를 구독하여 챙겨보는 것이 생각날 때마다 (보통 생각보다 자주…) 방문하는 것보다 편리하다. 이곳을 돌리는 블로그 엔진 MoonBurst에도 피드 발행 기능을 넣었다.

블로그 포스트들을 JSON Feed로 만드는 것은 Swift 4 부터 가능해진 Encodable 프로토콜을 이용하여 아름답게 해결 할 수 있었다. 이전에 진인웍스 블로그Atom 피드를 만들 때는 XML 템플릿에 문자 바꾸기 방식으로 했는데, 그때보다는 발전한 것 같아서 기분이 좋다.

Conditional GET을 어떻게 지원할까 잠시 고민하다가 발행할 때마다 feed.json을 파일로 만들어두면 골치아픈 부분은 Nginx가 알아서 한다는 상식적인 해결책이 떠올랐다.

그러니 각자 선호하는 피드 리더에 https://joyh.me/feed.json를 추가하면 되겠다. 나는 FeedWrangler를 쓴다.




모든 글 보기