모든 위치에서 유용한 메모

iPad에서 제품을 사용하는 여성을 보여주는 Goodnotes 마케팅 이미지

지난 2년 동안 Goodnotes 엔지니어링팀은 성공적인 iPad 메모 앱을 다른 플랫폼으로 가져오는 프로젝트를 진행해 왔습니다. 이 사례 연구에서는 2022년 iPad 올해의 앱이 웹 기술과 WebAssembly를 기반으로 웹, ChromeOS, Android, Windows로 확장된 과정을 다룹니다. 이 과정에서 팀은 10년 넘게 사용해 온 동일한 Swift 코드를 재사용했습니다.

Goodnotes 로고

Goodnotes가 웹, Android, Windows로 출시된 이유

2021년에는 Goodnotes가 iOS 및 iPad용 앱으로만 제공되었습니다. Goodnotes의 엔지니어링팀은 추가 운영체제와 플랫폼을 위한 새 버전의 Goodnotes를 만드는 엄청난 기술적 과제를 수락했습니다. 제품은 iOS 애플리케이션과 완벽하게 호환되고 동일한 메모를 렌더링해야 합니다. PDF 위에 작성된 메모나 첨부된 이미지는 동일해야 하며 iOS 앱에 표시되는 것과 동일한 획을 보여야 합니다. 추가된 모든 획은 사용자가 사용 중인 도구(예: 펜, 형광펜, 만년필, 도형, 지우개)와 관계없이 iOS 사용자가 만들 수 있는 획과 동일해야 합니다.

손글씨 메모와 스케치가 포함된 Goodnotes 앱 미리보기

요구사항과 엔지니어링팀의 경험을 바탕으로, 이미 수년 동안 작성되고 충분히 테스트된 Swift 코드베이스를 재사용하는 것이 가장 좋은 방법이라는 결론을 내렸습니다. 하지만 이미 존재하는 iOS/iPad 애플리케이션을 Flutter 또는 Compose 멀티플랫폼과 같은 다른 플랫폼이나 기술로 포팅하는 것이 더 나을 수 있습니다. 새 플랫폼으로 이전하려면 Goodnotes를 다시 작성해야 합니다. 이렇게 하면 이미 구현된 iOS 애플리케이션과 0에서 빌드할 새 애플리케이션 간에 개발 경쟁이 시작되거나 새 코드베이스가 따라잡는 동안 기존 애플리케이션의 새 개발이 중단될 수 있습니다. Goodnotes에서 Swift 코드를 재사용할 수 있다면 크로스 플랫폼팀이 앱 기본사항과 기능 동등성을 달성하기 위해 노력하는 동안 iOS팀에서 구현한 새로운 기능을 활용할 수 있습니다.

이 제품은 이미 iOS에서 다음과 같은 기능을 추가하기 위한 여러 가지 흥미로운 문제를 해결했습니다.

  • 메모 렌더링
  • 문서 및 메모 동기화
  • 충돌이 없는 복제 데이터 유형을 사용하여 메모의 충돌을 해결합니다.
  • AI 모델 평가를 위한 데이터 분석
  • 콘텐츠 검색 및 문서 색인 생성
  • 맞춤 스크롤 환경 및 애니메이션
  • 모든 UI 레이어의 뷰 모델 구현

엔지니어링팀이 iOS 및 iPad 애플리케이션에서 이미 작동하는 iOS 코드베이스를 가져와 Goodnotes가 Windows, Android 또는 웹 애플리케이션으로 제공할 수 있는 프로젝트의 일부로 실행할 수 있다면 모든 기능을 다른 플랫폼에 훨씬 더 쉽게 구현할 수 있습니다.

Goodnotes의 기술 스택

다행히 웹에서 기존 Swift 코드를 재사용할 수 있는 방법이 있었습니다. 바로 WebAssembly (Wasm)입니다. Goodnotes는 오픈소스 및 커뮤니티 유지관리 프로젝트인 SwiftWasm을 사용하여 Wasm을 사용한 프로토타입을 빌드했습니다. Goodnotes팀은 SwiftWasm을 사용하여 이미 구현된 모든 Swift 코드를 사용하여 Wasm 바이너리를 생성할 수 있었습니다. 이 바이너리는 Android, Windows, ChromeOS, 기타 모든 운영체제용 프로그레시브 웹 애플리케이션으로 제공되는 웹페이지에 포함될 수 있습니다.

