크로스-플랫폼 컴퓨팅이 대세다. 당장 요즘 핫하다는 앱들이 어떤 기술 스택을 사용해서 개발되었는지를 살펴보면, 크로스-플랫폼이라는 트렌드의 위력을 실감할 수 있다. 가령, Notion이나 Slack, 그리고 Skype 같은 여러 업무 필수 앱들의 데스크탑 버전은 GitHub가 개발한 Electron이라는 프레임워크를 사용해 개발되어 있다. 그 유명한 게임용 메신저인 Discord도 Electron 기반이다. 폐쇄적인 독점 제국이었던 Microsoft의 변화를 상징하다시피 한 코드 에디터, Visual Studio Code도 Electron을 사용해 개발되었다 (애초에 이쪽은 Electron의 존재 이유 그 자체였던 Atom을 기반으로 개발되었으니 당연한 일이긴 하다). 이러한 굵직한 앱들 이외에도, Figma 같은 "힙한" 업무 툴들까지 Electron을 사용하고 있으며 Golem 같은 블록체인 업계의 앱들 또한 예외는 아니다. Electron을 사용해 개발된 앱들이 너무나도 많은 나머지, Electron이 지구를 정복했다는 말이 농담이 아닌 수준(...)까지 와버렸을 지경이다.

ElectronJS 로고.

사실 Electron이 지구를 정복했다는 말보다 Chromium, 즉 Google이 지구를 정복했다는 말이 더 정확한 표현이다. ElectronJS라는 프레임워크는 단순히 Chromium의 렌더링 엔진만을 가져다가, 본래 웹 스택으로 개발된 웹앱들을 네이티브 앱처럼 "보이도록" 한 번 감싸 주는 프레임워크이기 때문이다. 실제 Electron으로 작성된 앱들을 살펴보면, 앱별로 별도의 Chromium 인스턴스가 돌아가고 있다는 (...) 사실을 쉽게 확인할 수 있다. 정확히는 Node.js과 Chromium 인스턴스가 단일 V8 JavaScript 처리 인스턴스를 공유하는 등 아예 최적화의 노력이 없는 것은 아니지만, 앱 하나를 켤 때마다 전혀 필요도 없는 크롬의 요소들이 처음부터 새로 로드된다고 생각해 보면 어마어마한 에너지 낭비가 아닐 수 없다. 각 OS만이 가지고 있는 특유의 UX 흐름을 싸그리 무시해버리는 통에, 사용자 경험 측면에서도 지옥에 가까운 것은 덤이다.

이전 WASM 및 Ethereum 관련 글에서 사용했던 짤 재활용

물론 이런 식의 개발 방식은 모바일에서는 그리 적합한 접근이 아니다. 모바일 앱에서는 UX의 통일성을 유지하기 위해 Native API에 원활하게 접근하는 것이 중요하며 (예: UI 요소들, 카메라, 사진, 마이크 접근 등등), 앱별로 웹 브라우저 인스턴스를 별도로 띄워놓는 것은 제한된 전원을 가진 모바일 기기의 특성상 배터리에 악영향이 갈 수밖에 없기 때문이다. 이를 실제로 실행에 옮긴 경우도 적지 않으나, UX를 조금이라도 신경쓴다면 모바일에서 웹앱을 단순히 와핑해 "앱"이라고 제공하는 것은 얼마 전까지만 해도 그리 좋은 선택지가 아니었다.

