지난 2년 동안 Goodnotes 엔지니어링팀은 성공적인 iPad 메모 작성 앱을 다른 플랫폼에 도입하기 위한 프로젝트를 진행해 왔습니다. 이 우수사례에서는 2022년 올해의 iPad 앱이 웹 기술을 기반으로 하는 웹, ChromeOS, Android, Windows와 WebAssembly가 팀에서 10년 이상 작업해 온 동일한 Swift 코드를 재사용한 방법을 다룹니다.
Goodnotes가 웹, Android, Windows에 도입된 이유
2021년에는 Goodnotes를 iOS 및 iPad용 앱으로만 사용할 수 있었습니다. Goodnotes의 엔지니어링팀은 새로운 버전의 Goodnotes를 개발하면서 추가 운영체제 및 플랫폼을 개발해야 하는 엄청난 기술적 도전과제를 수용했습니다. 제품은 iOS 애플리케이션과 완벽하게 호환되어야 하며 iOS 애플리케이션과 동일한 참고사항을 렌더링해야 합니다. PDF 위에 작성된 메모 또는 첨부된 이미지는 동등한 것이어야 하며 iOS 앱에 표시되는 것과 동일한 획을 보여주어야 합니다. 추가된 모든 획은 사용자가 사용 중인 도구(예: 펜, 형광펜, 만년필, 도형, 지우개)와 관계없이 iOS 사용자가 만들 수 있는 것과 동일해야 합니다.
요구사항과 엔지니어링팀의 경험을 바탕으로 팀은 Swift 코드베이스가 이미 수년 동안 잘 작성되고 철저한 테스트를 거쳤다는 점을 고려할 때 Swift 코드베이스를 재사용하는 것이 최선의 조치일 것이라고 빠르게 결론을 내렸습니다. 하지만 기존 iOS/iPad 애플리케이션을 Flutter 또는 Compose 멀티플랫폼과 같은 다른 플랫폼이나 기술로 포팅하면 안 되나요? 새로운 플랫폼으로 이동하려면 Goodnotes를 다시 작성해야 합니다. 이렇게 하면 이미 구현된 iOS 애플리케이션과 새 애플리케이션 없이 빌드할 대상 간에 개발 경합이 시작되거나 새 코드베이스가 따라오는 동안 기존 애플리케이션에서 새 개발을 중지할 수 있습니다. Goodnotes에서 Swift 코드를 재사용할 수 있다면 크로스 플랫폼팀이 앱 기초 작업을 하는 동안 iOS팀이 구현한 새로운 기능의 이점을 누리고 기능 패리티에 도달할 수 있습니다.
이 제품은 다음과 같은 기능을 추가하기 위한 iOS의 여러 흥미로운 과제를 이미 해결했습니다.
- 메모 렌더링
- 문서 및 메모 동기화
- 충돌 없는 복제 데이터 유형을 사용하여 메모의 충돌 해결
- AI 모델 평가를 위한 데이터 분석
- 콘텐츠 검색 및 문서 색인 생성
- 맞춤 스크롤 환경 및 애니메이션
- 모든 UI 레이어의 모델 구현을 확인합니다.
엔지니어링팀이 이미 iOS 및 iPad 애플리케이션에서 작동하는 iOS 코드베이스를 가져와서 Goodnotes가 Windows, Android 또는 웹 애플리케이션으로 제공할 수 있는 프로젝트의 일부로 실행할 수 있다면 이 모든 기능을 다른 플랫폼에도 훨씬 쉽게 구현할 수 있습니다.
Goodnotes의 기술 스택
다행히 웹에서 기존 Swift 코드를 재사용하는 방법인 WebAssembly (Wasm)가 있었습니다. Goodnotes는 오픈소스 및 커뮤니티에서 유지관리하는 프로젝트 SwiftWasm과 함께 Wasm을 사용하여 프로토타입을 빌드했습니다. SwiftWasm을 통해 Goodnotes팀은 이미 구현된 모든 Swift 코드를 사용하여 Wasm 바이너리를 생성할 수 있었습니다. 이 바이너리는 Android, Windows, ChromeOS 및 기타 모든 운영체제용 프로그레시브 웹 애플리케이션으로 제공되는 웹페이지에 포함될 수 있습니다.
Goodnotes를 PWA로 출시하고 모든 플랫폼의 스토어에 이를 등록할 수 있도록 하는 것이 목표였습니다. 이 프로젝트에서는 Swift 외에도 이미 iOS용으로 사용되는 프로그래밍 언어, 웹에서 Swift 코드를 실행하는 데 사용되는 WebAssembly 외에 다음과 같은 기술을 사용했습니다.
- TypeScript: 웹 기술에 가장 자주 사용되는 프로그래밍 언어입니다.
- React 및 webpack: 웹에 가장 많이 사용되는 프레임워크 및 번들러입니다.
- PWA 및 서비스 워커: 팀이 앱을 다른 iOS 앱처럼 작동하는 오프라인 애플리케이션으로 제공하고 개발자가 스토어 또는 브라우저 자체에서 앱을 설치할 수 있으므로 이 프로젝트를 위한 대규모 인에이블러입니다.
- PWABuilder: Goodnotes에서 사용하는 기본 프로젝트로, 팀이 Microsoft Store에서 앱을 배포할 수 있도록 PWA를 네이티브 Windows 바이너리로 래핑합니다.
- 신뢰할 수 있는 웹 활동: 회사에서 PWA를 내부적으로 네이티브 애플리케이션으로 배포하는 데 사용하는 가장 중요한 Android 기술입니다.
다음 그림은 기본 TypeScript 및 React를 사용하여 구현된 것과 SwiftWasm 및 vanilla JavaScript, Swift, WebAssembly를 사용하여 구현된 방법을 보여줍니다. 이 프로젝트 부분에서는 Swift 및 WebAssembly용 JavaScript 상호 운용성 라이브러리인 JSKit를 사용하여 필요한 경우 Swift 코드의 편집기 화면에서 DOM을 처리하거나 일부 브라우저별 API를 사용합니다.
Wasm과 웹을 사용하는 이유
Wasm은 Apple에서 공식적으로 지원하지 않지만 Goodnotes 엔지니어링팀은 이 접근 방식이 최선의 결정이라고 생각한 이유를 다음과 같습니다.
- 10만 줄 이상의 코드를 재사용합니다.
- 핵심 제품 개발을 계속하면서 크로스 플랫폼 앱에 기여할 수 있는 기능
- 반복적인 개발 프로세스를 사용하여 모든 플랫폼에 최대한 빨리 접근할 수 있습니다.
- 모든 비즈니스 로직을 복제하지 않고 동일한 문서를 렌더링할 수 있는 제어권을 보유하며 구현에 차이를 발생시킵니다.
- 모든 플랫폼에서 동시에 이루어지는 모든 성능 개선사항 (및 모든 플랫폼에 구현된 모든 버그 수정)의 이점을 누릴 수 있습니다.
10만 줄 이상의 코드와 렌더링 파이프라인을 구현하는 비즈니스 로직을 재사용해야 했습니다. 동시에 Swift 코드가 다른 도구 모음과 호환되도록 만들면 필요한 경우 향후 다른 플랫폼에서 이 코드를 재사용할 수 있습니다.
반복적인 제품 개발
팀은 가능한 한 빨리 사용자에게 무언가를 제공하기 위해 반복적인 접근 방식을 취했습니다. Goodnotes는 사용자가 모든 공유 문서를 가져와 모든 플랫폼에서 읽을 수 있는 제품의 읽기 전용 버전으로 시작되었습니다. 링크만 있으면 iPad에서 작성한 메모에 액세스하고 읽을 수 있습니다. 다음 단계는 크로스 플랫폼 버전을 iOS 버전과 동일하게 만들기 위해 편집 기능에 추가되었습니다.
읽기 전용 제품의 첫 번째 버전을 개발하는 데 6개월이 걸렸으며, 이후 9개월 동안은 내가 만든 모든 문서나 다른 사용자가 나와 공유한 모든 문서를 확인할 수 있는 UI 화면과 처음으로 다양한 편집 기능을 개발하는 데 사용되었습니다. 또한 SwiftWasm 툴체인 덕분에 iOS 플랫폼의 새로운 기능을 크로스 플랫폼 프로젝트로 쉽게 포팅할 수 있었습니다. 예를 들어 새로운 유형의 펜을 만들고 수천 줄의 코드를 재사용하여 크로스 플랫폼에서 쉽게 구현할 수 있습니다.
이 프로젝트를 구축하는 것은 놀라운 경험이었습니다. Goodnotes는 이를 통해 많은 것을 배웠습니다. 따라서 다음 섹션에서는 웹 개발과 WebAssembly 및 Swift와 같은 언어의 사용과 관련된 흥미로운 기술적 요점을 중점적으로 다룹니다.
초기의 장애물
이 프로젝트를 수행하는 것은 여러 관점에서 매우 어려웠습니다. 연구팀이 발견한 첫 번째 장애물은 SwiftWasm 툴체인과 관련이 있었습니다. 이 도구 모음은 팀에 큰 도움이 되었지만 모든 iOS 코드가 Wasm과 호환되지는 않았습니다. 예를 들어 뷰, API 클라이언트 또는 데이터베이스 액세스 구현과 같은 IO 또는 UI 관련 코드는 재사용할 수 없으므로 팀은 크로스 플랫폼 솔루션에서 재사용할 수 있도록 앱의 특정 부분을 리팩터링해야 했습니다. 팀에서 만든 대부분의 PR은 종속 항목 추상화로 리팩터링되었으므로 팀에서 나중에 종속 항목 삽입 또는 기타 유사한 전략을 사용하여 종속 항목을 대체할 수 있습니다. iOS 코드는 원래 Wasm에서 구현할 수 있는 원시 비즈니스 로직을 입력/출력 및 사용자 인터페이스를 담당하는 코드와 혼합했으며 Wasm도 지원하지 않기 때문에 Wasm에서 구현할 수 없었습니다. 따라서 Swift 비즈니스 로직을 플랫폼 간에 재사용할 준비가 되면 TypeScript로 IO 및 UI 코드를 다시 구현해야 했습니다.
성능 문제 해결됨
Goodnotes는 편집기 작업을 시작하자 편집 환경에 몇 가지 문제가 있음을 확인했으며, 어려운 기술 제약조건이 로드맵에 포함되었습니다. 첫 번째 문제는 성능과 관련이 있었습니다. JavaScript는 단일 스레드 언어입니다. 즉, 호출 스택 하나와 메모리 힙 하나가 있습니다. 코드는 순서대로 실행되며 다음 코드 실행으로 넘어가기 전에 코드 실행을 완료해야 합니다. 동기식이지만 때로는 유해할 수도 있습니다. 예를 들어 함수가 실행되는 데 시간이 오래 걸리거나 특정 작업을 기다려야 하는 경우 그동안 모든 함수를 정지합니다. 바로 엔지니어가 해결해야 했던 문제입니다. 렌더링 레이어나 기타 복잡한 알고리즘과 관련된 코드베이스의 일부 특정 경로를 평가하는 것은 팀에 문제가 되었습니다. 이러한 알고리즘은 동기식이고 실행 시 기본 스레드가 차단되었기 때문입니다. Goodnotes팀은 속도를 높이기 위해 문서를 다시 작성하고 그중 일부를 비동기식으로 만들었습니다. 또한 앱에서 알고리즘 실행을 중지하고 나중에 계속할 수 있도록 수익 전략도 도입했습니다. 이를 통해 브라우저가 UI를 업데이트하고 프레임 누락을 방지할 수 있습니다. iOS 애플리케이션은 기본 iOS 스레드가 사용자 인터페이스를 업데이트하는 동안 백그라운드에서 스레드를 사용하고 이러한 알고리즘을 평가할 수 있으므로 문제가 되지 않았습니다.
엔지니어링팀에서 해결해야 했던 또 다른 솔루션은 DOM에 연결된 HTML 요소를 기반으로 하는 UI를 전체 화면 캔버스 기반의 문서 UI로 이전하는 것이었습니다. 프로젝트에서는 다른 웹페이지와 마찬가지로 HTML 요소를 사용하여 DOM 구조의 일부로 문서와 관련된 모든 메모와 콘텐츠를 표시하기 시작했지만, 어느 시점에는 브라우저에서 DOM 업데이트 작업을 하는 시간을 줄여 저사양 기기의 성능을 개선하기 위해 전체 화면 캔버스로 이전했습니다.
엔지니어링팀은 프로젝트 초기에 이러한 작업을 수행했다면 발생한 문제의 일부를 줄일 수 있었다고 판단한 다음과 같은 변경사항은 다음과 같습니다.
- 과도한 알고리즘에 대해 웹 워커를 자주 사용하여 기본 스레드를 더 많이 오프로드합니다.
- Wasm 컨텍스트를 벗어나는 경우 성능에 미치는 영향을 줄일 수 있도록 처음부터 JS-Swift 상호 운용성 라이브러리 대신 내보낸 함수와 가져온 함수를 사용합니다. 이 JavaScript 상호 운용성 라이브러리는 DOM 또는 브라우저에 액세스하는 데 유용하지만 기본 Wasm 내보내기 함수보다 느립니다.
- 코드가 내부에서
OffscreenCanvas
사용을 허용하는지 확인합니다. 그래야 앱이 기본 스레드를 오프로드하고 Canvas API의 모든 사용을 웹 작업자로 이동하여 메모를 작성할 때 애플리케이션 성능을 극대화할 수 있습니다. - 앱이 기본 스레드 워크로드를 줄일 수 있도록 모든 Wasm 관련 실행을 웹 작업자 또는 웹 작업자 풀로 이동합니다.
텍스트 편집기
또 다른 흥미로운 문제는 하나의 특정 도구인 텍스트 편집기와 관련이 있었습니다.
이 도구의 iOS 구현은 내부적으로 RTF를 사용하는 작은 도구 모음인 NSAttributedString
를 기반으로 합니다. 그러나 이 구현은 SwiftWasm과 호환되지 않으므로 크로스 플랫폼팀은 먼저 RTF 문법을 기반으로 맞춤 파서를 만들고 나중에 RTF를 HTML로 변환하거나 그 반대로 변환하여 편집 환경을 구현해야 합니다. 한편 iOS팀은 RTF 사용을 맞춤 모델로 대체하는 이 도구의 새로운 구현 작업을 시작했습니다. 따라서 앱이 동일한 Swift 코드를 공유하는 모든 플랫폼에서 친숙한 방식으로 스타일이 지정된 텍스트를 표현할 수 있습니다.
이 과제는 사용자의 요구에 따라 반복적으로 해결되었기 때문에 프로젝트 로드맵에서 가장 흥미로운 부분 중 하나였습니다. 이 문제는 사용자 중심의 접근 방식으로 해결된 엔지니어링 문제였습니다. 팀은 텍스트를 렌더링할 수 있도록 코드의 일부를 다시 작성하여 두 번째 버전에서 텍스트 편집을 사용 설정해야 했습니다.
반복 출시
지난 2년간 이 프로젝트는 엄청난 발전을 겪었습니다. 팀은 읽기 전용 버전의 프로젝트 작업을 시작했고 몇 달 후 다양한 편집 기능이 포함된 새로운 버전을 출시했습니다. 팀은 코드 변경사항을 프로덕션에 자주 출시하기 위해 기능 플래그를 광범위하게 사용하기로 결정했습니다. 팀은 모든 출시에서 새로운 기능을 사용 설정하고 몇 주 후에 사용자에게 표시되는 새로운 기능을 구현하는 코드 변경사항도 출시할 수 있었습니다. 하지만 팀에서 개선의 여지가 있다고 생각하는 부분이 있습니다. 이들은 플래그 값을 변경하기 위해 재배포할 필요가 없기 때문에 동적 기능 플래그 시스템을 도입하면 작업 속도를 높이는 데 도움이 될 것이라고 생각합니다. Goodnotes는 프로젝트 배포를 제품 출시에 연결할 필요가 없으므로, Goodnotes는 더 많은 유연성을 확보하고 새로운 기능의 배포 속도를 높일 수 있습니다.
오프라인 작업
담당 팀이 작업한 주요 기능 중 하나는 오프라인 지원입니다. 문서를 편집하고 수정하는 것은 이와 같은 모든 애플리케이션에서 기대되는 기능 중 하나입니다. 하지만 Goodnotes는 공동작업을 지원하기 때문에 이 기능은 간단한 기능이 아닙니다. 즉, 여러 기기에서 여러 사용자가 실행한 모든 변경사항은 사용자에게 충돌 해결을 요청하지 않고도 모든 기기에서 이루어져야 합니다. Goodnotes는 오래전에 CRDT를 내부적으로 사용하여 이 문제를 해결했습니다. 이러한 충돌 없는 복제 데이터 유형 덕분에 Goodnotes는 모든 사용자가 문서에서 수행한 모든 변경사항을 결합하고 병합 충돌 없이 변경사항을 병합할 수 있습니다. IndexedDB의 사용과 웹브라우저에서 사용할 수 있는 저장용량은 웹에서 오프라인 공동작업 환경을 크게 향상하는 원동력이 되었습니다.
또한 Goodnotes 웹 앱을 열면 Wasm 바이너리 크기로 인해 초기 다운로드 비용이 약 40MB가 됩니다. 처음에 Goodnotes팀은 App Bundle 자체와 사용하는 대부분의 API 엔드포인트에 일반 브라우저 캐시에만 의존했지만, 나중에는 더 안정적인 Cache API 및 서비스 워커로 이익을 얻을 수 있었습니다. 팀은 원래 복잡하다고 추정되는 문제 때문에 이 작업을 피했지만, 결국 Workbox가 훨씬 덜 두려움을 덜어 주었다는 것을 깨달았습니다.
웹에서 Swift 사용 시 권장사항
iOS 애플리케이션에 재사용하고 싶은 코드가 많다면 멋진 여정을 시작할 준비를 하세요. 시작하기 전에 흥미로운 몇 가지 팁이 있습니다.
- 재사용할 코드를 확인합니다. 앱의 비즈니스 로직이 서버 측에서 구현되는 경우 UI 코드를 재사용하려고 할 수 있습니다. 이 경우에는 Wasm이 도움이 되지 않습니다. 팀에서 WebAssembly로 브라우저 앱을 빌드하기 위한 SwiftUI 호환 프레임워크인 Tokamak을 간략하게 살펴보았지만 앱의 요구사항을 충분히 충족시키지 못했습니다. 그러나 클라이언트 코드의 일부로 강력한 비즈니스 로직이나 알고리즘이 구현된 경우 Wasm을 사용하는 것이 가장 좋습니다.
- Swift 코드베이스가 준비되었는지 확인하세요. UI 레이어 또는 특정 아키텍처용 소프트웨어 디자인 패턴을 사용하면 UI 레이어 구현을 재사용할 수 없으므로 UI 로직과 비즈니스 로직이 강력하게 분리됩니다. 깔끔한 아키텍처 또는 육각형 아키텍처 원칙도 기본입니다. 모든 IO 관련 코드에 종속 항목을 삽입하여 제공해야 하기 때문입니다. 또한 구현 세부정보가 추상화로 정의되고 종속 항목 역전 원칙이 많이 사용되는 이러한 아키텍처를 따르면 훨씬 더 쉽게 진행할 수 있습니다.
- Wasm은 UI 코드를 제공하지 않습니다. 따라서 웹에 사용할 UI 프레임워크를 결정합니다.
- JSKit는 Swift 코드를 JavaScript와 통합하는 데 도움이 되지만 핫경로가 있는 경우 JS-Swift 브릿지를 교차하는 비용이 많이 들 수 있으며 이를 내보낸 함수로 바꿔야 할 수 있습니다. JSKit의 내부 작동 방식에 관한 자세한 내용은 공식 문서와 숨겨진 gem, Swift의 Dynamic Member Lookup! 게시물을 참고하세요.
- 아키텍처 재사용 가능 여부는 앱에서 따르는 아키텍처와 사용하는 비동기 코드 실행 메커니즘 라이브러리에 따라 다릅니다. MVVP 또는 구성 가능한 아키텍처와 같은 패턴을 사용하면 Wasm과 함께 사용할 수 없는 UIKit 종속 항목에 구현을 결합하지 않고도 뷰 모델과 UI 로직의 일부를 재사용할 수 있습니다. RXSwift 및 기타 라이브러리는 Wasm과 호환되지 않을 수 있으므로 OpenCombine, async/await, Goodnotes의 Swift 코드에서 스트림을 사용해야 합니다.
- gzip 또는 brotli를 사용하여 Wasm 바이너리를 압축합니다. 기본 웹 애플리케이션의 경우 바이너리의 크기가 상당히 크다는 점에 유의하세요.
- PWA 없이 Wasm을 사용할 수 있더라도 웹 앱에 매니페스트가 없거나 사용자가 이를 설치하지 못하게 하려는 경우에도 최소한 서비스 워커는 포함해야 합니다. 서비스 워커는 Wasm 바이너리와 모든 앱 리소스를 무료로 저장하고 제공하므로 사용자가 프로젝트를 열 때마다 다운로드할 필요가 없습니다.
- 채용은 예상보다 어려울 수 있습니다. Swift 경험이 있는 강력한 웹 개발자 또는 웹 경험이 있는 우수한 Swift 개발자를 고용해야 할 수 있습니다. 두 플랫폼 모두에 대한 지식을 갖춘 제너럴리스트 엔지니어를 찾을 수 있다면
결론
도전과제로 가득한 제품 작업을 하면서 복잡한 기술 스택을 사용해서 웹 프로젝트를 빌드하는 것은 놀라운 경험입니다. 어려운 일이지만 그만한 가치가 있습니다. Goodnotes는 이 접근 방식을 사용하지 않고 iOS 애플리케이션의 새 기능을 개발하는 동안 Windows, Android, ChromeOS, 웹용 버전을 출시할 수 없었습니다. 이 기술 스택과 Goodnotes의 엔지니어링팀 덕분에 Goodnotes는 현재 어디에나 있으며 다음 도전을 위해 계속 노력할 준비가 되었습니다. 이 프로젝트에 대해 자세히 알아보려면 Goodnotes팀이 NSSpain 2023에서 발표한 대담을 시청하세요. 웹용 Goodnotes를 사용해 보세요.