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

Part 2— KMM 으로 샘플 프로젝트 개발 준비

Myungpyo Shim
13 min readJan 26, 2021

목차로 돌아가기 >

이제 실제로 KMM 을 이용한 멀티플랫폼 지원 샘플 프로젝트를 작성해 보면서 KMM 이 상용 제품에 쓸만한 수준인지 알아보도록 하겠습니다. 포스팅이 너무 길어지는 것을 방지하기 위해서 주요 코드 및 프로젝트 진행 중 발생 가능한 몇몇 예외 상황들에 대한 해결책 위주로 설명하겠습니다.
(샘플 프로젝트의 전체 코드는 이곳에서 확인하실 수 있습니다.)

그럼 시작해 봅시다.🔥

이번에 KMM 을 이용하여 만들어 보고자 하는 샘플 앱은 사용자가 영화를 검색해 보고 검색 결과 중 마음에 드는 영화는 스크랩 해 놓았다가 나중에 찾아볼 수 있는 기능을 제공하는 앱을 만들어 보려고 합니다.
이 샘플 앱의 각 플랫폼 별 완성된 모습은 다음과 같습니다.

이 앱은 스크랩 한 영화 목록(왼쪽)영화 검색(오른쪽) 두 개의 탭 으로 구성되어 있으며 각 탭의 기능은 다음과 같습니다.

  • 스크랩 한 영화 목록 탭
    - 영화 검색 탭에서 스크랩 버튼을 눌러 스크랩 된 영화 목록 표시
    - 스크랩 버튼을 이용한 스크랩 토글 기능 제공
  • 영화 검색 탭
    - Naver Open API 를 이용한 영화 검색 기능 제공
    - 검색 결과는 오프라인 검색 지원을 위해 로컬 DB 저장
    - 스크랩 버튼을 이용한 스크랩 토글 기능 제공

KMM 프로젝트 개발 시 개발 환경으로는 플랫폼 간 공통으로 사용될 모듈만 만든다면 Android Studio 만 있으면 되겠지만 iOS 앱 프로젝트까지 생성하여 실행해 보려면 Mac OS 및 XCode 설치가 필요합니다.
(KMM 개발을 위한 보다 자세한 환경 구성은 공식 사이트의 개발 환경 구성 페이지 참고 바랍니다.)

본격적으로 프로젝트 생성을 하기 전에 영화 검색 시 사용할 API 를 Naver Open API 웹에 방문하여 신청해 놓도록 합시다. API 사용 신청 후 받은 Client ID 와 Client Secret 을 따로 기록해 놓았다가 검색 기능 구현 시 사용하겠습니다.

자, 이제 Android Studio 에서 프로젝트 생성부터 시작해 봅시다.

  • 먼저 KMM 플러그인이 설치되어 있지 않다면 Preferences > Plugins 메뉴를 통해 Jetbrains 에서 공식적으로 배포하는 Kotlin Multiplatform Mobile 플러그인을 설치합니다.
  • Android Studio 에서 File > New > New Project 메뉴를 선택한 후 KMM Application 을 선택하여 프로젝트를 생성합니다.

프로젝트 이름 및 패키지는 원하는 이름으로 설정한 후에 언어는 Kotlin (기본값)으로 합니다.

다음 화면에서 Android App, iOS App, Shared 모듈 이름 역시 원하는 이름으로 사용 지정하고 (샘플에서는 기본값을 그대로 사용하겠습니다) Finish 버튼을 누릅니다.

프로젝트 생성이 완료 되고나서 화면 왼쪽에는 프로젝트 파일을 표시하는 Project Navigator 가 있습니다. Project Navigator 위쪽의 스피너를 누르면 프로젝트 파일들을 표시하는 형식을 여러가지로 지정할 수 있습니다. 기본적으로는 Android 지만 Project 로 변경하면 KMM 프로젝트의 구조를 보다 명확하게 확인할 수 있습니다.
(Android 프로젝트에서는 Android 로 설정하면 각종 Android 프로젝트 파일들을 유형별로 볼 수 있어 편리하지만 저는 Android 프로젝트에서도 Project 모드가 더 직관적이기 때문에 이렇게 작업하고 있습니다. 😁)