...이를 여엿비 녀긴 Facebook께서 친히 React Native라는 솔루션을 내놓기 전까지는. (...) React Native는 너무 HTML5와 같은 웹 기술에만 의존한 나머지, 네이티브 앱의 UX적인 이점을 간과했다는 사실을 깨달은 Facebook이 보다 "네이티브 친화적인" 앱을 개발하기 위해 내놓은 솔루션이다. ReactJS에서 도입된 Virtual DOM을 과감히 없애버리고, 백그라운드에서 돌아가는 단일 프로세스를 통해 JavaScript를 구동함으로써 현재 화면에 표시된 State와 새롭게 렌더링된 State의 델타를 Native Bridge를 통해 60FPS로 뿌려준다는 발상은 그야말로 모바일 앱 개발의 혁명과도 같았다. (60FPS 이상으로 앱 애니메이션을 렌더링해줄 필요가 있는가?) 이러한 접근을 통해, JavaScript로 작성된 사실상 단 하나의 코드베이스로 Native OS API의 사용이 가능해졌으며, 네이티브 수준에서의 async 병렬화를 통해 웹 기술의 고질병이었던 성능 문제까지 해결해버렸기 때문이다. 2013년 4명이 이틀 만에 프로토타입을 개발해낸, Facebook의 사내 해커톤으로 나온 물건이지만 출시 이후 전세계 수많은 앱 개발자들의 고충을 덜어준 (하지만 동시에 수많은 네이티브 앱 개발자들의 일자리를 빼앗아간) React Native는 빠르게 개발 인력이 부족한 스타트업들의 필수품으로 자리잡았다.

Microsoft Office까지 일부 UI Element들에 대해서는 React Native를 사용하고 있다.

이와 같이 웹 기술과 크로스 플랫폼 트렌드가 네이티브 개발의 영역을 서서히 침범해오기 시작하면서, "네이티브는 끝났다"는 주장이 도는 것도 근거 없는 회의론이 아니게 되었다. 여기다 WebAssembly로 JS의 성능 페널티도 극복되고, Flutter 같은 Dart 기반의 (JavaScript-transpilable) 솔루션들까지 등장하는 판에 네이티브는 장기적으로 점점 설 자리를 잃게 되는 분위기이다. 어차피 데이터까지 대기업들의 서버에 다 맡겨놓는 판에, 서비스들까지 서버에서 불러오면 되지 뭐하러 네이티브 컴퓨팅이 필요하겠는가?

"크로스 네이티브"

대부분의 테크 기업들이 웹 기술과 호환되는 크로스 플랫폼 레이어를 만들어 로컬 디바이스와 웹 서비스의 경계를 없애고 있는 동안, 유독 애플만은 이와 정반대되는 전략을 취하고 있다. 애플이 제시하는 "네이티브" 크로스 플랫폼 전략의 중심에는 이들이 오랜 시간 동안 공들인 결과물, LLVM 컴파일러가 있다. 하나의 코드베이스를 사용하지만, 즉석으로 컴파일만 다시 하면 완전히 다른 플랫폼에서도 네이티브 앱을 구동할 수 있도록 하겠다는 것이다.

이것이 정확히 어떤 의미를 가지는지 조금 더 자세히 알아보자.

LLVM 컴파일러와 Bitcode

우선 LLVM 컴파일러가 정확히 무엇을 하는 물건인지부터 정의해둘 필요가 있다. 일반적으로, 현대적인 아키텍처의 컴파일러는 소스 코드를 머신 코드(기계어)로 빌드할 때 크게 세 가지의 단계를 거친다. 프런트 엔드, 미들 엔드, 그리고 백엔드가 그것이다. 간략하게 정리하자면, 프런트 엔드는 소스 코드의 문법을 검증하고 중간자 언어인 IR (Intermediary Representation) 로 변환하는 역할을 한다. 미들 엔드는 프런트 엔드에서 넘겨받은 IR에 대한 추가적인 성능 최적화를 수행하고, 백엔드는 미들 엔드가 최적화한 IR를 타깃 머신 코드로 변환해주는 역할을 한다. 이러한 3단계 설계는, 단일 컴파일러 인프라를 사용해서도 여러 종류의 프로그래밍 언어 (프런트 엔드) 와 여러 종류의 프로세서 아키텍처 (백엔드) 를 동시에 처리할 수 있도록 해 준다. 프런트 엔드와 백엔드 언어의 조합만큼 일일이 컴파일러를 따로 개발해줄 필요가 없다는 것이다.

