Kotlin Multiplatform (KMP)로 크로스플랫폼 모바일 앱 개발하기: 실전 가이드
Kotlin Multiplatform (KMP)를 활용한 크로스플랫폼 모바일 앱 개발 가이드입니다. React Native, Flutter와 차별화되는 KMP의 비즈니스 로직 공유 방식과 iOS, Android 통합 전략을 심층적으로 다룹니다.
Kotlin Multiplatform (KMP)로 크로스플랫폼 모바일 앱 개발하기: 실전 가이드
현대 모바일 애플리케이션 개발은 iOS와 Android라는 두 개의 거대한 생태계를 동시에 고려해야 하는 복잡한 과제를 안고 있습니다. 각각 Swift/Objective-C와 Kotlin/Java를 사용하여 별도의 코드베이스를 관리하는 것은 개발 비용과 시간, 그리고 유지보수 측면에서 상당한 부담으로 작용합니다. 이러한 문제를 해결하기 위해 React Native, Flutter와 같은 크로스플랫폼 프레임워크들이 등장했지만, 각각의 장단점과 특정 제약사항을 가지고 있습니다.
이 글에서는 JetBrains에서 개발한 Kotlin Multiplatform (KMP)를 소개하고, KMP가 기존 크로스플랫폼 솔루션들과 어떻게 차별화되는지, 그리고 KMP를 활용하여 iOS 및 Android 모바일 앱 개발을 효율적으로 수행하는 방법에 대해 심층적으로 다루고자 합니다. KMP의 핵심 개념부터 실제 코드 예제, 그리고 개발 시 고려해야 할 실전 팁까지, KMP를 통해 모바일 개발의 새로운 지평을 열고자 하는 분들을 위한 가이드가 될 것입니다.
Kotlin Multiplatform, 무엇이 다를까?
Kotlin Multiplatform은 "Share Code, Not UI"라는 독특한 철학을 가지고 있습니다. 기존의 React Native나 Flutter와 같은 크로스플랫폼 프레임워크들이 UI까지 포함한 모든 코드를 공유하는 것을 목표로 하는 반면, KMP는 비즈니스 로직, 데이터 모델, 네트워크 통신, 데이터베이스 접근 등 핵심 로직만을 공유하고, 사용자 인터페이스(UI)는 각 플랫폼의 네이티브 기술(iOS는 SwiftUI/UIKit, Android는 Jetpack Compose/XML)로 구현하는 방식을 취합니다.
이러한 접근 방식은 다음과 같은 중요한 이점을 제공합니다.
- 네이티브 UI/UX 보장: 각 플랫폼의 UI/UX 가이드라인을 완벽하게 따르면서 최상의 사용자 경험을 제공할 수 있습니다. 이는 프레임워크가 제공하는 커스텀 위젯에 의존하지 않고, 플랫폼 고유의 컴포넌트와 애니메이션을 활용할 수 있음을 의미합니다.
- 최적의 성능: UI 렌더링이 네이티브 방식으로 이루어지므로, 성능 측면에서 이점을 가집니다. 공유되는 비즈니스 로직 또한 Kotlin/Native를 통해 각 플랫폼의 바이너리로 컴파일되어 높은 성능을 보장합니다.
- 점진적 도입 가능: 기존에 이미 개발된 네이티브 앱에 KMP 모듈을 점진적으로 통합할 수 있습니다. 모든 것을 한 번에 전환할 필요 없이, 특정 기능부터 KMP로 개발하여 공유할 수 있습니다.
- 플랫폼별 특화 기능 활용:
expect/actual메커니즘을 통해 플랫폼별로 다른 API(예: 카메라, 센서, 특정 OS 기능)를 쉽게 통합하고 활용할 수 있습니다.
KMP는 Kotlin/JVM (Android 및 백엔드), Kotlin/JS (웹 프론트엔드), Kotlin/Native (iOS, macOS, Linux, Windows 등) 등 다양한 타겟으로 코드를 컴파일할 수 있는 유연성을 제공합니다. 모바일 개발의 맥락에서는 주로 Kotlin/Native를 사용하여 iOS 프레임워크를 생성하고, Kotlin/JVM을 사용하여 Android 라이브러리를 생성하는 방식으로 활용됩니다.
KMP의 핵심 구성 요소: expect/actual 메커니즘
KMP에서 가장 중요한 개념 중 하나는 expect/actual 메커니즘입니다. 이 메커니즘은 공유 코드(commonMain)에서 플랫폼별로 다른 구현이 필요한 기능을 선언(expect)하고, 각 플랫폼별 모듈(androidMain, iosMain)에서 해당 선언에 대한 실제 구현(actual)을 제공하도록 강제합니다. 이를 통해 플랫폼 독립적인 추상화와 플랫폼 종속적인 구현을 깔끔하게 분리할 수 있습니다.
예를 들어, 각 플랫폼의 고유한 이름을 가져오는 기능을 구현한다고 가정해 봅시다.
commonMain 모듈에 expect 선언:
// commonMain/kotlin/com/example/shared/Platform.kt
package com.example.shared
expect class Platform() {
val name: String
}
fun getPlatformName(): String = Platform().name
androidMain 모듈에 actual 구현:
// androidMain/kotlin/com/example/shared/Platform.kt
package com.example.shared
import android.os.Build
actual class Platform actual constructor() {
actual val name: String = "Android ${Build.VERSION.SDK_INT}"
}
iosMain 모듈에 actual 구현:
// iosMain/kotlin/com/example/shared/Platform.kt
package com.example.shared
import platform.UIKit.UIDevice
actual class Platform actual constructor() {
actual val name: String = UIDevice.currentDevice.systemName() + " " + UIDevice.currentDevice.systemVersion
}
이제 commonMain에서 정의된 getPlatformName() 함수를 호출하면, 각 플랫폼에서 컴파일될 때 해당 플랫폼의 actual 구현이 사용되어 올바른 플랫폼 이름을 반환하게 됩니다. 이처럼 expect/actual은 KMP가 플랫폼의 경계를 넘나들며 코드를 공유할 수 있게 하는 핵심적인 매커니즘입니다.
KMP 프로젝트 구조 이해하기
전형적인 KMP 모바일 프로젝트는 다음과 같은 Gradle 모듈 구조를 가집니다.
-
shared모듈: 핵심 비즈니스 로직을 포함하는 멀티플랫폼 라이브러리입니다.-
commonMain: 플랫폼 독립적인 코드 (데이터 모델, 인터페이스, 추상 클래스,expect선언 등) -
androidMain: Android 플랫폼에 특화된 코드 (actual구현, Android SDK 접근 등) -
iosMain: iOS 플랫폼에 특화된 코드 (actual구현, iOS SDK 접근 등) -
iosSimulatorArm64Main,iosX64Main등: iOS 아키텍처별 타겟 (Xcode 빌드 시 자동으로 통합됨)
-
-
androidApp모듈: Android 네이티브 애플리케이션 프로젝트.shared모듈을 의존성으로 추가합니다. -
iosApp모듈: iOS 네이티브 애플리케이션 프로젝트 (Xcode 프로젝트).shared모듈을 프레임워크 형태로 임베드합니다.
Gradle 설정(settings.gradle.kts 및 build.gradle.kts)을 통해 이러한 모듈 간의 의존성이 관리됩니다. shared 모듈은 Gradle의 kotlin-multiplatform 플러그인을 사용하여 설정되며, androidApp은 com.android.application, iosApp은 Xcode 프로젝트로 구성됩니다. shared 모듈은 Android에서는 일반적인 Gradle 라이브러리처럼, iOS에서는 .framework 또는 .xcframework 형태로 패키징되어 사용됩니다.
실전 KMP: 비즈니스 로직 공유
KMP의 가장 강력한 활용 사례는 복잡한 비즈니스 로직을 공유하는 것입니다. 여기에는 다음과 같은 요소들이 포함될 수 있습니다.
- 데이터 모델: 서버 API 응답을 위한 데이터 클래스 (
data class) - 네트워크 계층: REST API 호출 클라이언트 (Ktor,
kotlinx.serialization활용) - 데이터 저장 계층: 로컬 데이터베이스 또는 캐시 (SQLDelight, Realm 등)
- 비즈니스 로직: 유효성 검사, 데이터 처리, 특정 알고리즘 구현
- 상태 관리: MVI(Model-View-Intent)나 MVVM(Model-View-ViewModel) 패턴을 위한 ViewModel/Presenter 로직
예를 들어, Ktor 클라이언트를 사용하여 공유 네트워크 계층을 구축하는 방법을 살펴보겠습니다.
commonMain에 API 클라이언트 인터페이스 및 데이터 모델 정의:
// commonMain/kotlin/com/example/shared/data/model/User.kt
package com.example.shared.data.model
import kotlinx.serialization.Serializable
@Serializable
data class User(
val id: Int,
val name: String,
val email: String
)
// commonMain/kotlin/com/example/shared/data/api/UserApi.kt
package com.example.shared.data.api
import com.example.shared.data.model.User
interface UserApi {
suspend fun getUsers(): List<User>
suspend fun getUser(id: Int): User
}
commonMain에 Ktor 기반 구현 (플랫폼별 HTTP 엔진 expect/actual 사용):
// commonMain/kotlin/com/example/shared/data/api/KtorUserApi.kt
package com.example.shared.data.api
import com.example.shared.data.model.User
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.request.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.json.Json
// expect/actual로 플랫폼별 HttpClientEngine을 제공
expect fun getHttpClientEngine(): HttpClientEngine
class KtorUserApi : UserApi {
private val client = HttpClient(getHttpClientEngine()) {
install(ContentNegotiation) {
json(Json {
ignoreUnknownKeys = true
prettyPrint = true
isLenient = true
})
}
}
override suspend fun getUsers(): List<User> {
return client.get("https://jsonplaceholder.typicode.com/users").body()
}
override suspend fun getUser(id: Int): User {
return client.get("https://jsonplaceholder.typicode.com/users/$id").body()
}
}
androidMain에 Android용 HttpClientEngine 구현:
// androidMain/kotlin/com/example/shared/data/api/PlatformHttpClientEngine.kt
package com.example.shared.data.api
import io.ktor.client.engine.*
import io.ktor.client.engine.okhttp.*
actual fun getHttpClientEngine(): HttpClientEngine = OkHttp.create()
iosMain에 iOS용 HttpClientEngine 구현:
// iosMain/kotlin/com/example/shared/data/api/PlatformHttpClientEngine.kt
package com.example.shared.data.api
import io.ktor.client.engine.*
import io.ktor.client.engine.darwin.*
actual fun getHttpClientEngine(): HttpClientEngine = Darwin.create()
이제 KtorUserApi 인스턴스를 commonMain에서 생성하고, 이를 ViewModel이나 Presenter에서 사용하면 iOS와 Android 모두에서 동일한 네트워크 로직을 공유할 수 있습니다. kotlinx.coroutines를 사용하여 비동기 작업을 처리하며, 이는 iOS에서도 Swift의 async/await나 콜백으로 쉽게 통합될 수 있습니다.
iOS에서 KMP 모듈 사용하기
KMP shared 모듈을 iOS 애플리케이션에서 사용하는 방법은 주로 두 가지입니다.
- Xcode 프레임워크 임베딩: Gradle이 KMP 모듈을 iOS 프레임워크(
.framework또는.xcframework)로 빌드하고, Xcode 프로젝트에서 이 프레임워크를 직접 임베드하여 사용합니다. 가장 일반적인 방법입니다. - CocoaPods/Swift Package Manager (SPM): KMP 모듈을 CocoaPods 또는 SPM 패키지로 배포하고, Xcode에서 의존성 관리 도구를 통해 추가합니다.
프레임워크 임베딩 방식의 기본 흐름은 다음과 같습니다.
-
shared모듈의build.gradle.kts파일에 iOS 타겟을 정의하고,embedAndSign등의 설정을 추가하여 Xcode 프로젝트에 쉽게 통합될 수 있도록 합니다. - Xcode 프로젝트의 Build Phases에 Gradle 빌드 스크립트를 추가하여, 앱 빌드 시
shared모듈이 자동으로 빌드되고 프레임워크로 생성되도록 합니다. - 생성된 프레임워크를 Xcode 프로젝트에 링크하고, 필요한 경우
import문을 통해 Swift 코드에서 접근합니다.
Swift에서 KMP 공유 함수 호출 예제:
// iOS 앱의 ViewController.swift
import UIKit
import shared // shared 모듈 import
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// 코루틴 런처를 사용하여 비동기 함수 호출
// KMP에서 제공하는 헬퍼 함수를 통해 Swift의 async/await와 통합 가능
Task {
do {
let platformName = shared.getPlatformName()
print("Platform Name: \(platformName)") // "iOS 17.0"
let userApi = shared.KtorUserApi()
let users = try await userApi.getUsers().await() // .await()는 KMP 코루틴을 Swift async/await로 변환하는 헬퍼
print("Fetched Users: \(users)")
let user = try await userApi.getUser(id: 1).await()
print("Fetched User: \(user.name)")
} catch {
print("Error fetching data: \(error)")
}
}
}
}
KMP는 Kotlin 코루틴을 사용하여 비동기 처리를 수행합니다. iOS (Swift)에서 이 코루틴 기반의 비동기 함수를 호출하기 위해서는 shared 모듈 내에 Swift에서 사용하기 편리한 헬퍼 함수(예: createCoroutineScope, asyncFunction().asFlow().collectAsNativeFlow(), asyncFunction().await())를 추가하는 것이 일반적입니다. 이를 통해 Swift의 async/await나 Combine과 자연스럽게 연동할 수 있습니다.
KMP 개발 시 고려사항 및 팁
KMP는 강력하지만, 성공적인 도입을 위해서는 몇 가지 고려사항과 팁을 알아두는 것이 좋습니다.
- UI 통합 전략: KMP는 기본적으로 UI를 공유하지 않습니다. Android는 Jetpack Compose, iOS는 SwiftUI/UIKit을 사용하여 네이티브 UI를 개발해야 합니다. 최근 Compose Multiplatform이 모바일 타겟을 지원하기 시작했지만, 아직은 Android와 Desktop/Web에 더 초점이 맞춰져 있으며 iOS 지원은 활발히 개선 중입니다. 현재로서는 모바일 앱 개발 시 네이티브 UI를 유지하는 것이 일반적입니다.
- 상태 관리 라이브러리: 공유 로직에서 MVVM 또는 MVI 패턴을 적용할 때, KMP를 지원하는 상태 관리 라이브러리를 활용하면 좋습니다.
MVIKotlin,Decompose,KaMPKit등이 대표적이며, Kotlin Flow를 직접 사용하는 것도 좋은 방법입니다. - 테스팅:
commonMain에 작성된 비즈니스 로직은 JUnit 기반의 테스트 코드를 통해 쉽게 테스트할 수 있습니다. 이는 iOS와 Android 양쪽에서 동일한 로직이 올바르게 동작함을 보장하는 데 매우 중요합니다.expect/actual을 사용하는 경우, 각 플랫폼별actual구현도 테스트해야 합니다. - 빌드 시스템: Gradle에 대한 이해가 필요합니다. 특히
build.gradle.kts파일을 사용하여 멀티플랫폼 타겟을 설정하고, iOS 프레임워크를 빌드하며, Cocoapods/SPM 통합을 관리하는 방법을 익혀야 합니다. - 학습 곡선: Kotlin 언어에 대한 이해는 기본이며, Android 개발자는 iOS 개발 환경 (Xcode, Swift)에 대한 기본적인 지식이, iOS 개발자는 Android 개발 환경 (Gradle, Kotlin)에 대한 기본적인 지식이 필요할 수 있습니다. 특히 Swift와 Kotlin 간의 상호 운용성(interoperability)을 이해하는 것이 중요합니다.
- 생태계 및 커뮤니티: KMP는 React Native나 Flutter에 비해 상대적으로 젊은 기술 스택입니다. 따라서 라이브러리 생태계나 커뮤니티 지원이 아직은 부족하게 느껴질 수 있습니다. 하지만 JetBrains의 적극적인 지원과 빠른 커뮤니티 성장을 통해 빠르게 발전하고 있습니다.
다른 크로스플랫폼 프레임워크와의 비교 (React Native, Flutter)
KMP는 기존의 크로스플랫폼 프레임워크들과 다른 독특한 포지션을 가집니다. 다음 표를 통해 주요 특징을 비교해 볼 수 있습니다.
| 특징 | Kotlin Multiplatform (KMP) | React Native | Flutter |
|---|---|---|---|
| 코드 공유 | 비즈니스 로직 (Kotlin) | UI 및 비즈니스 로직 (JavaScript/TypeScript) | UI 및 비즈니스 로직 (Dart) |
| UI 공유 | 없음 (네이티브 UI) | 있음 (네이티브 컴포넌트 브릿징) | 있음 (자체 렌더링 엔진) |
| 성능 | 네이티브 UI + Kotlin/Native (매우 높음) | 브릿지를 통한 네이티브 접근 (높음) | 자체 렌더링 엔진 (높음) |
| 네이티브 접근 | expect/actual로 완벽한 네이티브 API 접근 가능 | 브릿지 모듈을 통해 네이티브 API 접근 가능 | 플랫폼 채널을 통해 네이티브 API 접근 가능 |
| 언어 | Kotlin | JavaScript / TypeScript | Dart |
| 학습 곡선 | Kotlin + 각 플랫폼 네이티브 지식 필요 | JavaScript + React 지식 필요 | Dart + Flutter 위젯 지식 필요 |
| 생태계 | 성장 중, JetBrains 적극 지원 | 성숙함, 방대한 라이브러리 | 빠르게 성장 중, Google 적극 지원 |
| 주요 장점 | 네이티브 UI/UX 보장, 점진적 도입, 고성능 | 빠른 개발 속도, 웹 개발자에게 친숙 | 아름다운 UI, 높은 생산성, 단일 코드베이스 |
| 주요 단점 | UI 코드 분리, 초기 설정 복잡, 생태계 성숙도 | 브릿지 오버헤드, 네이티브 모듈 개발 필요 | Dart 언어 학습, 앱 크기, 특정 플랫폼 제약 |
KMP는 네이티브 UI의 장점을 포기할 수 없지만, 비즈니스 로직의 중복을 줄이고 싶은 프로젝트에 이상적인 선택입니다. 특히 기존 네이티브 앱에 크로스플랫폼 기능을 점진적으로 도입하거나, 성능이 중요한 복잡한 로직을 공유해야 하는 경우에 큰 강점을 발휘합니다.
마무리
Kotlin Multiplatform은 모바일 개발의 효율성을 극대화하면서도 네이티브 앱의 장점을 유지할 수 있는 강력한 대안입니다. "Share Code, Not UI"라는 독특한 접근 방식을 통해, 개발자들은 iOS와 Android 앱에서 핵심 비즈니스 로직을 단일 코드베이스로 관리하고, 각 플랫폼의 최신 UI 기술을 활용하여 사용자에게 최상의 경험을 제공할 수 있습니다.
아직 발전하고 있는 기술이지만, KMP는 이미 많은 기업과 프로젝트에서 성공적으로 도입되고 있으며, JetBrains의 지속적인 투자와 커뮤니티의 성장을 통해 그 잠재력을 계속해서 확장해 나가고 있습니다. 이 글에서 다룬 KMP의 핵심 개념과 실전 팁들이 여러분의 크로스플랫폼 모바일 개발 여정에 도움이 되기를 바랍니다.
관련 게시글
Kotlin Multiplatform Mobile (KMM) Cross-Platform Development Deep Dive
Kotlin Multiplatform Mobile (KMM)을 활용한 크로스플랫폼 앱 개발의 모든 것! iOS, Android 네이티브 UI와 공유 로직의 강력한 조합으로 효율적인 모바일 개발을 경험하세요.
Kotlin Multiplatform으로 Cross-Platform 모바일 개발 가속화하기
Kotlin Multiplatform (KMP)을 활용한 크로스플랫폼 모바일 앱 개발 전략을 심층적으로 다룹니다. iOS와 Android에서 코드 재사용을 극대화하고, 네이티브 UI와 성능을 유지하는 방법을 알아보세요.
Kotlin Multiplatform (KMP): 크로스플랫폼 모바일 개발 심층 가이드
Kotlin Multiplatform (KMP)을 활용한 크로스플랫폼 모바일 앱 개발 가이드입니다. React Native, Flutter와 비교하며 KMP의 장점, 아키텍처, 실전 팁 및 코드 예제를 상세히 다룹니다.