Kotlin Multiplatform으로 Cross-Platform 모바일 개발 가속화하기
Kotlin Multiplatform (KMP)을 활용한 크로스플랫폼 모바일 앱 개발 전략을 심층적으로 다룹니다. iOS와 Android에서 코드 재사용을 극대화하고, 네이티브 UI와 성능을 유지하는 방법을 알아보세요.
Kotlin Multiplatform으로 Cross-Platform 모바일 개발 가속화하기
오늘날 모바일 애플리케이션 개발은 iOS와 Android라는 두 가지 주요 플랫폼을 동시에 지원해야 하는 복잡한 과제에 직면해 있습니다. 이로 인해 개발 비용과 시간이 증가하고, 플랫폼 간의 기능 불일치 문제가 발생하기도 합니다. 이러한 문제를 해결하기 위해 등장한 다양한 크로스플랫폼 솔루션 중, Kotlin Multiplatform (KMP)은 특히 비즈니스 로직의 코드 재사용에 초점을 맞춰 네이티브 경험을 유지하면서 개발 효율성을 극대화하는 강력한 대안으로 떠오르고 있습니다. 이 글에서는 KMP의 핵심 개념부터 실전 적용 팁, 그리고 다른 크로스플랫폼 프레임워크와의 비교를 통해 KMP가 모바일 개발에 어떤 변화를 가져올 수 있는지 심층적으로 살펴보겠습니다.
Kotlin Multiplatform이란 무엇인가요?
Kotlin Multiplatform은 JetBrains에서 개발한 오픈소스 기술로, Kotlin 언어를 사용하여 iOS, Android, 웹, 데스크톱 등 다양한 플랫폼에서 코드를 공유할 수 있도록 지원합니다. 특히 모바일 개발에서는 iOS와 Android 앱에서 비즈니스 로직, 데이터 모델, 네트워크 계층 등 핵심 로직을 Kotlin으로 한 번만 작성하고, 각 플랫폼의 네이티브 UI 프레임워크(iOS의 SwiftUI/UIKit, Android의 Jetpack Compose/View 시스템)와 통합하여 사용하는 방식이 주를 이룹니다.
KMP의 핵심 아이디어는 "Shared Code"와 "Platform-Specific Code"의 분리입니다. 대부분의 비즈니스 로직은 Shared Code로 작성되어 모든 플랫폼에서 재사용되며, 플랫폼별로 특화된 기능(예: 특정 OS API 접근, UI 렌더링)은 Platform-Specific Code로 구현됩니다. 이를 통해 개발자는 각 플랫폼의 강점을 최대한 활용하면서도 상당한 양의 코드를 공유하여 개발 시간과 유지보수 비용을 절감할 수 있습니다.
KMP의 장점과 단점
모든 기술이 그렇듯, KMP 또한 명확한 장점과 고려해야 할 단점을 가지고 있습니다.
장점
- 코드 재사용성 극대화: 비즈니스 로직, 데이터 모델, 네트워크 레이어, 유틸리티 등 핵심 로직을 80% 이상 공유하여 개발 시간과 비용을 크게 절감할 수 있습니다.
- 네이티브 성능 및 UI 경험: KMP는 UI를 공유하지 않고 각 플랫폼의 네이티브 UI 프레임워크를 사용하므로, 사용자에게 완전히 네이티브와 동일한 성능과 UI/UX 경험을 제공합니다. 이는 React Native나 Flutter와 같은 UI 공유 방식의 크로스플랫폼 프레임워크와 차별화되는 지점입니다.
- Kotlin 언어의 이점: 간결하고 안전하며 생산적인 Kotlin 언어의 모든 장점을 활용할 수 있습니다. Android 개발자에게는 익숙하며, iOS 개발자에게도 비교적 학습 곡선이 낮은 편입니다.
- 점진적 도입 가능: 기존 iOS 및 Android 프로젝트에 KMP 모듈을 점진적으로 통합할 수 있습니다. 처음부터 모든 것을 KMP로 전환할 필요 없이, 필요한 부분부터 적용하여 위험 부담을 줄일 수 있습니다.
- 플랫폼 API 접근 용이성:
expect/actual메커니즘을 통해 플랫폼별 API에 쉽게 접근할 수 있습니다. 이는 크로스플랫폼 개발에서 특정 OS 기능이 필요할 때 큰 강점이 됩니다.
단점
- 학습 곡선: 기존 네이티브 개발자에게는 새로운 빌드 시스템(Gradle Multiplatform)과
expect/actual패턴, Kotlin/Native 컴파일 과정 등을 이해해야 하는 학습 곡선이 존재합니다. - 생태계 및 도구의 성숙도: React Native나 Flutter에 비해 KMP의 생태계는 아직 성장 중입니다. 라이브러리, 도구, 커뮤니티 지원 등에서 부족함을 느낄 수 있습니다. (그러나 빠르게 발전하고 있습니다.)
- 빌드 시스템의 복잡성: Gradle Multiplatform 설정은 단일 플랫폼 프로젝트보다 복잡할 수 있으며, 특히 iOS 빌드 시스템(Xcode)과의 연동 과정에서 트러블슈팅이 필요할 수 있습니다.
- UI 공유의 부재: KMP는 기본적으로 UI를 공유하지 않습니다. 만약 UI까지 공유하고 싶다면 Compose Multiplatform을 고려해야 하지만, 이는 아직 iOS에서 안정화 단계가 아닙니다.
- iOS 개발 지식 필요: 공유 로직을 Kotlin으로 작성하더라도, iOS 앱을 빌드하고 배포하려면 여전히 Swift/Objective-C 및 Xcode에 대한 이해가 필수적입니다.
KMP와 다른 크로스플랫폼 프레임워크 비교
KMP는 React Native, Flutter와 같은 다른 크로스플랫폼 프레임워크와 접근 방식에서 중요한 차이를 보입니다. 다음 표는 주요 특징을 비교한 것입니다.
| 특징 | Kotlin Multiplatform (KMP) | Flutter | React Native |
|---|---|---|---|
| 주요 목표 | 비즈니스 로직 공유, 네이티브 UI 유지 | UI 및 로직 공유, 자체 렌더링 엔진 | UI 및 로직 공유, 네이티브 컴포넌트 브릿징 |
| UI 처리 | 각 플랫폼의 네이티브 UI (SwiftUI/UIKit, Jetpack Compose) | 자체 렌더링 엔진 (Skia), 위젯 기반 UI | JavaScript를 통한 네이티브 UI 컴포넌트 렌더링 |
| 사용 언어 | Kotlin (공유 로직), Swift/Objective-C, Java/Kotlin (UI) | Dart | JavaScript/TypeScript |
| 성능 | 네이티브에 준하는 성능 (공유 로직은 컴파일된 바이너리) | 네이티브에 준하는 성능 (AOT 컴파일) | 브릿지를 통한 통신 오버헤드 존재, JIT 컴파일 |
| 생태계 | 성장 중, Kotlin 라이브러리 활용 가능 | 매우 활발하고 성숙함 | 매우 활발하고 성숙함 |
| 장점 | 네이티브 UX/성능, 점진적 도입, Kotlin 언어 | 빠른 개발 속도, 아름다운 UI, 단일 코드베이스 | 웹 개발자 친숙, 넓은 커뮤니티, 다양한 라이브러리 |
| 단점 | UI 공유 불가 (기본), 학습 곡선, 생태계 성숙도 | 네이티브 UI와의 이질감 가능, 앱 크기, Dart 언어 | 브릿지 성능 이슈, 네이티브 모듈 개발 필요, JS 번들 크기 |
KMP는 "Write Once, Run Anywhere"보다는 "Write Once, Share Anywhere"에 가깝습니다. 즉, UI를 포함한 모든 것을 한 번에 개발하는 것보다, 핵심 로직을 공유하고 UI는 각 플랫폼에 최적화하여 개발하는 전략을 추구합니다.
KMP 아키텍처 및 모듈 구성
KMP 프로젝트는 일반적으로 다음과 같은 모듈로 구성됩니다.
-
commonMain: 플랫폼에 독립적인 코드를 작성하는 곳입니다. 비즈니스 로직, 데이터 모델, 인터페이스(expect) 등이 여기에 정의됩니다. -
androidMain: Android 플랫폼에 특화된 코드를 작성하는 곳입니다.commonMain에서 정의된expect인터페이스의 Androidactual구현, Android 특정 라이브러리 사용 등이 포함됩니다. -
iosMain: iOS 플랫폼에 특화된 코드를 작성하는 곳입니다.commonMain에서 정의된expect인터페이스의 iOSactual구현, iOS 특정 라이브러리 사용 등이 포함됩니다.
이러한 모듈 구조는 Gradle Multiplatform Plugin에 의해 관리됩니다. 특히 expect와 actual 키워드는 KMP의 핵심 기능 중 하나로, 플랫폼별 종속성을 추상화하고 공유 코드에서 플랫폼별 구현을 호출할 수 있게 해줍니다.
expect / actual 메커니즘: expect는 commonMain에서 인터페이스나 클래스의 선언을 정의합니다. 실제 구현은 각 플랫폼 모듈(예: androidMain, iosMain)에서 actual 키워드를 사용하여 제공합니다.
// commonMain/kotlin/Platform.kt
package com.example.kmpapp
expect class Platform() {
val name: String
fun getOsVersion(): String
}
// androidMain/kotlin/Platform.kt
package com.example.kmpapp
import android.os.Build
actual class Platform actual constructor() {
actual val name: String = "Android"
actual fun getOsVersion(): String {
return "Android ${Build.VERSION.SDK_INT}"
}
}
// iosMain/kotlin/Platform.kt
package com.example.kmpapp
import platform.UIKit.UIDevice
actual class Platform actual constructor() {
actual val name: String = "iOS"
actual fun getOsVersion(): String {
return "iOS ${UIDevice.currentDevice.systemVersion}"
}
}
이 예제에서 Platform 클래스는 commonMain에서 expect로 선언되었고, androidMain과 iosMain에서 각각 actual 구현을 제공합니다. commonMain에서 이 Platform 클래스를 사용하면, 빌드 시점에 각 플랫폼에 맞는 actual 구현이 연결됩니다.
실전 예제: 간단한 데이터 공유 로직 구현
이제 KMP를 사용하여 간단한 데이터 가져오기 로직을 공유하는 예제를 살펴보겠습니다. 우리는 공통 네트워크 클라이언트를 정의하고, 각 플랫폼에서 이를 사용하는 방법을 보여줄 것입니다. 여기서는 Ktor HTTP 클라이언트를 사용합니다.
먼저, commonMain에 ApiClient 인터페이스를 정의하고, 데이터를 가져오는 함수를 선언합니다.
// commonMain/kotlin/com/example/kmpapp/ApiClient.kt
package com.example.kmpapp
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 Post(
val id: Int,
val userId: Int,
val title: String,
val body: String
)
interface ApiClient {
suspend fun getPosts(): List<Post>
}
// 플랫폼별 구현을 위한 expect 함수 (실제 Ktor 클라이언트 생성)
expect fun getHttpClient(): HttpClient
class ApiClientImpl : ApiClient {
private val httpClient = getHttpClient()
override suspend fun getPosts(): List<Post> {
return httpClient.get("https://jsonplaceholder.typicode.com/posts").body()
}
}
이제 getHttpClient()에 대한 actual 구현을 각 플랫폼에 제공합니다.
// androidMain/kotlin/com/example/kmpapp/HttpClient.kt
package com.example.kmpapp
import io.ktor.client.*
import io.ktor.client.engine.android.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.json.Json
actual fun getHttpClient(): HttpClient {
return HttpClient(Android) {
install(ContentNegotiation) {
json(Json {
ignoreUnknownKeys = true
prettyPrint = true
isLenient = true
})
}
}
}// iosMain/kotlin/com/example/kmpapp/HttpClient.kt
package com.example.kmpapp
import io.ktor.client.*
import io.ktor.client.engine.darwin.* // iOS는 Darwin 엔진 사용
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.json.Json
actual fun getHttpClient(): HttpClient {
return HttpClient(Darwin) {
install(ContentNegotiation) {
json(Json {
ignoreUnknownKeys = true
prettyPrint = true
isLenient = true
})
}
}
}
Android에서 공유 로직 사용하기 (Kotlin)
Android 앱의 ViewModel이나 Activity에서 ApiClientImpl을 사용하여 데이터를 가져올 수 있습니다.
// androidApp/src/main/java/com/example/kmpapp/MainActivity.kt (예시)
package com.example.kmpapp
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch
class MainActivity : ComponentActivity() {
private val apiClient: ApiClient = ApiClientImpl()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
try {
val posts = apiClient.getPosts()
posts.forEach { post ->
Log.d("KMP_APP", "Post Title: ${post.title}")
}
} catch (e: Exception) {
Log.e("KMP_APP", "Error fetching posts: ${e.message}")
}
}
}
}
iOS에서 공유 로직 사용하기 (Swift)
KMP 모듈은 iOS 프로젝트에서 프레임워크로 빌드되어 Swift 코드에서 직접 호출할 수 있습니다.
// iosApp/iOSApp.swift (예시)
import SwiftUI
import KmpApp // KMP 모듈 이름을 임포트
@main
struct iOSApp: App {
let apiClient = ApiClientImpl() // KMP 공유 코드의 인스턴스 생성
var body: some Scene {
WindowGroup {
ContentView(apiClient: apiClient)
}
}
}
struct ContentView: View {
let apiClient: ApiClient
@State private var posts: [Post] = []
@State private var errorMessage: String?
var body: some View {
NavigationView {
List(posts, id: \.id) { post in
VStack(alignment: .leading) {
Text(post.title).font(.headline)
Text(post.body).font(.subheadline).foregroundColor(.gray)
}
}
.navigationTitle("Posts")
.onAppear {
Task {
do {
let fetchedPosts = try await apiClient.getPosts() // KMP suspend 함수 호출
await MainActor.run {
self.posts = fetchedPosts
}
} catch {
await MainActor.run {
self.errorMessage = error.localizedDescription
print("Error fetching posts: \(error)")
}
}
}
}
.alert(item: $errorMessage) { message in
Alert(title: Text("Error"), message: Text(message), dismissButton: .default(Text("OK")))
}
}
}
}
iOS에서 Kotlin의 suspend 함수는 Swift의 async/await와 호환되도록 자동으로 변환됩니다. KmpApp은 Gradle에서 설정한 iOS 프레임워크 이름입니다.
KMP UI 통합 전략
KMP의 가장 큰 특징은 UI를 공유하지 않고 각 플랫폼의 네이티브 UI 프레임워크를 사용한다는 점입니다. 이 전략은 다음과 같은 장점을 가집니다.
- 최적의 사용자 경험: 각 플랫폼의 디자인 가이드라인(Human Interface Guidelines for iOS, Material Design for Android)을 완벽하게 따를 수 있으며, 플랫폼별 특유의 제스처나 애니메이션 등을 구현하기 용이합니다.
- 네이티브 성능: UI 렌더링이 각 플랫폼의 최적화된 엔진을 통해 이루어지므로, 성능 저하 없이 부드러운 사용자 경험을 제공합니다.
- 기존 개발 지식 활용: Android 개발자는 Jetpack Compose나 View 시스템을, iOS 개발자는 SwiftUI나 UIKit을 기존 방식대로 사용하여 UI를 구축할 수 있습니다.
KMP 모듈은 각 플랫폼의 네이티브 UI 코드에 라이브러리 형태로 임포트되어 사용됩니다. 예를 들어, Android에서는 Gradle 종속성으로 추가되고, iOS에서는 .framework 또는 .xcframework 형태로 Xcode 프로젝트에 추가됩니다.
최근에는 JetBrains에서 Compose Multiplatform을 통해 KMP로 UI까지 공유하려는 시도가 활발합니다. Compose Multiplatform은 Jetpack Compose를 iOS, 데스크톱, 웹 등 다양한 플랫폼에서 사용할 수 있도록 확장한 것으로, 아직 iOS 지원은 실험적 단계이지만 미래에는 KMP의 UI 공유 전략을 바꿀 잠재력을 가지고 있습니다.
KMP 개발 환경 설정 및 빌드
KMP 개발을 위해서는 다음과 같은 환경이 필요합니다.
- IDE: IntelliJ IDEA Ultimate (Kotlin Multiplatform 플러그인 내장) 또는 Android Studio (Kotlin 플러그인 설치)를 주로 사용합니다.
- JDK: Kotlin 컴파일을 위해 Java Development Kit가 필요합니다.
- Xcode (macOS 전용): iOS 앱 개발 및 KMP 모듈의 iOS 프레임워크 빌드를 위해 필요합니다. Xcode Command Line Tools도 설치해야 합니다.
- Gradle: KMP 프로젝트의 빌드 시스템으로 사용됩니다.
프로젝트 설정은 주로 build.gradle.kts 파일에서 이루어집니다. 이 파일에서 kotlin-multiplatform 플러그인을 적용하고, sourceSets 블록에서 commonMain, androidMain, iosMain 등의 종속성 및 소스 경로를 정의합니다. iOS 프레임워크 생성을 위해서는 iosX64(), iosArm64(), iosSimulatorArm64() 등의 타겟을 설정하고, binaries.framework 설정을 통해 .xcframework를 생성하도록 지정합니다.
빌드 과정은 다음과 같습니다.
- Gradle Sync: IDE에서 Gradle 프로젝트를 동기화하여 모든 모듈과 종속성을 인식시킵니다.
- Android Build: Android Studio에서 일반적인 Android 앱 빌드 과정과 동일하게 진행됩니다. KMP 모듈은 AAR 라이브러리로 컴파일되어 Android 앱에 포함됩니다.
- iOS Build: Xcode에서 iOS 앱을 빌드하면, Gradle이 KMP 모듈을 iOS 프레임워크(
.xcframework)로 컴파일하고, Xcode는 이 프레임워크를 링크하여 Swift/Objective-C 코드와 함께 최종 iOS 앱을 생성합니다.
KMP 도입을 위한 고려사항
KMP를 프로젝트에 도입하기 전에 몇 가지 중요한 고려사항이 있습니다.
- 팀 역량 및 학습 곡선: 팀원들이 Kotlin, Gradle Multiplatform, 그리고 필요한 경우 Swift/Objective-C에 대한 이해가 충분한지 평가해야 합니다. 새로운 기술 스택에 대한 학습 시간을 충분히 확보해야 합니다.
- 기존 코드베이스 통합: 기존에 이미 iOS와 Android 네이티브 앱이 존재하는 경우, KMP 모듈을 점진적으로 통합하는 전략을 세워야 합니다. 어떤 로직부터 KMP로 전환할지, 기존 코드와의 상호 운용성은 어떻게 가져갈지 계획이 필요합니다.
- 프로젝트 규모 및 복잡성: KMP는 특히 비즈니스 로직이 복잡하고, 많은 기능을 공유해야 하는 대규모 프로젝트에서 빛을 발합니다. 간단한 유틸리티 앱이라면 오버헤드가 더 클 수도 있습니다.
- 서드파티 라이브러리 지원: 필요한 서드파티 라이브러리가 KMP를 지원하는지 확인해야 합니다. Ktor, SQLDelight, Koin, Kodein 등 많은 유명 라이브러리들이 KMP를 지원하고 있지만, 그렇지 않은 경우 직접
expect/actual로 래핑하거나 대안을 찾아야 합니다. - UI 공유 여부: UI를 공유할 필요가 없다면 KMP는 훌륭한 선택입니다. 만약 UI까지 공유하고자 한다면 Compose Multiplatform의 현재 상태와 프로젝트의 요구사항을 신중하게 저울질해야 합니다.
마무리
Kotlin Multiplatform은 iOS와 Android 개발 간의 코드 재사용성을 획기적으로 높이면서도 네이티브 앱의 성능과 사용자 경험을 온전히 유지할 수 있는 강력한 크로스플랫폼 솔루션입니다. 비즈니스 로직의 단일화를 통해 개발 효율성을 높이고, 유지보수 비용을 절감하며, 플랫폼 간의 일관된 동작을 보장할 수 있습니다. 비록 아직 성장 단계에 있는 기술이지만, JetBrains의 적극적인 지원과 활발한 커뮤니티 활동을 통해 빠르게 발전하고 있으며, 모바일 개발의 미래를 이끌어갈 중요한 기술 중 하나로 자리매김할 것입니다. KMP는 특히 기존 네이티브 앱을 보유하고 있거나, 네이티브에 준하는 고품질 앱을 목표로 하는 팀에게 매력적인 선택지가 될 것입니다.
관련 게시글
Kotlin Multiplatform (KMP): 크로스플랫폼 모바일 개발 심층 가이드
Kotlin Multiplatform (KMP)을 활용한 크로스플랫폼 모바일 앱 개발 가이드입니다. React Native, Flutter와 비교하며 KMP의 장점, 아키텍처, 실전 팁 및 코드 예제를 상세히 다룹니다.
SwiftUI 입문 가이드: Cross-Platform 개발자를 위한 iOS 앱 여정 시작하기
React Native, Flutter 개발자도 쉽게 시작할 수 있는 SwiftUI 입문 가이드! 선언형 UI, 상태 관리, 핵심 컴포넌트, 그리고 실전 To-Do 앱 예제까지, iOS 모바일 앱 개발의 첫걸음을 떼어보세요.
SwiftUI Introduction: Building Modern iOS and Cross-Platform Apps
SwiftUI의 기본부터 심화 개념, 그리고 React Native, Flutter와 같은 크로스 플랫폼 프레임워크와의 비교를 통해 현대 모바일 앱 개발에 SwiftUI를 활용하는 방법을 상세히 다룹니다.