Goodnotes 출시 시퀀스는 Chrome으로 시작하여 Windows, Android, 마지막으로 Linux와 같은 다른 플랫폼으로 이어지며 모두 PWA를 기반으로 합니다.

목표는 Goodnotes를 PWA로 출시하고 모든 플랫폼 스토어에 등록하는 것이었습니다. 이미 iOS에 사용된 프로그래밍 언어인 Swift와 웹에서 Swift 코드를 실행하는 데 사용된 WebAssembly 외에도 프로젝트에서는 다음과 같은 기술을 사용했습니다.

  • TypeScript: 웹 기술에 가장 많이 사용되는 프로그래밍 언어입니다.
  • React 및 Webpack: 웹에서 가장 인기 있는 프레임워크 및 번들러입니다.
  • PWA 및 서비스 워커: 이 프로젝트의 큰 지원 요소입니다. 팀에서 다른 iOS 앱과 마찬가지로 작동하는 오프라인 애플리케이션으로 앱을 제공할 수 있고 스토어 또는 브라우저 자체에서 설치할 수 있기 때문입니다.
  • PWABuilder: Goodnotes에서 Microsoft Store에서 앱을 배포할 수 있도록 PWA를 네이티브 Windows 바이너리로 래핑하는 데 사용하는 기본 프로젝트입니다.
  • 신뢰할 수 있는 웹 활동: 회사에서 PWA를 네이티브 애플리케이션으로 배포하는 데 사용하는 가장 중요한 Android 기술입니다.

Swift, Wasm, React, PWA로 구성된 Goodnotes 기술 스택

다음 그림은 기존 TypeScript 및 React를 사용하여 구현된 항목과 SwiftWasm 및 표준 JavaScript, Swift, WebAssembly를 사용하여 구현된 항목을 보여줍니다. 이 프로젝트 부분은 필요한 경우 Swift 코드에서 편집기 화면의 DOM을 처리하거나 일부 브라우저별 API를 사용하기 위해 팀에서 사용하는 Swift 및 WebAssembly용 JavaScript 상호 운용성 라이브러리인 JSKit를 사용합니다.

Wasm에서 구동되는 특정 그리기 영역과 React에서 구동되는 UI 영역을 보여주는 모바일 및 데스크톱의 앱 스크린샷

Wasm과 웹을 사용해야 하는 이유

Wasm은 Apple에서 공식적으로 지원하지 않지만 Goodnotes 엔지니어링팀은 다음과 같은 이유로 이 접근 방식이 가장 적합하다고 생각했습니다.

  • 10만 개가 넘는 코드 줄을 재사용합니다.
  • 핵심 제품의 개발을 계속하면서 크로스 플랫폼 앱에도 기여할 수 있는 능력
  • 반복적인 개발 프로세스를 사용하여 최대한 빨리 모든 플랫폼에 도달할 수 있는 강력한 기능
  • 모든 비즈니스 로직을 중복하지 않고 동일한 문서를 렌더링할 수 있는 제어 기능을 갖추고 구현에 차이를 도입합니다.
  • 모든 플랫폼에서 동시에 이루어진 모든 성능 개선사항 (및 모든 플랫폼에서 구현된 모든 버그 수정사항)의 이점을 누릴 수 있습니다.

10만 줄이 넘는 코드와 렌더링 파이프라인을 구현하는 비즈니스 로직을 재사용하는 것이 기본이었습니다. 동시에 Swift 코드를 다른 도구 모음과 호환하면 향후 필요한 경우 다른 플랫폼에서 이 코드를 재사용할 수 있습니다.

반복적 제품 개발

팀은 사용자에게 최대한 빨리 결과물을 제공하기 위해 반복적인 접근 방식을 취했습니다. Goodnotes는 사용자가 공유된 문서를 가져와 어떤 플랫폼에서든 읽을 수 있는 읽기 전용 버전으로 시작했습니다. 링크 하나만 있으면 iPad에서 작성한 것과 동일한 메모에 액세스하여 읽을 수 있습니다. 다음 단계에서는 교차 플랫폼 버전을 iOS 버전과 동등하게 만들기 위해 수정 기능을 추가했습니다.