GCC

POSIX 호환 시스템에서 가장 대중적으로 사용되는 GNU Compiler Collection (GCC) 의 경우, 위와 같은 표준적 설계를 따르나 컴파일러 아키텍처로서의 확장이 다소 제한되어 있다. 예를 들어, GCC 4.0 이전까지는 미들 엔드 단에서의 표준화된 AST(Abstract Syntax Tree)가 존재하지 않았으며 각 프런트 엔드 단에서 직접 트리 구현체를 제공하는 경우가 잦았다. 이로 인한 파편화 문제와 성능 최적화 문제를 해결하기 위해, GCC 4.0 이후 최신 버전의 GCC에서는 미들 엔드 단계에서 크게 세 가지의 IR를 사용한다. GCC 3.x의 Java 프런트 엔드에서 제공하는 IR를 기반으로 하는 GENERIC, 이를 단순화해 몇 가지 opcode constructs를 lower한 (즉,더욱 더 로우 엔드에서 존재하는 - 즉, 더 단순한 - instruction들의 조합들로 한 가지 instruction을 대체한) GENERIC의 subset IR인 GIMPLE, 그리고 미들 엔드가 최종적으로 반환하는 IR 형태인 Register Transfer Language (RTL)이 그것이다.

GCC에서의 C 계열 언어들과 (GCC 7에서 성능 문제로 지원이 중단된) Java 프런트 엔드의 경우, 프런트 엔드에서 직접 GENERIC을 IR로서 반환하며, 미들 엔드는 이를 RTL로 전개(expand)해 백 엔드로 넘긴다. (반면, GCC가 지원하는 다른 언어의 경우, 과거 자체 트리 및 IR 구현체를 제공했던 것의 흔적으로서 자체 내부 IR로 우선 코드를 빌드한 후 이것을 다시 GENERIC으로 빌드하는 경우가 존재한다.) 최종적으로는 미들 엔드에서 반환된 RTL을 백 엔드의 앞쪽에서 최종적인 프로세서 아키텍처에 맞추어 다시 조정하고, 이를 (실제 하드웨어 레지스터를 코드에 할당하는 등의 절차를 거친) strict RTL로 변환한 후 다시 최종 타깃 아키텍처의 ISA에 맞춘 머신 코드를 생성하게 된다.

컴파일러를 전공하신 분이라면 이미 잘 아는 사실이겠지만, 상위의 설명은 매우 개괄적인 overview에 불과하며 실제 컴파일러가 동작하는 방식은 (두꺼운 전공 서적 여러 권으로도 부족할 정도로도) 훨씬 더 복잡하다. 하지만, 이 정도 개괄적인 설명만으로도 GCC가 머신 코드를 직접적으로 타게팅하는 데에 초점이 맞추어진 아키텍처를 가지고 있으며 언어 간 자유로운 interoperability에는 "기본기에 충실한" 정도라는 것을 쉽게 파악할 수 있었을 것이다. 다시 말해, 다른 개발자가 프런트 엔드 또는 백엔드를 개발할 수는 있지만, 기본적인 아키텍처 최적화에 상당한 공을 들일 수밖에 없다. 이는 GCC가 본래 1987년, 리처드 스톨만이 이끌던 GNU 운영 체제 프로젝트의 일부로서 출시된 오래된 소프트웨어라는 점과 무관치 않다. GCC가 "현대적인" 컴파일러 구조를 갖추기 시작한 것은 미들 엔드에 단일 표준 IR이 도입된 GCC 4.0에 와서면서부터이며, 점진적으로 LLVM과 경쟁하며 훨씬 더 현대적인 아키텍처를 갖추게 되었다.

LLVM

