KMM 을 통한 Android, iOS 간 코드 공유

Part 5— 샘플 프로젝트 iOS 앱 개발

Myungpyo Shim
6 min readJan 26, 2021

목차로 돌아가기 >

이제부터 영화 검색 및 스크랩 앱을 위해 생성한 공통 모듈을 이용하여 iOS App 을 만들어 보겠습니다.

샘플 프로젝트에서 iOS App 은 Project Root 에 iosApp 디렉토리에 위치합니다. 애플리케이션 코드 전체를 설명하기 보다 구조에 대한 설명과 더불어 공통 모듈을 사용하는 부분을 집중적으로 다루겠습니다. 🙌

먼저 iOS App 에서 사용한 라이브러리 및 기본 구조에 대해 알아보겠습니다. iOS App 에서 사용한 주요 라이브러리는 다음과 같습니다.

iOS App 은 UITabBarController 에 ScrapViewController 와 SearchViewController 를 순서대로 탭 및 페이지에 할당하였습니다.
그리고 Android App 과 동일하게 MVVM 기반으로 구현 하였으며 샘플 애플리케이션이 복잡하지 않아 MovieViewModel 이라는 단일 뷰 모델을 정의하고 두 뷰 컨트롤러들이 각각 인스턴스화 하여 사용하도록 하였습니다.

UI (ViewController) Layer

가장 왼쪽의 ViewController 레벨에서는 주로 UI Lifecycle 이나 사용자 인터렉션에 대응하여 ViewModel 의 데이터를 구독하거나 관련 요청을 전달하는 일을 합니다. 위 이미지를 보면 UI 가 준비되면 iOS Combine 을 구독(sink) 하고, 스크랩 한 영화 목록의 로드(loadScrapMovies)를 요청합니다. 로드가 완료되면 구독한 Combine 으로부터 데이터를 전달 받아 화면에 그리게 됩니다.

다음은 검색 뷰 컨트롤러의 일부 코드 입니다. 샘플 앱의 두 뷰 컨트롤러가 매우 유사하기 때문에 검색 뷰 컨트롤러의 코드로 UI-ViewModel 상호작용을 살펴봅시다.

코드 제일 윗 부분부터 살펴보면 15번 라인에서 뷰 모델을 생성하고 16번 라인에서 CombineDisposeBag 이란 것을 생성하고 있습니다. CombineDisposeBag 은 Swift Combine 구독 시 취소를 위해 전달 받는 Cancellable 을 보관 하였다가 뷰 컨트롤러 라이프사이클 종료에 맞추어 취소 하기 위한 장치 입니다.

viewDidLoad 라이프사이클 콜백이 호출되면 화면에 필요한 뷰 및 뷰 간의 관계를 설정하기 위한 setupViews 함수를 호출하고 observeMovies 함수를 호출하여 뷰 모델에 있는 Combine 형식으로 제공하는 movieViewData 를 구독합니다(sink).

UITextFieldDelegate 확장 구현에서 검색 키워드 입력 후 엔터 키를 누르면 뷰 모델에 영화 검색 요청(searchMovies) 을 하도록 구현합니다. 이를 통해 뷰 모델로 영화 검색을 요청하고 결과가 도착하면 ViewData 에 반영되어 observeMovies 에서 구독하면서 새로운 데이터 도착 시 상태에 따라 스크린 레이아웃 변경 및 데이터를 반영하는 코드가 실행됩니다.

MovieTableViewAdapterDelegate 는 테이블 뷰에서 스크랩 버튼이 터치되면 테이블 뷰 아이템의 스크랩 상태 변경(토글)을 요청하기 위해 사용됩니다.

이제 뷰 컨트롤러와 상호작용하는 뷰 모델을 살펴 봅시다.

ViewModel Layer

아래는 iOS 에서 구현된 MovieViewModel 의 일부 입니다. 위에서 부터 천천히 살펴봅시다.

먼저 @LazyInject 어노테이션은 의존성 주입을 용이하게 하기 위해 구현된 property wrapper 이며 이를 통해 MovieSDKForIOS 를 주입 받습니다.
LazyInject property wrapper 구현은 다음과 같습니다.

AppDelegate 에는 DI Tool 인 Swinject 를 이용하여 빌드한 의존성 컨테이너가 있고 LazyInject property wrapper 는 이 컨테이너에서 대상이 되는 프로퍼티 타입을 찾아 주입하도록 작성되었습니다.

10 번 라인의 @Published 어노테이션은 대상 프로퍼티를 Combine 으로 만들어줍니다. 이를 통해 movieViewData 가 변경될 때 마다 movieViewData Combine 의 구독자들에게 변경된 데이터를 전달합니다.

앞서 뷰 컨트롤러 코드 46 line viewModel.$movieViewData.sink { } 가 구독하는 부분입니다.

CFlowDisposeBag 은 앞서 뷰 컨트롤러에서 사용하는 CombineDisposeBag 과 유사한 동작을 수행하며 뷰 모델에서 구독하는 CFlow 를 뷰 모델 deinit 시점에 취소 합니다.

searchMovies 함수가 호출되면 기존에 구독중인 CFlow 를 먼저 취소하고, movieSdk 를 통해 영화 검색 CFlow 를 반환 받아 구독 합니다. 영화 검색 결과가 도착하면 movieViewData 에 반영합니다.

ViewData 는 Android app 에서 정의했던 것과 마찬가지로 다음과 같이 정의되어 UI 에 바인딩 되는 데이터의 각종 상태를 나타냅니다.

updateScrap 함수가 호출되면 먼저 메모리에서 해당 영화를 찾아 스크랩 상태를 토글하고 movieSdk 를 통해 스크랩 상태 업데이트를 요청하며 실패할 경우 메모리의 스크랩 상태를 다시 원래대로 복원합니다.

지금까지 공통 모듈을 사용하는 iOS App 에 대해 간략히 알아보았습니다.

다음 파트에서는 KMM 샘플 프로젝트가 완료되었으니 그동안 해 온 작업들에 대해 다시 한번 돌아보고 멀티 플랫폼, 특히 Kotlin-Native 를 사용할 경우 주의해야 할 점에 대해서도 알아보겠습니다.

감사의 말.

Android 개발자로서 KMM 샘플 프로젝트를 만들어 보면서 가장 어려웠던 부분은 역시 iOS 앱을 만드는 것이었습니다. 과거 몇 회의 교육을 들어보긴 했지만 아무래도 실무에 사용하지 않다보니 Kotlin 과 문법적 유사도가 높은 Swift 이지만 늘 생소하기만 합니다. 😅

Common Module 과 Android App 완성 후 iOS APP 개발을 시작하며 두통이 찾아올 무렵 저의 옛 동료인 iOS 개발자 ⭐️ Stella Park ⭐️ 의 도움으로 프로젝트의 기본적인 구조 및 Pod 등의 설정을 수월하게 진행할 수 있었습니다. 또한, “Android 에 이런 것이 있는데 iOS 에는 없느냐?” 라는 수 많은 질문에 늘 함께 답을 찾고자 노력해주어 고맙습니다. 🙏

--

--