읽기 전용에서 모든 기능을 갖춘 제품으로 전환되는 것을 나타내는 앱 스크린샷 2개

읽기 전용 제품의 첫 번째 버전은 개발하는 데 6개월이 걸렸고, 그 후 9개월은 내가 만들었거나 다른 사용자가 공유한 모든 문서를 확인할 수 있는 UI 화면과 첫 번째 편집 기능을 만드는 데 사용되었습니다. 또한 SwiftWasm 도구 모음을 통해 iOS 플랫폼의 새로운 기능을 크로스 플랫폼 프로젝트로 쉽게 이식할 수 있었습니다. 예를 들어 수천 줄의 코드를 재사용하여 새로운 유형의 펜을 만들고 크로스 플랫폼을 쉽게 구현했습니다.

이 프로젝트를 빌드하는 것은 놀라운 경험이었고 Goodnotes는 이 프로젝트를 통해 많은 것을 배웠습니다. 따라서 다음 섹션에서는 웹 개발 및 WebAssembly 사용과 Swift와 같은 언어에 관한 흥미로운 기술적 요소에 중점을 둡니다.

초기 장애물

이 프로젝트를 진행하는 것은 여러 관점에서 매우 어려운 일이었습니다. 팀이 발견한 첫 번째 장애물은 SwiftWasm 도구 모음과 관련이 있었습니다. 도구 모음은 팀에 큰 도움이 되었지만 일부 iOS 코드는 Wasm과 호환되지 않았습니다. 예를 들어 뷰, API 클라이언트 또는 데이터베이스 액세스 구현과 같이 IO 또는 UI와 관련된 코드는 재사용할 수 없으므로 팀은 크로스 플랫폼 솔루션에서 재사용할 수 있도록 앱의 특정 부분을 리팩터링해야 했습니다. 팀에서 만든 PR의 대부분은 추상 종속 항목으로 리팩터링되었으므로 나중에 종속 항목 주입이나 기타 유사한 전략을 사용하여 이를 대체할 수 있었습니다. iOS 코드는 원래 Wasm에서 구현할 수 있는 원시 비즈니스 로직을 Wasm에서 지원하지 않기 때문에 Wasm에서 구현할 수 없는 입력/출력 및 사용자 인터페이스를 담당하는 코드와 혼합했습니다. 따라서 Swift 비즈니스 로직을 플랫폼 간에 재사용할 준비가 되면 IO 및 UI 코드를 TypeScript에서 다시 구현해야 했습니다.

성능 문제가 해결됨

Goodnotes에서 편집기 작업을 시작하면서 수정 환경에 몇 가지 문제가 있는 것으로 확인되었으며 로드맵에 기술적 제약이 발생했습니다. 첫 번째 문제는 성능과 관련이 있습니다. JavaScript는 단일 스레드 언어입니다. 즉, 호출 스택이 하나 있고 메모리 힙이 하나 있습니다. 순서대로 코드를 실행하며 다음으로 이동하기 전에 한 부분의 코드 실행을 완료해야 합니다. 동기식이지만 때로는 유해할 수 있습니다. 예를 들어 함수를 실행하는 데 시간이 걸리거나 무언가를 기다려야 하는 경우 그동안 모든 것이 정지됩니다. 엔지니어들이 해결해야 했던 문제는 바로 이 문제였습니다. 렌더링 레이어 또는 기타 복잡한 알고리즘과 관련된 코드베이스의 일부 특정 경로를 평가하는 것은 팀에 문제가 되었습니다. 이러한 알고리즘은 동기식이며 실행하면 기본 스레드가 차단되기 때문입니다. Goodnotes팀은 속도를 높이기 위해 코드를 다시 작성하고 일부 코드를 리팩터링하여 비동기식으로 만들었습니다. 또한 앱이 알고리즘 실행을 중지하고 나중에 계속할 수 있도록 브라우저가 UI를 업데이트하고 프레임 드롭을 방지할 수 있는 yield 전략을 도입했습니다. 이는 iOS 애플리케이션에는 문제가 되지 않았습니다. 기본 iOS 스레드가 사용자 인터페이스를 업데이트하는 동안 백그라운드에서 스레드를 사용하고 이러한 알고리즘을 평가할 수 있기 때문입니다.