그렇다면, LLVM은 대체 무엇이 다른 것인가? 먼저 LLVM은 단일의 컴파일러를 칭하는 데에 사용되는 명칭이 아니다. LLVM은 컴파일러를 구성하는 데에 필요한 요소들을 모듈러한 인프라로 제공함으로써, 컴파일러 구성에 들어가는 노력을 크게 줄이는 것을 목표로 하는 컴파일러 인프라 프로젝트를 총괄하는 브랜드이다. LLVM은 그야말로 IR부터 통상적인 컴파일러, JIT 컴파일러, 디버거 그리고 C-언어 호환 라이브러리 등등 컴파일에 필요한 사실상 모든 것을 하나의 패키지로 제공하고 있다.

컴파일러 자체만 놓고 본다면, LLVM의 컴파일러는 그리 특별하지는 않다. 오히려 POSIX 계열 운영 체제에서는 같은 C 소스 코드를 빌드했을 때 (GCC vs Clang/LLVM) 벤치마크상 평균 1-4% 정도 GCC보다 성능이 떨어진다. 성능 최적화를 어느 정도 포기하는 대신 LLVM이 얻는 것은 극도의 유연함이다. 컴파일러 자체를 직접 관리하는 것이 아니라, 컴파일러 인프라 전체를 구성하는 하나의 표준, 즉 프레임워크를 구축해놓고 이를 이용하는 생태계를 구축해낸 것이다.

앞서 GCC는 내부적으로 GENERIC, GIMPLE, 그리고 RTL이라는 크게 세 가지의 IR을 사용한다고 언급한 바 있다. 구조적으로는 모듈러하다 하더라도, 프런트 엔드, 미들 엔드, 그리고 백 엔드가 실질적으로 하나의 요소로서 붙어있어 (프런트 엔드에서 새로운 언어를 지원하거나 백 엔드에서 새로운 ISA를 지원하는 등의 사유로) 이 중 하나의 요소를 새롭게 개발한다고 한다면 이는 GCC의 소스 코드 일부에 기여하는 것이지 그 자체로는 새로운 소프트웨어가 아니다. LLVM은 다르다. LLVM에서는 LLVM IR이라는 단 하나의 IR 포맷만 사용한다. 이뿐만이 아니라, LLVM 프레임워크를 따르는 컴파일러 요소들은 모두 독립된 소프트웨어로서 존재할 수 있다. 어느 하나의 프로젝트에 종속되는 것이 아니라는 것이다.

LLVM에도 프런트 엔드와 백 엔드가 존재한다. 하지만 GCC와는 달리 이 둘은 서로 완전히 독립될 수 있는 소프트웨어이다. 이들 사이의 유일한 공통점은 단 하나, LLVM IR이다. 프런트 엔드와 백 엔드의 개발자가 서로 직접적으로 협업하지 않더라도, LLVM IR이라는 표준(프로토콜)만 따라준다면 하나의 컴파일러 구조가 뚝딱 완성된다. 가령, LLVM에서 사용되는 C 계열 언어 프런트엔드인 Clang은 C/C++/Objective-C 등의 언어 입력을 받아 "순수하게" LLVM IR만을 반환하는 소프트웨어이다 (본래 LLVM은 GCC의 프런트 엔드를 사용할 예정이었으나 (아직까지 존재하는 llvm-gcc), GCC 코드의 어마어마한 레거시와 Objective-C를 제대로 지원해주지 않으면서 라이선스를 이유로 Objective-C 관련 코드를 모두 내놓으라고 요구하는 등 여러 가지 어른들의 사정이 겹치자 (...) 애플은 LLVM의 창시자인 (그리고 Swift의 아버지이기도 한) 크리스 라트너를 고용해 LLVM을 위한 Objective-C 프런트엔드를 새롭게 개발하기에 이르는데, 이것이 바로 Clang이다. 여담이지만, 이 때문에 LLVM은 현재까지도 GCC용으로 개발된 프런트 엔드와도 완전히 호환된다).

