이 글은 누구를 위한 것인가
- iOS/Android 코드베이스를 통합하고 중복을 줄이려는 팀
- Kotlin Multiplatform으로 비즈니스 로직을 공유하려는 개발자
- Compose Multiplatform으로 UI까지 공유하려는 팀
들어가며
"iOS 팀이 만든 기능을 Android에도 그대로 구현해야 한다" — KMP는 비즈니스 로직(API 호출, DB, 검증)을 Kotlin으로 한 번 작성하고 iOS/Android에서 공유한다. Compose Multiplatform은 UI까지 공유해 진정한 코드 재사용을 가능하게 한다.
이 글은 bluefoxdev.kr의 Kotlin Multiplatform Compose 가이드 를 참고하여 작성했습니다.
1. KMP 아키텍처
[모듈 구조]
shared/
commonMain/ ← iOS + Android 공통
data/ (Ktor, SQLDelight)
domain/ (비즈니스 로직)
ui/ (Compose Multiplatform)
iosMain/ ← iOS 전용 (expect/actual)
androidMain/ ← Android 전용
[expect/actual 패턴]
commonMain: expect fun getPlatformName(): String
androidMain: actual fun getPlatformName() = "Android"
iosMain: actual fun getPlatformName() = "iOS"
[공유 가능한 것]
네트워킹: Ktor
로컬 DB: SQLDelight
직렬화: kotlinx.serialization
날짜: kotlinx-datetime
코루틴: kotlinx.coroutines
DI: Koin (멀티플랫폼)
UI: Compose Multiplatform (iOS 베타)
[플랫폼 전용]
iOS: SwiftUI 통합, CoreBluetooth, NFC
Android: Android View, 권한, 알림
2. KMP 구현
// shared/commonMain/kotlin/data/ProductRepository.kt
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.coroutines.flow.Flow
import kotlinx.serialization.Serializable
@Serializable
data class Product(
val id: String,
val name: String,
val price: Double,
val category: String
)
class ProductRepository(private val db: ProductDatabase) {
private val client = HttpClient {
install(ContentNegotiation) { json() }
}
// 네트워크 + 로컬 캐시 (Offline First)
suspend fun getProducts(): List<Product> {
return try {
val products = client.get("https://api.example.com/products").body<List<Product>>()
db.productQueries.insertAll(products.map { it.toEntity() })
products
} catch (e: Exception) {
db.productQueries.selectAll().executeAsList().map { it.toProduct() }
}
}
fun observeProducts(): Flow<List<Product>> {
return db.productQueries.selectAll().asFlow().mapToList()
}
}
// shared/commonMain/kotlin/presentation/ProductViewModel.kt
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
class ProductViewModel(
private val repository: ProductRepository,
private val scope: CoroutineScope
) {
private val _uiState = MutableStateFlow(ProductUiState())
val uiState: StateFlow<ProductUiState> = _uiState.asStateFlow()
init {
loadProducts()
}
fun loadProducts() {
scope.launch {
_uiState.update { it.copy(isLoading = true) }
try {
val products = repository.getProducts()
_uiState.update { it.copy(products = products, isLoading = false) }
} catch (e: Exception) {
_uiState.update { it.copy(error = e.message, isLoading = false) }
}
}
}
}
data class ProductUiState(
val products: List<Product> = emptyList(),
val isLoading: Boolean = false,
val error: String? = null
)
// shared/commonMain/kotlin/ui/ProductListScreen.kt (Compose Multiplatform)
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
@Composable
fun ProductListScreen(viewModel: ProductViewModel) {
val state by viewModel.uiState.collectAsState()
when {
state.isLoading -> CircularProgressIndicator()
state.error != null -> Text("오류: ${state.error}")
else -> LazyColumn {
items(state.products) { product ->
ProductCard(product = product)
}
}
}
}
@Composable
fun ProductCard(product: Product) {
Card(modifier = Modifier) {
ListItem(
headlineContent = { Text(product.name) },
supportingContent = { Text("₩${product.price.toLong()}") },
trailingContent = { Badge { Text(product.category) } }
)
}
}
// expect/actual: 플랫폼 특화 기능
// commonMain
expect class PlatformLogger() {
fun log(message: String)
}
// androidMain
actual class PlatformLogger actual constructor() {
actual fun log(message: String) = android.util.Log.d("KMP", message)
}
// iosMain
actual class PlatformLogger actual constructor() {
actual fun log(message: String) = println("[KMP] $message")
}
// iOS에서 Kotlin 모듈 사용 (Swift)
// import shared
// let viewModel = ProductViewModel(repository: ..., scope: ...)
// viewModel.uiState.collect { state in ... }
마무리
KMP의 핵심은 "비즈니스 로직을 한 번만 작성"이다. Ktor + SQLDelight + ViewModel을 commonMain에 구현하면 iOS/Android가 동일한 데이터 레이어를 사용한다. Compose Multiplatform은 UI까지 공유하지만 iOS 지원이 안정화 단계라 프로덕션에는 신중하게 검토해야 한다. expect/actual로 플랫폼 전용 기능을 깔끔하게 분리한다.