엔지니어링팀이 해결해야 했던 또 다른 문제는 DOM에 연결된 HTML 요소를 기반으로 하는 UI를 전체 화면 캔버스를 기반으로 하는 문서 UI로 이전하는 것이었습니다. 이 프로젝트는 다른 웹페이지와 마찬가지로 HTML 요소를 사용하여 문서와 관련된 모든 메모와 콘텐츠를 DOM 구조의 일부로 표시하기 시작했지만, 브라우저가 DOM 업데이트를 실행하는 시간을 줄여 저가형 기기의 성능을 개선하기 위해 어느 시점에서 전체 화면 캔버스로 이전했습니다.

엔지니어링팀은 프로젝트 초기에 다음과 같은 변경사항을 적용했다면 발생한 문제를 일부 줄일 수 있었을 것이라고 판단했습니다.

  • 웹 작업자를 자주 사용하여 과도한 알고리즘에 더 많은 기본 스레드를 오프로드합니다.
  • Wasm 컨텍스트에서 나가기의 성능 영향을 줄일 수 있도록 처음부터 JS-Swift 상호 운용성 라이브러리 대신 내보낸가져온 함수를 사용합니다. 이 JavaScript 상호 운용성 라이브러리는 DOM 또는 브라우저에 액세스하는 데 유용하지만 네이티브 Wasm 내보내기 함수보다 느립니다.
  • 앱이 메인 스레드를 오프로드하고 메모를 작성할 때 애플리케이션의 성능을 극대화하도록 Canvas API의 모든 사용을 웹 워커로 이동할 수 있도록 코드에서 내부적으로 OffscreenCanvas 사용을 허용해야 합니다.
  • 앱이 기본 스레드 워크로드를 줄일 수 있도록 모든 Wasm 관련 실행을 웹 작업자 또는 웹 작업자 풀로 이동합니다.

텍스트 편집기

또 다른 흥미로운 문제는 특정 도구인 텍스트 편집기와 관련이 있습니다. 이 도구의 iOS 구현은 내부적으로 RTF를 사용하는 소형 도구 모음인 NSAttributedString를 기반으로 합니다. 그러나 이 구현은 SwiftWasm과 호환되지 않으므로 크로스 플랫폼팀은 먼저 RTF 문법을 기반으로 하는 맞춤 파서를 만든 다음 나중에 RTF를 HTML로 변환하고 그 반대로 변환하여 수정 환경을 구현해야 했습니다. 한편 iOS팀은 앱이 동일한 Swift 코드를 공유하는 모든 플랫폼에서 친숙한 방식으로 스타일이 지정된 텍스트를 표현할 수 있도록 RTF 사용을 맞춤 모델로 대체하는 이 도구의 새로운 구현 작업을 시작했습니다.

Goodnotes 텍스트 편집기

이 문제는 사용자의 요구사항에 따라 반복적으로 해결되었기 때문에 프로젝트 로드맵에서 가장 흥미로운 점 중 하나였습니다. 이 문제는 사용자 중심 접근 방식을 사용하여 해결된 엔지니어링 문제로, 팀에서 텍스트를 렌더링할 수 있도록 코드의 일부를 다시 작성해야 했으므로 두 번째 출시에서 텍스트 수정을 사용 설정했습니다.

반복 출시

지난 2년간 프로젝트가 놀라운 발전을 거듭했습니다. 팀은 프로젝트의 읽기 전용 버전으로 작업을 시작했고 몇 달 후 수많은 편집 기능이 포함된 완전히 새로운 버전을 출시했습니다. 코드 변경사항을 프로덕션에 자주 출시하기 위해 팀은 기능 플래그를 광범위하게 사용하기로 결정했습니다. 팀은 출시마다 새 기능을 사용 설정하고 사용자가 몇 주 후에 볼 수 있는 새 기능을 구현하는 코드 변경사항을 출시할 수 있습니다. 하지만 YouTube팀은 개선할 수 있었던 부분이 있다고 생각합니다. 동적 기능 플래그 시스템을 도입하면 플래그 값을 변경하기 위해 재배포할 필요가 없으므로 속도를 높일 수 있다고 생각합니다. 이렇게 하면 Goodnotes가 프로젝트 배포를 제품 출시에 연결할 필요가 없으므로 Goodnotes의 유연성이 향상되고 새 기능의 배포 속도가 빨라집니다.