이것과는 완전히 별개로, 프런트 엔드에서 반환된 LLVM IR만 가지고 있다면 LLVM 프로젝트에서 직접적으로 지원하지 않더라도 서드 파티 백엔드를 통해 손쉽게 타깃 머신코드를 얻어낼 수도 있다. 이게 무슨 말이냐 하면, Clang을 통해 C 코드를 LLVM IR로 빌드하기만 해도 LLVM 프로젝트에서 공식적으로 지원하는 x86이나 ARM 등 아키텍처뿐만 아니라, 인터프리터 모드로 LLVM IR을 직접 실행할 수도 있고, WebAssembly나 (무려!) JVM 같은 가상 머신 타깃의 머신 코드를 생성할 수 있다는 것이다.

이 중 LLVM IR을 받아 WebAssembly로 빌드해주는 서드 파티 백엔드인 Emscripten은, LLVM IR로 빌드될 수 있는 어떤 코드든지 웹 브라우저에서 돌릴 수 있는 코드로 빌드할 수 있다는 사실 때문에 해커들의 온갖 미친 짓(...)에 현재까지 활용되고 있다. 예컨대, Windows 95는 주로 C로 작성되어 있다. 그렇다면, 웹 브라우저에서 통째로 Windows 95를 돌리거나, 한 술 더 떠서 Windows 95를 Electron 앱으로 패키징(!)까지 할 수 있는 것이다. (크롬 위에 진짜 운영체제까지 돌리는 세상이 왔다) 이론상으로는 WASM 말고 JVM이라고 못할 것도 없으나, 자바 가상머신 위에 Windows 95를 통째로 돌리는 (사탄도 경악할) 짓을 생각한 자는 다행히도 아직까지는 없는 듯하다.

올해 2월에는 이더리움 클래식 랩스에서 이더리움의 EVM을 타깃으로 하는 LLVM 백엔드를 내놓기도 했다. 이론상으로 LLVM 프런트 엔드를 가지고 있는 모든 언어를 이더리움 스마트 컨트랙트 개발에 사용할 수 있게 되었지만, 블록체인을 literal하게 가스로 테러하려고 하는 게 아닌 한 제발 이더리움 위에서 Windows 95를 구동하려고 시도하는 자는 없기를 바란다.

Electron 앱으로 패키징된 Windows 95가 macOS 위에서 구동되는 모습. 말세다 말세...

그렇다면 LLVM은 엔지니어들의 장난 말고 또 어디에 사용될 수 있는가? 바로 처음에 언급했던 바로 그 주제, 크로스-플랫폼 컴퓨팅이다. 정확히는 네이티브 바이너리만으로도 크로스 플랫폼 컴퓨팅을 구현할 수 있게 되는 것이다. 가상 머신이나 웹 브라우저 스택을 전혀 거치지 않고도.

이는 애플이 Bitcode라고 부르는 새로운 컴파일러 기능 때문에 가능해졌다. 겉으로는 잘 드러나지 않지만, 2015년 이후 iOS 등 애플 플랫폼에서 구동되는 앱을 개발할 경우 Bitcode가 기본값으로 활성화되며 Bitcode 파일들이 실제 바이너리와 같이 App Store에 제출된다. watchOS와 tvOS의 경우에는 이 Bitcode 옵션을 강제로 활성화해야만 App Store에 앱을 제출할 수 있다. 굳이 왜 이런 정책을 강제하는 것일까?

LLVM bitcode와 Apple Bitcode

Bitcode는 기본적으로, LLVM의 bitcode를 이용한 일종의 애플 전용 IR 기능이다. 소문자 b로 시작하는 bitcode는 LLVM IR을 컴파일러 친화적인 방식으로 인코딩하는 표준을 뜻한다. 대문자 B로 시작하는 Bitcode는, 최종 컴파일된 앱 바이너리에 해당 LLVM bitcode를 포함하여 App Store에 제출할 수 있는 방법을 포괄하는 기능들이다.