프로젝트 구조를 보면 크게 androidApp, iosApp, shared 모듈이 있습니다.

androidApp 은 Android 애플리케이션 모듈이며 Android Studio 에서 shared 모듈과 함께 빌드되어 Android App 을 만들 수 있습니다.
iosApp 은 iOS 애플리케이션 모듈이며 XCode 에서 framework 형태로 아카이빙 된 *shared 모듈과 함께 빌드되어 iOS App 을 만들 수 있습니다.
shared 모듈은 Android, iOS 두 모바일 플랫폼에서 공유 할 비지니스 로직을 담은 공통 모듈로써 내부적으로는 테스트 패키지를 제외하면 androidMain, iOSMain, commonMain 패키지로 이루어 집니다.

shared 모듈은 내부적으로 다음과 같은 패키지로 구성 됩니다.
shared:commonMain 패키지는 공통 모듈에서 특정 플랫폼 구현에 의존성이 없이 순수 Kotlin 만으로 구현되는 비지니스 로직이 위치하며, 멀티플랫폼 프로젝트에서 지원하는 모든 타겟이 공유하는 공통 모듈의 기반이 됩니다.
shared:androidMain 은 공통 모듈에서 Android 플랫폼에 종속적인 로직들이 위치하는 곳이며 Kotlin 으로 Android Framework (Preferences, LocationManager, …) 에 접근하여 비지니스 로직을 구현할 수 있습니다.
shared:iosMain 은 공통 모듈에서 iOS 플랫폼에 종속적인 로직들이 위치하는 곳이며 역시 Kotlin 으로 iOS Framework (Foundation, UIKit, …) 에 접근하여 비지니스 로직을 구현할 수 있습니다.

이제 프로젝트를 생성을 마쳤으니 Android Studio 에서 Android 와 iOS 타겟으로 각각 실행 해 봅시다.

만약 Android 실행 시 아직 Android Emultator 가 없다면
Menu > Tools > Avd Manager 를 통해 Emutator 를 생성 후 다시 실행 해 봅시다.

만약 iOS Target 으로 실행 하려는데 다음과 같은 오류로 실패한다면,

kmm Xcode schemesxcode-select: error: tool ‘xcodebuild’ requires Xcode, but active developer directory ‘/Library/Developer/CommandLineTools’ is a command line tools instance …

XCode 가 설치되어 있는지 확인한 후 다음의 명령어를 콘솔에서 실행하면 해결 될 것입니다.

$ sudo xcode-select -s /Applications/Xcode.app/Contents/Developer

특별한 문제가 없다면 정상적으로 Android Emulator 와 iOS Simulator 가 실행되어 프로젝트 생성 시 템플릿으로 구현 된 기본 뷰를 볼 수 있을 것 입니다.

자, 이제 기본적인 KMM 프로젝트 개발 준비가 완료 되었습니다. 👍

BuildScript

우선 프로젝트 템플릿을 통해 자동으로 생성 된 빌드 스크립트를 살펴보고, 앞으로 샘플 프로젝트 개발에 필요한 스크립트 및 의존성들을 추가해 봅시다. 프로젝트의 빌드 스크립트인 Gradle 스크립트 파일들은 2021년 현재 기준으로 다음과 같이 기본으로 Kotlin DSL 을 이용하여 작성하도록 되어 있습니다.

1⃣ /build.gradle.kts
2⃣ /shared/build.gradle.kts
3⃣ /androidApp/build.gradle.kts

빌드 스크립트의 파일명은 Groovy DSL 을 사용하는 경우 build.gradle, Kotlin DSL 을 사용하는 경우 build.gradle.kts 를 사용합니다.

1⃣ /build.gradle.kts 는 프로젝트 루트에 위치하여 프로젝트 전반적으로 영향을 미치는 스크립트 파일 입니다. 프로젝트 전반적으로 영향을 미치는 플러그인이나 의존성을 가져오기 위한 저장소 위치 등을 설정 할 수 있습니다.
2⃣ /shared/build.gradle.kts 는 공통 모듈의 빌드 스크립트 파일로 commonMain, androidMain, iosMain 의 소스 및 의존성 설정 등을 할 수 있습니다.
3⃣ /androidApp/build.gradle.kts 는 Andoird 애플리케이션 빌드를 위한 스크립트 파일로 shared 모듈에 대한 의존성을 포함, 앱 의존성들의 설정 및 앱과 관련된 여러가지 설정을 할 수 있습니다 (iOS App 은 XCode 프로젝트의 별도의 설정 파일을 사용합니다).

