Kotlin Multiplatform Mobile (KMM) Cross-Platform Development Deep Dive
Kotlin Multiplatform Mobile (KMM)을 활용한 크로스플랫폼 앱 개발의 모든 것! iOS, Android 네이티브 UI와 공유 로직의 강력한 조합으로 효율적인 모바일 개발을 경험하세요.
Kotlin Multiplatform Mobile (KMM) Cross-Platform Development Deep Dive
모바일 앱 개발은 iOS와 Android라는 두 개의 거대한 플랫폼을 중심으로 이루어집니다. 각 플랫폼은 고유의 언어(Swift/Objective-C, Kotlin/Java)와 프레임워크를 사용하여 앱을 구축하며, 이는 종종 비즈니스 로직의 중복 구현으로 이어져 개발 시간과 유지보수 비용을 증가시킵니다. 이러한 비효율성을 해결하기 위해 등장한 크로스플랫폼 개발 솔루션 중 하나인 Kotlin Multiplatform Mobile (KMM)은 개발자들이 공유된 코어 로직을 통해 iOS와 Android 앱을 동시에 구축할 수 있도록 돕습니다. 이 글에서는 KMM의 아키텍처, 장점, 실전 팁 및 코드 예제를 통해 KMM이 어떻게 모바일 개발의 새로운 지평을 열고 있는지 심층적으로 살펴보겠습니다.
KMM, 왜 주목해야 할까요?
모바일 앱 시장이 성장하면서, 많은 기업들이 iOS와 Android 양쪽 플랫폼에 앱을 출시하길 원합니다. 하지만 각 플랫폼에 대해 별도의 팀과 코드를 유지하는 것은 상당한 비용과 노력을 요구합니다. 이러한 문제를 해결하기 위해 React Native, Flutter와 같은 크로스플랫폼 프레임워크들이 등장하여 단일 코드베이스로 여러 플랫폼의 UI까지 공유하는 접근 방식을 제시했습니다.
그러나 KMM은 이들과는 다른 독특한 접근 방식을 취합니다. KMM은 UI를 포함한 앱의 모든 부분을 공유하는 대신, 비즈니스 로직, 데이터 모델, 네트워킹, 데이터베이스 처리 등 핵심적인 비즈니스 로직만 Kotlin으로 공유합니다. UI는 각 플랫폼의 네이티브 기술(iOS의 SwiftUI/UIKit, Android의 Jetpack Compose/View 시스템)을 사용하여 구축합니다.
이러한 접근 방식은 다음과 같은 강력한 이점을 제공합니다.
- 네이티브 UI/UX: 각 플랫폼의 고유한 디자인 가이드라인과 성능 최적화를 따르므로 사용자에게 완벽한 네이티브 경험을 제공합니다.
- 코드 재사용: 가장 복잡하고 버그가 발생하기 쉬운 비즈니스 로직을 한 번만 구현하고, iOS와 Android에서 재사용하여 개발 시간을 단축하고 버그 발생 가능성을 줄입니다.
- 유지보수 용이성: 공유 로직의 변경 사항은 한 곳에서만 관리하면 되므로 유지보수 효율성이 극대화됩니다.
- 점진적 도입: 기존 네이티브 프로젝트에 KMM 모듈을 점진적으로 통합할 수 있어 위험 부담이 적습니다.
- Kotlin의 강점: 간결하고 안전하며 생산적인 Kotlin 언어의 모든 이점을 활용할 수 있습니다.
다른 크로스플랫폼 프레임워크들이 '모든 것을 공유'하는 데 초점을 맞춘다면, KMM은 '필요한 것을 공유'하여 네이티브 앱의 장점을 유지하면서 효율성을 극대화하는 데 주력합니다.
Kotlin Multiplatform Mobile (KMM) 아키텍처 이해하기
KMM 프로젝트는 기본적으로 세 가지 모듈로 구성됩니다. 이 모듈들은 논리적으로 분리되어 있지만, 서로 유기적으로 연결되어 동작합니다.
-
commonMain:- iOS와 Android 모두에서 공유되는 코드를 포함합니다.
- 비즈니스 로직, 데이터 모델(DTO), 네트워킹 인터페이스, 데이터베이스 스키마 정의, 유틸리티 함수 등 플랫폼에 독립적인 모든 코드가 여기에 위치합니다.
-
expect/actual메커니즘을 사용하여 플랫폼별 구현이 필요한 기능을 추상화합니다.
-
androidMain:- Android 플랫폼에 특화된 코드를 포함합니다.
-
commonMain에서expect로 선언된 기능에 대한actual구현을 제공합니다. - Android UI 코드(Activity, Fragment, Composable 등)와 Android 전용 API 호출 로직이 여기에 위치합니다.
-
commonMain모듈에 정의된 공유 로직을 Android 앱에서 사용합니다.
-
iosMain:- iOS 플랫폼에 특화된 코드를 포함합니다.
-
commonMain에서expect로 선언된 기능에 대한actual구현을 Swift 또는 Objective-C가 호출할 수 있는 Kotlin 코드로 제공합니다. - iOS UI 코드(ViewController, View 등)와 iOS 전용 API 호출 로직이 여기에 위치합니다.
-
commonMain모듈은 컴파일되어 iOS 프레임워크(또는 XCFramework) 형태로 제공되며, Swift/Objective-C 코드에서 이 프레임워크를 임포트하여 공유 로직을 사용합니다.
이러한 아키텍처의 핵심은 expect/actual 메커니즘입니다. commonMain에서 특정 기능이 플랫폼에 따라 다르게 구현되어야 함을 expect 키워드로 선언하면, androidMain과 iosMain에서 각각 actual 키워드를 사용하여 해당 기능의 플랫폼별 구현을 제공합니다. 예를 들어, 플랫폼별 시스템 정보를 가져오는 함수를 구현할 때 유용합니다.
// commonMain/src/commonMain/kotlin/com/example/shared/Platform.kt
package com.example.shared
expect class Platform() {
val name: String
}
fun getPlatformName(): String = Platform().name
// androidMain/src/androidMain/kotlin/com/example/shared/Platform.kt
package com.example.shared
actual class Platform actual constructor() {
actual val name: String = "Android ${android.os.Build.VERSION.SDK_INT}"
}
// iosMain/src/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
}
이렇게 정의된 getPlatformName() 함수는 commonMain에서 호출 가능하며, Android 앱에서는 Android 시스템 정보를, iOS 앱에서는 iOS 시스템 정보를 반환하게 됩니다.
KMM 프로젝트 시작하기: 기본적인 설정 및 코드 예제
KMM 프로젝트를 시작하는 가장 쉬운 방법은 Android Studio에 KMM 플러그인을 설치하고 프로젝트 위자드를 사용하는 것입니다. 위자드는 commonMain, androidMain, iosMain을 포함하는 기본적인 프로젝트 구조를 자동으로 생성해 줍니다.
새로운 KMM 프로젝트를 생성한 후, commonMain 모듈의 build.gradle.kts 파일을 살펴보면 다음과 같은 멀티플랫폼 설정이 되어 있는 것을 볼 수 있습니다.
// commonMain/build.gradle.kts
plugins {
kotlin("multiplatform")
kotlin("native.cocoapods") // iOS 프레임워크 생성을 위한 플러그인
id("com.android.library") // Android 라이브러리 모듈로 빌드하기 위함
}
kotlin {
androidTarget {
// Android specific configuration
}
iosX64()
iosArm64()
iosSimulatorArm64()
sourceSets {
val commonMain by getting {
dependencies {
// 여기에 공유 로직에 필요한 의존성을 추가합니다.
// 예: Ktor, SQLDelight, Coroutines 등
}
}
val androidMain by getting {
dependencies {
// Android 특정 의존성
}
}
val iosMain by getting {
dependencies {
// iOS 특정 의존성
}
}
}
}
android {
// Android build configuration
}
이제 위에서 정의한 getPlatformName() 함수를 각 플랫폼 앱에서 어떻게 사용하는지 살펴보겠습니다.
Android 앱에서 공유 로직 사용하기:
Android 모듈은 commonMain 모듈에 직접 의존하므로, commonMain에 정의된 함수를 일반 Kotlin 함수처럼 호출할 수 있습니다.
// androidApp/src/main/java/com/example/myapplication/MainActivity.kt
package com.example.myapplication
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.material3.Text
import com.example.shared.getPlatformName // commonMain에서 정의된 함수 임포트
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Text(text = "Hello from ${getPlatformName()}")
}
}
}
iOS 앱에서 공유 로직 사용하기:
iOS 앱에서는 commonMain이 컴파일된 프레임워크를 임포트하여 사용합니다. build.gradle.kts 파일에 cocoapods 플러그인이 설정되어 있다면, Xcode 프로젝트에서 KMM 모듈을 Pod으로 추가하여 사용할 수 있습니다.
// iosApp/iosApp/ContentView.swift
import SwiftUI
import shared // KMM 모듈이 컴파일된 프레임워크 임포트
struct ContentView: View {
var body: some View {
Text("Hello from \(SharedKt.getPlatformName())") // Kotlin 함수 호출
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
여기서 SharedKt는 Kotlin 파일 shared.kt (패키지명 shared)에서 정의된 최상위 함수들을 Swift에서 호출하기 위한 래퍼 클래스입니다.
네트워킹 및 데이터 관리: Ktor와 SQLDelight 활용
실제 모바일 앱은 대부분 네트워크 통신을 통해 데이터를 가져오고, 로컬에 데이터를 저장해야 합니다. KMM 환경에서는 이러한 작업을 멀티플랫폼 라이브러리를 통해 commonMain에서 처리할 수 있습니다.
Ktor Client를 이용한 멀티플랫폼 네트워킹
Ktor Client는 HTTP 클라이언트 라이브러리로, KMM 프로젝트에서 네트워크 요청을 commonMain에 구현할 수 있도록 지원합니다.
// commonMain/src/commonMain/kotlin/com/example/shared/GreetingService.kt
package com.example.shared
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.Serializable
import kotlinx.serialization.json.Json
@Serializable
data class Greeting(val message: String)
class GreetingService {
private val httpClient = HttpClient {
install(ContentNegotiation) {
json(Json {
ignoreUnknownKeys = true
useAlternativeNames = false
})
}
}
suspend fun getGreeting(): Greeting {
return httpClient.get("https://api.example.com/greeting").body()
}
}
이 GreetingService는 commonMain에 정의되어 있으므로, Android와 iOS UI 코드에서 동일하게 GreetingService().getGreeting()을 호출하여 네트워크 요청을 수행할 수 있습니다. Ktor는 각 플랫폼에 맞는 HTTP 엔진을 자동으로 사용합니다.
SQLDelight를 이용한 멀티플랫폼 데이터베이스
SQLDelight는 SQL 쿼리를 Kotlin 코드로 변환하여 타입 세이프한 데이터베이스 접근을 가능하게 하는 라이브러리입니다. commonMain에서 데이터베이스 스키마를 정의하고 쿼리를 작성하면, SQLDelight가 각 플랫폼에 맞는 코드를 생성해 줍니다.
먼저, commonMain의 build.gradle.kts에 SQLDelight 플러그인과 의존성을 추가합니다.
// commonMain/build.gradle.kts
plugins {
// ...
id("app.cash.sqldelight") version "2.0.0" // SQLDelight 플러그인
}
kotlin {
// ...
sourceSets {
val commonMain by getting {
dependencies {
// ...
implementation("app.cash.sqldelight:runtime:2.0.0")
}
}
val androidMain by getting {
dependencies {
implementation("app.cash.sqldelight:android-driver:2.0.0")
}
}
val iosMain by getting {
dependencies {
implementation("app.cash.sqldelight:native-driver:2.0.0")
}
}
}
}
sqldelight {
databases {
create("AppDatabase") { // 데이터베이스 이름
packageName.set("com.example.shared")
}
}
}
그리고 commonMain의 sqldelight 디렉터리에 .sq 파일을 생성하여 스키마와 쿼리를 정의합니다.
-- commonMain/sqldelight/com/example/shared/AppDatabase.sq
CREATE TABLE User (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT NOT NULL
);
insertUser:
INSERT INTO User(name, email) VALUES (?, ?);
selectAllUsers:
SELECT * FROM User;
이렇게 하면 SQLDelight가 AppDatabase와 같은 Kotlin 코드를 생성하여 commonMain에서 타입 세이프하게 데이터베이스에 접근할 수 있게 해줍니다. 실제 드라이버는 expect/actual을 통해 각 플랫폼에서 구현됩니다.
KMP/KMM 개발 실전 팁
KMM 프로젝트를 성공적으로 개발하기 위한 몇 가지 실전 팁을 공유합니다.
1. UI는 네이티브로 유지하기
KMM의 가장 큰 강점은 비즈니스 로직 공유와 네이티브 UI의 조합입니다. Jetpack Compose Multiplatform(JCM)과 같은 시도도 있지만, 아직까지는 각 플랫폼의 네이티브 UI 기술(SwiftUI/UIKit, Jetpack Compose/View System)을 사용하는 것이 가장 안정적이고 성능이 뛰어납니다. KMM은 UI 코드를 공유하는 것이 목적이 아님을 명심해야 합니다.
2. 의존성 관리의 효율화
commonMain의 build.gradle.kts 파일에서 sourceSets 블록을 통해 각 플랫폼에 필요한 의존성을 명확히 분리하여 관리할 수 있습니다. implementation, api 등의 스코프를 적절히 활용하고, 버전 관리는 gradle/libs.versions.toml 파일을 사용하여 중앙에서 관리하는 것이 좋습니다.
3. 테스트 전략: commonTest의 중요성
공유 로직은 앱의 핵심이므로 철저한 테스트가 필요합니다. commonMain에 해당하는 테스트 코드는 commonTest 디렉터리에 작성하여 iOS와 Android 양쪽에서 모두 실행될 수 있도록 합니다. 이는 코드 변경 시 모든 플랫폼에서의 동작을 보장하는 데 필수적입니다.
4. CI/CD 통합
KMM 프로젝트는 Android 앱과 iOS 프레임워크를 동시에 빌드해야 하므로, CI/CD 파이프라인 구축이 더욱 중요합니다. GitHub Actions, GitLab CI/CD, Bitrise 등 다양한 CI/CD 툴을 활용하여 코드 변경 시 자동으로 테스트를 실행하고, 각 플랫폼의 빌드 아티팩트(Android APK/AAB, iOS 프레임워크/앱)를 생성하도록 자동화해야 합니다. 특히 iOS 빌드를 위해서는 macOS 환경이 필요함을 기억하세요.
5. Swift/Objective-C와의 상호운용성 고려
iOS 개발 시 Kotlin 코드와 Swift/Objective-C 코드 간의 원활한 상호운용성을 이해하는 것이 중요합니다. Kotlin에서 nullability를 명확히 선언(?, !!)하면 Swift에서 Optional 타입으로 매핑되어 안전하게 사용할 수 있습니다. 또한, Kotlin의 코루틴은 Swift의 async/await와 직접적으로 호환되지 않으므로, Flow를 Combine이나 async/await로 변환하는 래퍼를 만드는 등의 전략이 필요할 수 있습니다.
다른 크로스플랫폼 프레임워크와의 비교
KMM의 독특한 포지셔닝을 이해하기 위해 React Native, Flutter와 비교해 보겠습니다.
| 특징 | Kotlin Multiplatform Mobile (KMM) | React Native | Flutter |
|---|---|---|---|
| 공유 대상 | 비즈니스 로직, 데이터 모델, 네트워킹 등 핵심 로직 | UI & 로직 (JavaScript) | UI & 로직 (Dart) |
| UI 구현 | 네이티브 UI (SwiftUI/UIKit, Jetpack Compose/View System) | 네이티브 컴포넌트 렌더링 (Bridge 통신) | 자체 렌더링 엔진 (Skia) |
| 성능 | 네이티브에 준하는 성능 (UI는 네이티브) | JavaScript 브리지로 인한 오버헤드 가능 | 자체 렌더링으로 높은 성능, 그러나 네이티브와는 다소 차이 |
| 개발 언어 | Kotlin (공유 로직), Swift/Objective-C (iOS UI), Kotlin/Java (Android UI) | JavaScript / TypeScript | Dart |
| 네이티브 기능 접근 | Kotlin에서 expect/actual로 직접 접근, 네이티브 UI에서 직접 접근 | 브리지를 통해 네이티브 모듈 호출 | 플러그인을 통해 네이티브 모듈 호출 |
| 생태계 | Kotlin/JVM, iOS/Android 네이티브 생태계 활용 | JavaScript 생태계 (npm), React Native 전용 라이브러리 | Dart 생태계 (pub.dev), Flutter 전용 위젯/라이브러리 |
| 학습 곡선 | Kotlin, iOS/Android 네이티브 개발 지식 필요 | JavaScript/React 지식 필요, 네이티브 모듈 개발 시 추가 학습 | Dart/Flutter 프레임워크 지식 필요, 네이티브 모듈 개발 시 추가 학습 |
| 적합한 상황 | 네이티브 UI/UX 품질이 최우선이며, 로직 재사용이 중요한 경우 | 빠른 프로토타이핑, 웹 개발자에게 익숙한 환경, UI 공유가 중요한 경우 | 일관된 UI/UX가 중요하며, 높은 성능의 애니메이션/커스텀 UI가 필요한 경우 |
이 표에서 볼 수 있듯이 KMM은 네이티브 앱의 장점을 최대한 유지하면서 코드 재사용을 통한 효율성을 추구하는 독특한 위치를 차지합니다.
마무리
Kotlin Multiplatform Mobile (KMM)은 iOS와 Android 모바일 개발의 패러다임을 변화시킬 잠재력을 가진 강력한 기술입니다. 비즈니스 로직을 Kotlin으로 공유하고 UI는 각 플랫폼의 네이티브 기술을 사용하여 구축함으로써, 개발자는 네이티브 앱의 뛰어난 성능과 사용자 경험을 유지하면서도 개발 효율성을 극대화할 수 있습니다. KMM은 기존 네이티브 프로젝트에 점진적으로 도입할 수 있다는 장점과 함께, 견고한 공유 로직으로 유지보수를 용이하게 하여 장기적인 관점에서 개발 비용을 절감하는 데 기여합니다. 앞으로 KMM 생태계가 더욱 성숙해짐에 따라, 더 많은 기업과 개발자들이 KMM을 통해 혁신적인 모바일 앱을 선보일 것으로 기대됩니다.
관련 게시글
Kotlin Multiplatform (KMP)로 크로스플랫폼 모바일 앱 개발하기: 실전 가이드
Kotlin Multiplatform (KMP)를 활용한 크로스플랫폼 모바일 앱 개발 가이드입니다. React Native, Flutter와 차별화되는 KMP의 비즈니스 로직 공유 방식과 iOS, Android 통합 전략을 심층적으로 다룹니다.
Kotlin Multiplatform으로 Cross-Platform 모바일 개발 가속화하기
Kotlin Multiplatform (KMP)을 활용한 크로스플랫폼 모바일 앱 개발 전략을 심층적으로 다룹니다. iOS와 Android에서 코드 재사용을 극대화하고, 네이티브 UI와 성능을 유지하는 방법을 알아보세요.
Kotlin Multiplatform (KMP): 크로스플랫폼 모바일 개발 심층 가이드
Kotlin Multiplatform (KMP)을 활용한 크로스플랫폼 모바일 앱 개발 가이드입니다. React Native, Flutter와 비교하며 KMP의 장점, 아키텍처, 실전 팁 및 코드 예제를 상세히 다룹니다.