bitcode는 기본적으로 IR이기 때문에, 소스 코드 자체보다는 덜하지만 최종 컴파일된 바이너리보다는 훨씬 유연하다. 다시 말해, bitcode를 앱과 같이 제출할 경우 애플 서버에서 해당 bitcode를 재빌드해 여러 가지 최적화를 적용할 수 있다는 뜻이다. 예컨대, Apple Watch Series 4부터는 64비트 프로세서(ARMv8)가 적용된다는 사실을 눈치챈 사람이 있는가? 본래 64비트 바이너리를 얻기 위해서는 재컴파일이 필요하지만, 희한하게도 Series 4에서는 출시 당일부터 모든 앱들이 바로 64비트 바이너리로 구동되었다. 이는 watchOS에서 그 이전에 App Store 배포 시 Bitcode 사용이 의무화되었고, 따라서 해당 Bitcode을 App Store 서버에서 (LLVM을 이용해) 자동으로 ARMv8 타깃으로 재빌드해 배포해줄 수 있었기 때문이다. 굳이 32비트, 64비트 바이너리를 모두 받을 필요도 없다. Series 3 이하 사용자에게는 자동으로 32비트 버전만이, Series 4 이상 사용자에게는 64비트 버전만이 보여지고 다운로드되기 때문에 앱 다운로드 사이즈도 크게 줄일 수 있다. 사용자가 전혀 눈치채지 못하는데도 단 하루 만에 조용히 아키텍처 전환이 이루어진 것이다.

마찬가지로, iPhone XS와 XR 시리즈에 탑재되는 A12 Bionic부터 SoC의 ISA가 ARMv8.3-A(arm64e)로 업그레이드되었다는 사실 또한 아무도 눈치채지 못했다. Bitcode를 사용한 iOS 앱들은 아무도 보지 않는 사이 애플 서버에서 자동으로 ARMv8.3-A의 개선사항들을 적용하도록 재빌드되어 출시 첫날 새로운 아이폰을 구매한 사용자에게 제공되었다. ARMv8.3-A에서는 기존 아키텍처 리비전에 비해서 메모리 상 포인터 접근 권한에 관련한 새로운 필수 instruction이 추가되었기 때문에 보안상 큰 이점이 있다. 앱을 재빌드하거나 재배포하지 않고도 자동으로 해당 하드웨어 변경사항이 적용되었다는 것은 실로 어마어마한 일이 아닐 수 없다. 앞선 watchOS와 마찬가지로, 이전 버전의 아이폰을 사용하는 소비자에게는 그 SoC에 맞는 바이너리가 자동으로 제공되었기 때문에 그 누구도 전혀 신경쓸 필요가 없다.

그렇다면, ARM 아키텍처 간 이동이 자동으로 가능해졌으니 혹시 ARM과 x86 아키텍처 간 이동, 즉 iOS나 iPadOS에서 macOS로 자동으로 이동하는 것도 가능할까? 애플이 공식적으로 제공하거나 권장하는 사항은 아니지만 충분히 가능하다. Bitcode는 그저 LLVM IR의 브랜딩일 뿐이며, 앞서 언급했듯 LLVM IR만 있으면 사실상 LLVM이 지원하는 모든 플랫폼 타깃으로 빌드가 가능하기 때문이다.

iOS와 macOS 앱은 사용하는 UI 프레임워크가 다르다는 문제가 남아있지만, 이는 Catalyst(구 Project Marzipan)를 통해 해결된다. UIKit을 그대로 macOS에서 사용할 수 있기 때문이다. 그렇다면 남은 것은 iOS 앱에서 Bitcode를 추출해 x86으로 재빌드하는 것밖에 없다. 재컴파일 없이 네이티브로 iOS 앱을 macOS에서 구동할 수 있게 되는 것이다.