위에서 설명한 build.gradle.kts 파일들을 살펴보면 공통적으로 프로젝트 구성에 필요한 여러가지 의존성들을 정의하고 있는 알 수 있습니다. 멀티 플랫폼 프로젝트인 만큼 앞으로도 모듈 간에 중복된 의존성들이 각각의 스크립트 파일에 추가될 수 있고, 이러한 의존성들의 버전 관리를 위해서 여러 경로의 스크립트 파일들을 확인해 보아야 할 것 이라는 생각이 들것 입니다.
꼭 KMM 과 같은 멀티플랫폼 프로젝트의 경험이 아니더라도 하나의 플랫폼에서 멀티 모듈 프로젝트를 개발해본 경험이 있다면 의존성 간의 관계 및 버전을 하나의 파일에 정의해서 관리하고 각각의 모듈들은 이 의존성 정의 파일을 참조하도록 만들면 의존성 관리가 훨씬 편해진다는 것을 경험으로 알고 있을 것입니다.

과거 Gradle DSL 을 사용 할 때는 Dependencies.gradle 이라는 파일을 별도로 만들고 Gradle ext 속성에 자체 정의한 의존성 트리를 등록하여 각 모듈에서 참조하여 필요한 의존성을 사용하도록 구성 했었습니다.
이번에 우리는 Kotlin DSL 을 사용하고 있으므로 보다 타입에 안전하고 코드 네비게이션까지 지원되는 방식으로 개발할 수 있습니다.
(자세한 내용이 궁금하면 kotlin buildSrc 로 구글링 해봅시다. 😁)

❗️Disclaimer
저는 이렇게 중앙화 된 의존성 관리 방식을 좋아하긴 하지만 아직까지는 호불호가 갈리는 부분이긴 합니다. 혹시 이 방식을 싫어하시는 분은 이 부분은 생략하고 다음 구현으로 넘어가시기 바랍니다. 사실 저도 이 방식이 조금 불편한 점이 있긴 한데 의존성 관리 방식을 이렇게 변경하고 나면 의존성 관리를 위한 IDE 지원이 제대로 되지 않는 부분들이 있습니다. 빠른 시일 내에Android Studio(IntelliJ) 가 이러한 의존성 정의 파일까지 트래킹하여 IDE 의 다양한 의존성 관련 기능들이 지원 되었으면 좋겠습니다.

그러면 이제 빌드 타임에 프로젝트의 모든 모듈에서 참조할 의존성을 정의할 buildSrc 디렉토리를 프로젝트 루트에 만들고 다음과 같이 build.gradle.kts 파일과 Dependencies.kt 파일을 만들어 봅시다.

build.gradle.kts 파일은 kotlin DSL 을 사용하기 위한 플러그인 설정이 전부입니다.

다음으로 Dependencies.kt 파일을 살펴보겠습니다. 모듈간 버전 관리를 위한 여러가지 Best Practice 들이 있겠지만 저는 KMM 프로젝트에서 어울릴 것 같은 방식으로 다음과 같이 구성해 보았습니다.

VersionMap 에는 Dependencies 오브젝트에서 사용 할 모든 의존성들을 이름과 버전으로 매핑하여 정의하고 있습니다. 그리고 각 의존성들은 유형별로 오브젝트로 그룹핑하여 관리하고 있습니다 (e.g. Network, Storage, …).

예를 들어 Network 오브젝트 부분을 살펴봅시다. 샘플 프로젝트에서는 HTTP 통신을 위한 네트워크 모듈로 Ktor 를 사용합니다. Ktor 는 멀티 플랫폼을 지원하는 가볍고 유연한 네트워크 지원 라이브러리 입니다. 보통 멀티 플랫폼을 지원하는 라이브러리들은 현재 우리 샘플 프로젝트와 유사하게 모든 플랫폼에서 공유하는 부분과 각 플랫폼에 의존적인 부분을 분리하여 개발하고 배포합니다. 그래서 Ktor 의 경우도 살펴보면 모든 플랫폼에서 공유하는 부분을 ktor-client-core 모듈로 배포하고, 각 플랫폼에 의존적인 부분을 ktor-client-{PLATFORM} 형식으로 배포합니다. 그래서 위 Dependencies.kt 파일의 Network 부분을 보면 core 라이브러리를 Network 오브젝트의 다이렉트 속성으로 정의하고, 각 플랫폼 의존적인 라이브러리는 각 플랫폼 오브젝트를 추가한 후 플랫폼 오브젝트의 속성으로 정의하였습니다.

