# 2026년 Kotlin Multiplatform: 안드로이드와 iOS 코드 공유 > 2026년 Kotlin Multiplatform이 Android와 iOS에서 비즈니스 로직을 공유하는 방법을 설명합니다. Gradle 설정, expect/actual, Ktor, Compose Multiplatform, KMP 면접 질문까지 다룹니다. - Published: 2026-06-26 - Updated: 2026-07-06 - Author: SharpSkill - Tags: Kotlin Multiplatform, KMP, Android, iOS, Cross-platform - Reading time: 10 min --- Kotlin Multiplatform(KMP)은 하나의 Kotlin 코드베이스로 Android와 iOS 앱의 비즈니스 로직을 함께 구동하면서도, 각 플랫폼이 완전한 네이티브 UI와 성능, 그리고 자체 SDK 접근을 그대로 유지하게 해 줍니다. 2026년의 KMP는 더 이상 실험이 아닙니다. 안정 버전이며 [Google](https://developer.android.com/kotlin/multiplatform)이 공식적으로 지원하고, Netflix, McDonald's, Forbes, Cash App의 프로덕션 환경에서 실제로 동작하고 있습니다. 이 글에서는 코드 공유가 실제로 어떻게 작동하는지, 공유 모듈을 어떻게 구성하는지, 그리고 KMP 면접에 앞서 이해해 둘 만한 트레이드오프를 깊이 있게 다룹니다. > **KMP가 실제로 공유하는 것** > > Kotlin Multiplatform이 공유하는 것은 컴파일된 로직이지 런타임이 아닙니다. 공통 Kotlin은 Android용으로는 JVM 바이트코드로, iOS용으로는 (LLVM을 통해) 네이티브 바이너리로 컴파일됩니다. JavaScript 브리지도, 내장 웹뷰도, 리플렉션 계층도 없기 때문에 공유 코드는 양쪽 플랫폼에서 네이티브 속도로 실행됩니다. ## Kotlin Multiplatform이 네이티브 성능을 희생하지 않고 코드를 공유하는 방식 핵심 메커니즘은 소스 세트의 계층 구조입니다. `commonMain`에는 네트워킹, 직렬화, 비즈니스 규칙, 뷰 모델처럼 플랫폼에 종속되지 않는 Kotlin을 둡니다. `androidMain`과 `iosMain` 같은 플랫폼 소스 세트에는 플랫폼 API를 다루는 코드를 둡니다. 컴파일러는 Android용으로 `commonMain`과 `androidMain`에서 JVM 아티팩트를, iOS용으로 `commonMain`과 `iosMain`에서 네이티브 프레임워크를 생성합니다. 2026년의 Kotlin Multiplatform은 단일 진실 공급원에서 진정으로 이득을 보는 것, 즉 모델, API 클라이언트, 캐싱, 검증을 공유하는 데 무게를 둡니다. UI는 Compose Multiplatform으로 공유할 수도 있고, 화면 단위로 SwiftUI를 사용해 완전한 네이티브로 남겨 둘 수도 있습니다. "로직은 공유하고 UI는 선택한다"는 이 모델이야말로 기존 앱을 다시 작성하지 않고도 팀이 KMP를 도입하는 이유입니다. Kotlin Multiplatform은 하나의 공유 모듈을 여러 타깃(JVM 위의 Android, 네이티브로서의 iOS, 그리고 데스크톱과 웹)으로 컴파일하는 Kotlin 기술입니다. 덕분에 비즈니스 로직은 한 번만 작성하면 되고, 각 플랫폼은 네이티브 UI와 완전한 SDK 접근을 그대로 유지합니다. 이제 Google은 이를 일급 옵션으로 지원하며, Room, DataStore, ViewModel, Paging 같은 Jetpack 라이브러리가 KMP 아티팩트를 제공합니다. 공유 모듈에 무엇을 넣을지 결정하는 일은 KMP 프로젝트에서 가장 중요한 아키텍처 판단입니다. 아래 표는 2026년에 대다수 프로덕션 팀이 경계를 어디에 긋는지를 보여 줍니다. 프레젠테이션 계층 아래의 모든 것은 공유의 유력한 후보이며, 플랫폼의 UX 관례에 닿는 것은 대개 네이티브로 남습니다. | 계층 | commonMain에서 공유? | 이유 | |-------|---------------------|--------| | 데이터 모델과 DTO | 예 | 양쪽 플랫폼에서 동일한 API 계약 | | 네트워킹과 캐싱 | 예 | HTTP 스택 하나, 버그 표면 하나 | | 비즈니스 규칙과 검증 | 예 | 도메인은 OS마다 다르지 않음 | | 뷰 모델과 상태 | 대개 | Compose와 SwiftUI 모두 공유 상태를 소비 | | UI 렌더링 | 선택 | Compose로 공유하거나 화면별 네이티브 | | 플랫폼 API(카메라, 생체 인증) | 아니오 | 대신 expect/actual로 노출 | 얻는 이득은 앱이 이 표의 아래쪽 절반을 얼마나 차지하느냐에 비례해 커집니다. UI가 얇고 데이터 중심인 제품일수록 공유량이 많고, 고도로 맞춤화되고 애니메이션 중심인 앱일수록 공유량이 적습니다. 어느 쪽 극단도 틀린 것은 아닙니다. 그래서 KMP는 전부 아니면 전무 식의 재작성보다 점진적 도입에 잘 맞습니다. ## Gradle에서 공유 모듈 구성하기 KMP 모듈은 Kotlin DSL로 타깃과 소스 세트를 선언합니다. 다음 `build.gradle.kts`는 Android와 세 가지 iOS 아키텍처(실기기, Intel 시뮬레이터, Apple Silicon 시뮬레이터)를 설정하고, iOS 출력을 `Shared`라는 이름의 프레임워크로 노출합니다. ```kotlin // shared/build.gradle.kts plugins { kotlin("multiplatform") kotlin("plugin.serialization") id("com.android.library") } kotlin { // Android target compiles commonMain + androidMain to JVM bytecode androidTarget() // Three iOS targets: device, Intel sim, Apple Silicon sim listOf(iosArm64(), iosX64(), iosSimulatorArm64()).forEach { target -> target.binaries.framework { baseName = "Shared" // becomes "import Shared" in Swift isStatic = true } } sourceSets { commonMain.dependencies { implementation("io.ktor:ktor-client-core:3.3.0") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2") } androidMain.dependencies { implementation("io.ktor:ktor-client-okhttp:3.3.0") } iosMain.dependencies { implementation("io.ktor:ktor-client-darwin:3.3.0") } } } ``` 각 타깃은 플랫폼별 Ktor 엔진(Android에서는 `okhttp`, iOS에서는 `darwin`)을 가져오는 반면, 공유 코드는 공통 `ktor-client-core` API에만 의존합니다. 설정 가능한 옵션 전체는 [Kotlin Multiplatform 공식 문서](https://kotlinlang.org/docs/multiplatform.html)에 정리되어 있습니다. ## expect와 actual로 플랫폼별 코드 작성하기 일부 로직은 플랫폼 API가 필요하기 때문에 `commonMain`에 둘 수 없습니다. `expect`/`actual` 메커니즘은 공통 코드에서 형태를 선언하고 타깃별로 구체적인 구현을 제공하며, 컴파일 시점에 해결되어 런타임 비용이 전혀 없습니다. ```kotlin // commonMain/kotlin/Platform.kt // commonMain declares what it needs but cannot implement here expect class Platform() { val name: String val osVersion: String } // Shared logic can now depend on Platform freely fun analyticsUserAgent(): String { val platform = Platform() return "SharpSkill/" + platform.name + " " + platform.osVersion } ``` Android 구현은 `Build` 클래스에서, iOS 구현은 `UIDevice`에서 값을 읽습니다. 둘 다 같은 계약을 충족하므로 `commonMain`은 자신이 어느 쪽과 대화하는지 결코 알지 못합니다. ```kotlin // androidMain/kotlin/Platform.kt import android.os.Build // actual supplies the Android-specific implementation actual class Platform actual constructor() { actual val name: String = "Android" actual val osVersion: String = Build.VERSION.RELEASE } ``` ```kotlin // iosMain/kotlin/Platform.kt import platform.UIKit.UIDevice // actual uses UIKit directly, no Objective-C bridging code required actual class Platform actual constructor() { actual val name: String = "iOS" actual val osVersion: String = UIDevice.currentDevice.systemVersion } ``` Kotlin/Native는 iOS SDK 전체에 대한 타입 지정 바인딩을 함께 제공하므로 `UIDevice`, `NSUserDefaults`, Foundation 타입을 손으로 작성한 연결 코드 없이 Kotlin에서 호출할 수 있습니다. ## 네트워킹과 직렬화 로직 공유하기 네트워킹은 API 계약이 양쪽 플랫폼에서 동일하기 때문에 공유 가치가 가장 높은 코드입니다. 하나의 [Ktor](https://ktor.io/docs/) 클라이언트와 `kotlinx.serialization`이 두 개의 병렬 구현을 대체합니다. `suspend` 함수는 [Kotlin 코루틴](/blog/android/mastering-kotlin-coroutines)에 의존하며, 양쪽에서 `async`/`await`로 깔끔하게 대응됩니다. ```kotlin // commonMain/kotlin/data/InterviewRepository.kt import io.ktor.client.HttpClient import io.ktor.client.call.body import io.ktor.client.plugins.contentnegotiation.ContentNegotiation import io.ktor.client.request.get import io.ktor.serialization.kotlinx.json.json import kotlinx.serialization.Serializable // One serializable model, zero duplication across platforms @Serializable data class Question(val id: String, val prompt: String, val difficulty: String) // One HttpClient, one JSON parser, one repository for both apps class InterviewRepository { private val client = HttpClient { install(ContentNegotiation) { json() } } suspend fun fetchQuestions(tech: String): List = client.get("https://api.sharpskill.dev/questions/" + tech).body() } ``` 이 리포지토리와 그 재시도 로직, 데이터 모델은 한 번만 작성됩니다. 파서에서 고친 버그는 Android와 iOS에서 동시에 고쳐지며, 바로 이 지점에서 KMP가 도입 비용을 회수합니다. 같은 패턴이 SQLDelight를 이용한 공유 데이터베이스, 공유 키-값 저장소, Koin을 이용한 공유 의존성 주입으로도 확장되므로, 데이터 계층 전체를 UI가 의존하는 인터페이스 뒤의 `commonMain`에 둘 수 있습니다. 흔한 실수는 너무 이른 시점에 너무 많은 것을 공유하려는 것입니다. 실용적인 방법은 먼저 리포지토리 하나로 시작해 양쪽 플랫폼에서 빌드와 CI 파이프라인을 검증한 뒤, 자신감이 붙을수록 기능을 하나씩 옮기는 것입니다. ## Compose Multiplatform으로 공유 UI 구축하기 Compose Multiplatform은 2025년 5월에 iOS에서 안정 버전(1.8.0)에 도달했고, 2026년 중반에는 네이티브 텍스트 입력과 기본 활성화된 동시 렌더링을 갖춘 1.11.0에 이르렀습니다. `commonMain`에 있는 `@Composable`은 양쪽 플랫폼에서 렌더링되며, 공유 뷰 모델은 [MVVM 또는 MVI 아키텍처](/blog/android/mvvm-vs-mvi-architecture)와 자연스럽게 어울립니다. ```kotlin // commonMain/kotlin/ui/QuestionList.kt import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material3.Card import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.compose.foundation.layout.padding // This list renders identically on Android and iOS @Composable fun QuestionList(questions: List) { LazyColumn { items(questions) { question -> Card(modifier = Modifier.padding(8.dp)) { Text(text = question.prompt, modifier = Modifier.padding(16.dp)) } } } } ``` UI 공유는 선택 사항이며 화면 단위입니다. 팀은 [Compose Multiplatform](https://github.com/JetBrains/compose-multiplatform)으로 인터페이스 전체를 공유할 수도, 앱 전체를 SwiftUI로 유지한 채 로직만 공유할 수도, 둘을 섞을 수도 있습니다. 위젯이나 App Clip처럼 플랫폼별 UX에 의존하는 화면은 보통 네이티브로 남깁니다. ## iOS에서 Swift로 공유 코드 사용하기 공유 모듈은 Swift가 여느 라이브러리처럼 임포트하는 프레임워크로 컴파일됩니다. 역사적으로 마찰이 되었던 부분은 Kotlin의 `suspend` 함수와 `Flow`가 어색한 콜백 API로 드러난다는 점이었습니다. Touchlab의 [SKIE](https://skie.touchlab.co/)는 관용적인 Swift를 생성해 이를 해결합니다. `suspend`는 네이티브 `async`/`await`가 되고 `Flow`는 `AsyncSequence`가 됩니다. ```swift // iosApp/ContentView.swift import SwiftUI import Shared // the KMP framework, baseName = "Shared" struct ContentView: View { @State private var questions: [Question] = [] private let repository = InterviewRepository() var body: some View { List(questions, id: \.id) { question in Text(question.prompt) } .task { // With SKIE, the Kotlin suspend fun is a Swift async fun questions = (try? await repository.fetchQuestions(tech: "android")) ?? [] } } } ``` iOS 팀은 평범한 Swift와 SwiftUI를 작성합니다. 이들은 공유 모델과 리포지토리를 네이티브 Swift 패키지와 똑같이 사용하므로, KMP는 iOS 코드베이스의 대부분에서 보이지 않는 상태로 남습니다. 면접에서 언급할 가치가 있는 거친 부분이 아직 두 가지 있습니다. 첫째는 빌드 시간입니다. Kotlin/Native 컴파일은 순수 Swift 빌드보다 느리지만, Gradle 빌드 캐시와 미리 빌드된 XCFramework 배포가 일상적인 비용을 완화합니다. 둘째는 Swift에서 Kotlin으로 넘어가는 경계에서의 디버깅입니다. 개선되고 있지만 하나의 언어에 머무를 때만큼 아직 매끄럽지는 않습니다. 이 또한 팀이 공유 범위를 무질서하게 넓히기보다 의도적으로 유지하는 또 하나의 이유입니다. ## 예상되는 Kotlin Multiplatform 면접 질문 KMP 면접 질문은 Android와 모바일 직무에서 점점 더 자주 등장하므로, 폭넓은 [Android 면접 준비](/technologies/android)와 함께 반복되는 몇 가지를 연습해 둘 가치가 있습니다. - **expect/actual은 인터페이스와 어떻게 다릅니까?** expect/actual은 런타임 디스패치 없이 타깃별로 컴파일 시점에 해결되며, 클래스, 함수, 프로퍼티, typealias를 뒷받침할 수 있습니다. 인터페이스는 런타임에 동적으로 해결됩니다. 플랫폼 API에는 expect/actual을, 공유 코드 내부의 다형성에는 인터페이스를 사용합니다. - **Kotlin/Native는 메모리와 스레딩을 어떻게 다룹니까?** Kotlin 1.7.20부터 새 메모리 관리자가 기존의 객체 동결 모델을 없앴기 때문에, 공유되는 가변 상태와 코루틴이 JVM에서와 거의 같은 방식으로 스레드를 넘나들며 동작합니다. - **기존 앱이 KMP를 점진적으로 도입할 수 있습니까?** 예. 공유 모듈은 Android용으로는 AAR, iOS용으로는 XCFramework로 배포되므로 재작성 없이 리포지토리나 기능 하나씩 이전할 수 있습니다. - **Compose Multiplatform 대신 UI를 네이티브로 남겨야 할 때는 언제입니까?** 화면이 플랫폼별 UX에 크게 의존할 때, 또는 iOS 팀이 프레젠테이션 계층을 소유하고 직접 제어하기를 원할 때입니다. > **2026년의 툴체인** > > K2 컴파일러를 갖춘 Kotlin 2.3, 네트워킹에는 Ktor 3.x, 공유 데이터베이스에는 SQLDelight, 의존성 주입에는 Koin을 목표로 삼습니다. iOS 상호운용을 위해서는 SKIE를 일찍 추가합니다. Swift 계층을 작성한 뒤에 나중에 끼워 넣는 것은 훨씬 더 고통스럽습니다. ## 결론 2026년의 Kotlin Multiplatform은 네이티브 UI나 성능을 포기하지 않으면서 Android와 iOS에 걸친 중복 로직을 줄이는 실용적인 방법입니다. - 단일 진실 공급원을 가진 로직(모델, 네트워킹, 검증)을 공유하고, 화면마다 네이티브 UI와 Compose UI 중에서 선택합니다. - 플랫폼 API에는 `expect`/`actual`을 사용합니다. 컴파일 시점에 해결되어 런타임 오버헤드가 없습니다. - Ktor와 `kotlinx.serialization`으로 네트워킹을 한 번만 작성하면 수정이 양쪽 플랫폼에 동시에 반영됩니다. - 재작성에 몰두하는 대신 AAR과 XCFramework를 통해 점진적으로 도입합니다. - 처음부터 SKIE를 추가해 Swift가 `suspend`와 `Flow`를 네이티브 `async`/`await`와 `AsyncSequence`로 사용하게 합니다. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/ko/blog/android/kotlin-multiplatform-2026