한 가지 밝히고 넘어가야 할 사실은, 이 방식은 애플이 오리지널 iPhone SDK 시절부터 써왔던 방식이라는 것이다. iPhone SDK는 처음부터 LLVM 컴파일러를 사용했는데, 이 덕분에 iPhone Simulator를 사용하는 경우에는 어떠한 프로세서 시뮬레이션도 거치지 않았다. 곧바로 x86용으로 빌드된 SpringBoard (iOS UI)와 UIKit만 로드해놓은 후, 그 위에서 x86용으로 빌드된 iOS (iPhone OS) 앱을 구동하면 되었다. 실제 바이너리가 ARM 타깃으로 빌드되는 경우는 오직 실물 iPhone을 타깃으로 앱을 빌드하는 경우였다. 아이폰 프로토타입들의 내부 소프트웨어 개발 및 시연도 대부분 Mac에서 구동되는 x86 바이너리로 진행되었으니 말 다 했다.

의심된다면 직접 (간단하게 UI 없이) 해보자

  1. Xcode에서 Objective-C로 된 Hello World iOS 앱을 하나 생성한다. 단순히 iOS 바이너리가 Mac에서 구동되는 것을 보기 위한 목적이라면 NSLog(@"Hello iOS App on Intel!"); 한 줄이면 충분하다.
왜 Swift 말고 Objective-C냐고요? 현재 버전의 Clang과 iOS, macOS 소프트웨어는 Garbage Collection 대신 Automatic Reference Counting (ARC)을 사용합니다. 앞서 언급한 LLVM의 "네이티브" 패러다임을 그대로 따라가는 것으로, 가상 머신이나 외부 프로세스가 상시 구동되면서 메모리를 대신 정리해주는 것이 아니라 아예 네이티브 코드상으로 남는 메모리가 없도록 하겠다는 겁니다. 다만, ARC를 사용할 경우 LLVM bitcode만으로 아직 처리될 수 없는 부분들이 있어 일부 inline assembly를 사용하는데, 이것이 ARM 전용이기 때문에 해당 bitcode를 x86 타깃으로 빌드할 수 없게 됩니다. 따라서 빌드 이전에 ARC를 반드시 해제해주셔야 하며, Swift의 경우 설계상으로 ARC를 강제하므로 이를 피해갈 수 있는 방법이 없기 때문에 이 실험에 사용할 수 없습니다. 아직 애플이 Bitcode를 통한 x86 자동 재빌드를 지원하지 않는 유력한 사유 중 하나로 보입니다.

2. -fembed-bitcode 플래그를 사용하거나, BITCODE_GENERATION_MODE = bitcode빌드 옵션을 사용해 App Store용 바이너리 생성이 아닌 상황에서도 강제로 Bitcode가 생성되도록 한다.

3. 보통 하던 것처럼 iOS 앱을 빌드해준다. 타깃 디바이스가 A12 이상 SoC를 탑재(arm64e)한 경우, 위의 옵션을 활성화한 상태에서도 아래에서 지정하는 arm64 아키텍처용 바이너리를 생성하지 않기 때문에 Xcode 빌드 옵션에서 'Compile for active architectures only' 옵션을 해제해주어야 한다.

4. Mach-O Universal Binary에서 Bitcode를 추출해주는 ebcutil이라는 라이브러리가 있다. 이를 사용해, 다음의 명령어를 실행해준다.

ebcutil -a arm64 -e (앱 바이너리 경로)/(앱 이름).app/(앱 이름)

5. 추출된 각각의 Bitcode 오브젝트들을 모두 x86 바이너리로 강제 재빌드한다. x86 타깃의 바이너리를 생성할 것이기 때문에, 본래 iOS Simulator에서 구동되는 x86 바이너리를 빌드해 주는 iPhoneSimulator.sdk 를 대신 사용함에 유의하자.