오프라인 작업

팀에서 작업한 주요 기능 중 하나는 오프라인 지원입니다. 문서를 수정하고 수정할 수 있는 기능은 이와 같은 애플리케이션에서 기대할 수 있는 기능 중 하나입니다. 하지만 Goodnotes에서 공동작업을 지원하므로 간단한 기능은 아닙니다. 즉, 여러 사용자가 여러 기기에서 실행한 모든 변경사항은 사용자에게 충돌을 해결하도록 요청하지 않고도 모든 기기에 반영되어야 합니다. Goodnotes는 오래 전부터 내부에서 CRDT를 사용하여 이 문제를 해결했습니다. 이러한 충돌 없는 복제 데이터 유형 덕분에 Goodnotes는 모든 사용자가 문서에서 수행한 모든 변경사항을 결합하고 병합 충돌 없이 변경사항을 병합할 수 있습니다. IndexedDB 및 웹브라우저에서 사용할 수 있는 스토리지를 사용하면 웹에서 공동작업 오프라인 환경을 쉽게 구현할 수 있었습니다.

Goodnotes 앱이 오프라인에서 작동합니다.

또한 Goodnotes 웹 앱을 열면 Wasm 바이너리 크기로 인해 초기 선행 다운로드 비용이 약 40MB 발생합니다. 처음에 Goodnotes팀은 앱 번들 자체와 사용하는 대부분의 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의 작동 방식에 관한 자세한 내용은 공식 문서Swift의 숨겨진 보석, 동적 멤버 조회 게시물을 참고하세요.
  • 아키텍처를 재사용할 수 있는지 여부는 앱이 따르는 아키텍처와 사용하는 비동기 코드 실행 메커니즘 라이브러리에 따라 다릅니다. MVVP 또는 컴포저블 아키텍처와 같은 패턴을 사용하면 Wasm과 함께 사용할 수 없는 UIKit 종속 항목에 구현을 결합하지 않고도 뷰 모델과 UI 로직의 일부를 재사용할 수 있습니다. RXSwift 및 기타 라이브러리가 Wasm과 호환되지 않을 수 있으므로 주의하세요. Goodnotes의 Swift 코드에서 OpenCombine, async/await, 스트림을 사용해야 하기 때문입니다.
  • gzip 또는 brotli를 사용하여 Wasm 바이너리를 압축합니다. 기존 웹 애플리케이션의 경우 바이너리 크기가 상당히 큽니다.
  • PWA 없이 Wasm을 사용할 수 있는 경우에도 웹 앱에 매니페스트가 없거나 사용자가 설치하지 못하도록 하려는 경우에도 서비스 워커를 포함해야 합니다. 서비스 워커는 Wasm 바이너리와 모든 앱 리소스를 무료로 저장하고 제공하므로 사용자가 프로젝트를 열 때마다 다운로드할 필요가 없습니다.
  • 채용이 예상보다 어려울 수 있습니다. Swift에 대한 경험이 있는 우수한 웹 개발자 또는 웹에 대한 경험이 있는 우수한 Swift 개발자를 고용해야 할 수 있습니다. 두 플랫폼에 대한 지식이 있는 제너럴리스트 엔지니어를 찾을 수 있다면 좋습니다.

결론

과제가 많은 제품을 개발하는 동시에 복잡한 기술 스택을 사용하여 웹 프로젝트를 빌드하는 것은 놀라운 경험입니다. 쉽지는 않겠지만 그만한 가치가 있습니다. Goodnotes는 이 접근 방식을 사용하지 않고 iOS 애플리케이션의 새 기능을 개발하는 동안 Windows, Android, ChromeOS, 웹용 버전을 출시할 수 없었을 것입니다. 이 기술 스택과 Goodnotes 엔지니어링팀 덕분에 이제 Goodnotes가 어디서나 사용할 수 있게 되었으며, 팀은 다음 과제에 계속해서 매진할 준비가 되었습니다. 이 프로젝트에 대해 자세히 알아보려면 Goodnotes팀이 NSSpain 2023에서 진행한 강연을 시청하세요. 웹용 Goodnotes를 사용해 보세요.