네트워크 오브젝트에서 기본적인 ktor 의존성 외에
"io.ktor:ktor-client-serialization:${KTOR.version}"
부분은 ktor http client 에서 json 응답을 파싱하는데 사용할 kotlin-serialization integration library 입니다.
샘플 프로젝트에서는 JSON parsing 라이브러리로 kotlin-serialization 을 사용하고 있기 때문에 이를 Ktor 에 사용하기 위해서 추가 하였습니다. Gson, Jackson 같은 다른 직렬화 라이브러리를 사용한다면 Ktor 에서 다양한 라이브러리를 지원하고 있기 때문에 그에 적합한 라이브러리를 추가하면 됩니다.

샘플 프로젝트의 전체 Dependencies.kt 파일은 링크를 누르면 보실 수 있습니다. 링크의 Dependencies.kt 를 보면 많은 의존성들이 정의되어 있는데 앞서 이야기 한 것 처럼 프로젝트 전체의 의존성을 이 한 파일에서 관리하기 때문에 다소 길어질 수 있습니다. 하지만 프로젝트를 진행하면서 의존성을 변경/업데이트 할 때 이러한 방식을 편리함을 느낄 수 있게 될 것 입니다.

이후부터는 기능 구현 시 의존성 정의에 대한 설명은 생략하고 각 모듈에서 정의 된 의존성을 가져다 쓰는 부분만 이야기 할 것이기 때문에 의존성 파일은 위 링크의 전체 내용을 추가해 놓고 다음으로 진행합시다.
(이 파일에 의존성을 정의 한 것 만으로 프로젝트에 사용되는 것은 아니며 각 모듈에서 이 파일의 의존성을 참조하여 필요한 타입-api, implementation, …-으로 추가해야 실제로 사용하게 됩니다.)

이제 샘플 프로젝트 루트의 build.gradle 파일을 살펴 봅시다.
새로 생성한 프로젝트와 조금 다를 수 있지만 문제되지는 않습니다.

빌드 스크립트 플러그인들을 위한 저장소들 정의와 플러그인 의존성 정의, 그리고 프로젝트 전체 모듈에서 사용 될 저장소 위치 등이 정의되어 있습니다. 이 파일에서 빌드 스크립트 의존성을 조금 전에 정의한 의존성 파일의 오브젝트들을 이용하도록 수정해 봅시다.

dependencies { } 블록을 보면 우리가 정의한 Dependencies 오브젝트를 이용하여 빌드 스크립트 의존성을 정의하고 있음을 알 수 있습니다. 추가로 Kotlin 이 지원하는 직렬화 라이브러리를 사용하기 위해 kotlin-serialization 플러그인, 로컬 DB 로 사용 할 SqlDelight 의 빌드 태스크 생성 등을 위한 sqlDelight 플러그인을 정의하였습니다.

allprojects.repositories 에 몇몇 저장소가 추가되었는데 현재 KMM 프로젝트에 알려진 이슈의 해결을 위한 Workaround 라이브러리들이 임시 저장소에 배포되고 있는데 이러한 라이브러리들을 사용하기 위해 추가되었습니다. 공식적인 배포가 완료되면 임시 버전을 삭제하고 공식 버전을 사용하도록 수정해야 합니다.

프로젝트 전반적으로 영향을 주는 프로젝트 루트의 빌드 스크립트는 Dependencies 오브젝트를 사용하도록 정리가 되었으니 이제 샘플 프로젝트를 위한 공통 모듈을 개발하고, 이 공통 모듈을 사용하는 Android / iOS 앱을 개발해 보도록 합시다.

다음 (샘플 프로젝트 공통 모듈 개발) >
목차로 돌아가기 >

--

--