for object in *;
    do clang -arch x86_64 -c -Xclang -disable-llvm-passes -emit-llvm -x ir -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk $object -o $object.o;
done

6. 빌드된 x86 머신 코드를 최종 바이너리로 링킹(linking)해주자.

clang -arch x86_64 -mios-version-min=13.0 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk *.o -o (앱 바이너리 경로)/(앱 이름).app/(앱 이름)

7. 바이너리가 생성되었으니 실행해주자. NSLog 한 줄을 넣었다면 터미널에 Hello iOS App on Intel! 이 타임스탬프와 함께 나타날 것이다.

cd (앱 바이너리 경로)/(앱 이름).app
./(앱 이름)

결론: Central이냐 Edge냐, Web이냐 Local이냐

앞서 서술했다시피, 애플이 크로스-플랫폼 시대에 대응하는 방식은 결국 컴파일러 매직을 통한 네이티브 컴퓨팅이다. 가상 머신 없이도 가상 머신과 동일한 효과를 내는 인프라를 이미 오래전부터 구축해 왔던 터라, 이 방식대로라면 아키텍처 및 플랫폼 이질성 문제에서 거의 완전히 해방되었다고 보아도 무방하다.

반면, 구글 등 다른 기업들은 웹 및 브라우저 기반 기술 - 특히나 JavaScript와 WebAssembly - 을 기반으로 동일한 문제를 해결하려 하고 있다. WASM이나 React Native 같은 신기술들 덕분에 웹 기반 기술의 오버헤드는 사실상 체감 불가능할 정도 수준까지 떨어졌지만, 웹은 여전히 치명적인 단점을 하나 안고 있다. 바로 컴퓨테이션 프라이버시를 보장하기 힘들다는 것이다.

물론 대부분의 소비자는 프라이버시 따윈 잘 신경쓰지 않는 듯하다. iOS 13에서 사실상 백그라운드 위치추적이 불가능하게 만들어놓으면 뭐하나, Z세대의 요구사항은 결국 Zenly 같이 거의 항상 서로 연결되게 해 주는 (그러면서 데이터는 잔뜩 빨아가는) 서비스인데. 프라이버시 지키려다가 버그 대잔치로 올해 OS 농사 다 망치게 생긴 애플 입장에서는 상당히 곤란한 요구사항이 아닐 수 없다.

하지만, 최소한 앱은 그 UX를 지키면서 최대한 프라이버시를 유지하려는 시도라도 해볼 수 있다. 현재 형태의 웹은 본질적으로 중앙 서버에 붙어있기 때문에 프라이버시고 뭐고 없다. 개인적으로 엣지컴퓨팅이나 MPC를 논하기 이전에, 이미 있는 컴파일러 기술을 어떻게 활용할지부터 먼저 고민하는 것이 낫겠다고 생각하는 이유는 바로 여기에 있다.

웹 기술을 싫어하는 것은 아니지만, 네이티브의 영역까지 웹 기술이 완전히 점령해 버린다면 그 때는 각 플랫폼의 개성이나 프라이버시 같은 건 완전히 사라져 버릴 것이라고 생각한다. 엣지 컴퓨팅을 논하려면 먼저 네이티브 플랫폼을 어떻게 살려낼 것인지부터 먼저 논의해야 한다.

단일 State와 하나의 로직을 실행하면서 - 즉, 각 엣지 기기의 컴퓨테이션 포텐셜을 최대한 뽑아내면서 - 웹이나 가상 머신의 오버헤드에서 벗어날 방법은 사실상 컴파일러 매직 말고는 없다. 오버헤드 제거에 따른 최적화의 문제도 크지만, 컴퓨테이션 주권의 문제에서 주도권을 빼앗기지 않으려면 "내가 하는 작업은 내 기기에서 실행되는 것"이 우선적인 원칙이 되어야 하지 않을까 싶다.