Split View: Kotlin Swift export가 Alpha가 됐다 — Objective-C 다리를 걷어내는 중이지만, 아직 건너지는 마십시오
Kotlin Swift export가 Alpha가 됐다 — Objective-C 다리를 걷어내는 중이지만, 아직 건너지는 마십시오
- 들어가며 — KMP가 iOS에서 내는 세금
- Objective-C 다리가 실제로 부수는 것들
- Swift export가 하는 일
- 2.4.0이 바꾼 것 — 승격, 그리고 코루틴
- "Alpha"가 JetBrains 사전에서 뜻하는 것
- 아직 못 하는 것들
- Int가 Int32가 되는 문제
- 2.4.0의 나머지 Kotlin/Native 변화
- 그래서, 지금 써야 하나
- 마치며
- 참고 자료
들어가며 — KMP가 iOS에서 내는 세금
Kotlin Multiplatform의 세일즈 문구는 늘 깔끔했습니다. 비즈니스 로직을 한 번 쓰고 Android와 iOS가 같이 쓴다. Android 쪽은 실제로 그렇게 됩니다 — 같은 언어, 같은 툴체인이니까요.
문제는 늘 iOS 쪽 경계면이었습니다. Kotlin/Native가 만들어 내는 결과물이 Swift가 바로 읽는 무언가가 아니었기 때문입니다. Kotlin은 Objective-C 프레임워크를 뱉고, Swift는 그 Objective-C 헤더를 Swift에서 보이는 API로 다시 번역해서 씁니다. 즉 Kotlin과 Swift 사이에 아무도 원하지 않은 제3의 언어가 끼어 있습니다. 둘 다 널 안정성을 타입에 담는 현대 언어인데, 그 사이 통역이 1984년산 언어인 셈입니다.
이 통역 비용은 추상적인 게 아닙니다. 아래에서 보겠지만 타입이 뭉개지고, 이름이 망가지고, 코루틴이 콜백으로 되돌아갑니다. Swift export는 이 다리를 아예 걷어내려는 시도이고, Kotlin 2.4.0(2026년 6월 3일 릴리스)에서 Alpha가 됐습니다.
Alpha가 됐다는 건 좋은 소식입니다. 그런데 "Alpha"가 JetBrains 사전에서 정확히 무슨 뜻인지 확인하고 나면, 지금 당장 마이그레이션 티켓을 끊을 마음은 사라질 겁니다. 이 글은 그 양쪽을 다 정리합니다.
Objective-C 다리가 실제로 부수는 것들
먼저 왜 이 기능이 존재하는지부터 봐야 합니다. Kotlin의 Objective-C interop 문서가 스스로 인정하는 손실들입니다.
프리미티브 박싱. 문서 표현 그대로, kotlin.Int의 박스는 Swift에서 KotlinInt 클래스 인스턴스로 표현됩니다. 이 클래스들은 NSNumber에서 파생되죠. 그리고 역방향은 자동으로 안 됩니다 — 문서는 NSNumber가 Swift/Objective-C 파라미터나 반환 타입으로 쓰일 때 Kotlin 프리미티브로 자동 변환되지 않는다고 명시합니다. 이유는 NSNumber가 감싼 값이 Byte인지 Boolean인지 Double인지에 대한 정보를 정적으로 갖고 있지 않기 때문이고, 그래서 수동으로 캐스팅해야 합니다. 널 가능한 정수 하나 넘기려고 힙 할당된 래퍼가 생기고, Swift 쪽에서 수동 캐스팅 코드가 생깁니다.
제네릭 손실. Objective-C의 "lightweight generics"는 Kotlin의 제네릭을 담기에 좁습니다. 문서가 못 박는 제약은 두 가지입니다. 첫째, 제네릭은 클래스에만 정의할 수 있고 인터페이스(Objective-C·Swift의 프로토콜)나 함수에는 정의할 수 없습니다. 둘째, 널 가능성이 뭉개집니다. 문서의 예를 그대로 옮기면, 이런 Kotlin 코드가
class Sample<T>() {
fun myVal(): T
}
Swift에서는 이렇게 보입니다.
class Sample<T>() {
fun myVal(): T?
}
T가 T?로 바뀌었습니다. Objective-C 헤더가 myVal을 널 가능으로 선언할 수밖에 없기 때문입니다. 문서가 권하는 우회는 T : Any 상한을 직접 걸어서 헤더가 non-null로 마킹하게 만드는 것 — 즉 Kotlin 쪽 API 설계를 Objective-C 헤더 사정에 맞춰 비틀라는 이야기입니다.
코루틴이 콜백이 됨. Kotlin의 suspend 함수는 생성된 Objective-C 헤더에서 콜백을 받는 함수, Swift 용어로는 컴플리션 핸들러로 나타납니다. Swift 5.5부터 async 함수로 부를 수 있는 길이 열리긴 했는데, 문서는 이 기능이 "highly experimental"이며 특정 제약이 있다고 적고 자세한 건 KT-47610을 보라고 합니다. 이 글을 쓰는 2026년 7월 16일 기준으로 그 이슈는 여전히 Open 상태입니다. 서브시스템은 Language Design과 Native. ObjC Export로 잡혀 있습니다. 몇 년째 열려 있는 이 이슈가, Swift export라는 별도 경로가 필요했던 이유를 압축해서 보여 줍니다.
이름 뭉개짐. Swift export 문서가 스스로 밝히는 동기가 이것입니다 — Objective-C의 헷갈리는 언더스코어와 뭉개진 이름(mangled names)을 없애는 것. 최상위 함수들이 어딘가의 SharedKt 같은 클래스에 붙어 나타나는 걸 본 적이 있다면 무슨 얘긴지 아실 겁니다.
정리하면, Objective-C 다리는 Kotlin API를 그대로 옮기지 못하고 Objective-C가 표현할 수 있는 부분집합으로 투영합니다. 손실은 설계상 불가피한 것이었습니다.
Swift export가 하는 일
Swift export의 아이디어는 단순합니다. 중간 언어를 빼는 것.
공식 문서의 첫 문장이 정확합니다 — Swift export는 Kotlin 소스를 직접 내보내서 Swift에서 Kotlin 코드를 관용적으로 호출하게 하고, Objective-C 헤더를 없앤다. 컴파일러가 swiftmodule 파일, 정적 .a 라이브러리, 헤더 파일, modulemap 파일을 직접 만들어 앱 빌드 디렉터리로 복사합니다.
설정은 Gradle 쪽 블록 하나입니다.
// build.gradle.kts
kotlin {
iosArm64()
iosSimulatorArm64()
swiftExport {
// 루트 모듈 이름
moduleName = "Shared"
// 패키지 접두사를 생성된 Swift 코드에서 제거
flattenPackage = "com.example.sandbox"
// 외부 모듈 export 설정
export(project(":subproject")) {
moduleName = "Subproject"
flattenPackage = "com.subproject.library"
}
}
}
그리고 Xcode의 Build Phases에서 Run Script를 embedAndSignAppleFrameworkForXcode 대신 embedSwiftExportForXcode 태스크로 바꿔 줍니다. 문서가 제시하는 스크립트는 ./gradlew :<모듈 이름>:embedSwiftExportForXcode 형태입니다.
앞 절에서 본 손실 중 몇 개는 실제로 사라집니다. 문서가 나열하는 것들입니다.
- 프리미티브 널 가능성 개선.
Int?같은 타입을KotlinInt래퍼로 박싱해야 했던 Objective-C interop과 달리, Swift export는 널 가능성 정보를 직접 변환합니다. - 멀티모듈 지원. Kotlin 모듈 하나가 Swift 모듈 하나로 나갑니다.
- 패키지 보존. Kotlin 패키지가 명시적으로 보존되어 이름 충돌을 피합니다(중첩 Swift enum으로 번역됩니다).
- 오버로드. Kotlin의 오버로드된 함수를 Swift에서 모호함 없이 부를 수 있습니다.
- 타입 별칭.
typealias가 Swift에 그대로 남습니다.
그리고 Kotlin 2.3.0(2025년 12월 16일)에서 두 가지가 더 붙었습니다. Kotlin enum class가 예전처럼 평범한 Swift 클래스로 나가지 않고 진짜 네이티브 Swift enum으로 매핑되기 시작했고, vararg 함수가 Swift의 가변 파라미터로 직접 매핑됐습니다.
2.4.0이 바꾼 것 — 승격, 그리고 코루틴
Kotlin 2.4.0 릴리스 노트가 말하는 이번 변화의 핵심은 두 가지입니다.
첫째, 구조적 동시성. Kotlin의 suspend 함수와 suspend 함수 타입이 Swift의 관용적 async로 나갑니다. 컴플리션 핸들러가 아니라요.
// Kotlin
suspend fun hello(): String {
delay(1000)
return "Hello Swift! This is Kotlin."
}
// Swift
let msg = try await hello()
둘째, Flow export. 이게 실무적으로 더 큽니다. 문서가 정확히 짚듯이, 이전까지 kotlinx.coroutines.flow의 Flow 인터페이스를 Swift로 노출하는 유일한 방법은 서드파티 솔루션이었습니다. 이제 Flow가 Swift의 AsyncSequence로 기본 활성화된 채 나가고, 타입 정보도 보존됩니다.
// Kotlin
fun flowOfStrings(): Flow<String> = flowOf("hello", "any", "world")
// Swift
var actual: [String] = []
// String 타입이 Kotlin에서 올바르게 추론됨
for try await element in flowOfStrings().asAsyncSequence() {
actual.append(element)
}
KMP를 실제로 굴려 본 사람이라면 이 한 줄의 무게를 압니다. "Kotlin 쪽 상태 스트림을 SwiftUI에 어떻게 꽂나"는 KMP 도입 팀이 반드시 마주치는 질문이었고, 지금까지의 답은 항상 서드파티 래퍼였습니다.
기본 활성화라는 점도 짚어 둘 만합니다. Swift export 자체는 opt-in이지만, 그 안에서 Flow export는 따로 켤 필요가 없습니다.
승격의 기준선이 무엇이었는지도 흥미롭습니다. Kotlin 로드맵에 "Swift Export: Alpha release"라는 라벨로 걸려 있던 항목은 KT-80305인데, 티켓의 실제 제목은 "Support coroutines in Swift Export"입니다. 2026년 3월 24일에 Fixed로 닫혔고 2.4.0-Beta2에 들어갔습니다. 즉 Alpha의 문턱은 사실상 "코루틴이 되느냐"였습니다. 반대로 말하면, 코루틴이 안 되는 동안에는 이 기능이 iOS 앱의 실제 API 표면을 감당할 수 없다고 JetBrains 스스로 판단하고 있었다는 뜻이기도 합니다.
"Alpha"가 JetBrains 사전에서 뜻하는 것
여기서 정직해질 부분입니다. 많은 프로젝트에서 Alpha는 "곧 됩니다" 정도의 마케팅 단어지만, Kotlin은 이 등급을 문서에 정의해 뒀습니다. components-stability 문서의 정의를 그대로 옮기면 이렇습니다.
Alpha는 "we are testing whether this should become production-ready" — 이걸 프로덕션 레디로 만들어야 하는지를 시험하는 중이라는 뜻입니다. 그리고 이어지는 설명이 핵심입니다. 제품으로 만들 의도가 있고 사용자 가치와 시장 적합성을 검증해 최종 형태를 잡는 중이며, 기능 집합은 아직 불완전하고 파괴적 변경이 예상되며, 가설이 성립하지 않으면 기능을 크게 바꾸거나 중단할 수 있다("we may significantly change or discontinue the feature").
바로 아래 등급인 Beta의 정의와 비교해 보면 격차가 선명합니다. Beta는 "you can use it, we'll do our best to minimize migration issues for you" — 써도 된다, 마이그레이션 이슈를 최소화하려 최선을 다하겠다는 약속이 들어 있습니다. Alpha에는 그 약속이 없습니다. Kotlin 문서는 Experimental·Alpha·Beta를 묶어 pre-stable이라 부릅니다.
그리고 문서가 덧붙이는 한 문장을 그대로 옮길 가치가 있습니다 — 안정성 등급은 그 컴포넌트가 얼마나 빨리 Stable로 릴리스될지에 대해 아무것도 말하지 않는다. 즉 Alpha가 됐다고 해서 Beta나 Stable이 가깝다는 뜻이 전혀 아닙니다.
Swift export 문서 본문도 같은 톤입니다. "currently in Alpha and still incomplete, so breaking changes are expected."
현재 공개된 Kotlin 로드맵은 마지막 수정이 2026년 2월, 다음 갱신 예정이 2026년 8월로 적혀 있습니다. 그 로드맵에 걸린 Swift export 항목은 Alpha 릴리스까지이고, Beta나 Stable을 겨냥한 항목은 올라와 있지 않습니다. 2.4.0 다음 EAP인 2.4.20-Beta1(2026년 6월 24일) 릴리스 노트에도 Swift export 관련 변경은 없습니다. 다음 로드맵 갱신 전까지, 공개된 자료만으로 Beta 시점을 추정할 근거는 없습니다. 없는 날짜를 지어내느니 없다고 말하는 편이 낫습니다.
한 가지 더 — JetBrains 자신도 이번 릴리스에서 Swift export를 크게 홍보하지 않았습니다. 2.4.0 릴리스 블로그의 Kotlin/Native 요약 줄은 "Support for Swift packages as dependencies, updates on Swift export, and the CMS GC enabled by default"가 전부입니다. "updates on Swift export" — Alpha 승격을 헤드라인으로 뽑지 않았다는 것 자체가 하나의 신호입니다.
아직 못 하는 것들
문서의 "Current limitations" 절과 본문 주석에 흩어진 제약을 모으면 이렇습니다. 각각이 실제 프로젝트에서 어떻게 아픈지를 붙여 봅니다.
direct integration 전용. 이게 가장 큰 벽입니다. Swift export는 iOS 프레임워크를 Xcode 프로젝트에 연결할 때 direct integration을 쓰는 프로젝트에서만 동작합니다. iOS 통합 방식 문서를 보면 통합 경로는 다섯 갈래입니다 — direct integration, 로컬 Swift 패키지, 로컬 podspec CocoaPods(여기까지 local), 그리고 SwiftPM + XCFramework, CocoaPods + XCFramework(remote). Swift export는 이 중 하나만 지원합니다. 게다가 direct integration 자체가 "KMP 프로젝트에 CocoaPods 의존성을 import하지 않는 경우"에 쓰는 방식입니다. 정리하면 이렇습니다.
- 공유 모듈을 XCFramework로 말아 별도 iOS 저장소에 던지는 조직 — 해당 없음.
- KMP 쪽에서 CocoaPods 의존성을 당겨 쓰는 프로젝트 — 해당 없음.
- IntelliJ IDEA의 KMP 플러그인이나 웹 마법사로 만든 모노레포 — 해당 됨.
이 제약은 새것도 아닙니다. Swift export가 처음 기본 제공된 Kotlin 2.2.20 릴리스 노트에 이미 같은 문장이 적혀 있었고, Alpha가 된 2.4.0 시점의 문서에도 그대로 남아 있습니다. 세 릴리스를 건너오는 동안 이 벽은 움직이지 않았습니다.
제네릭. 문서의 주석이 짧고 단호합니다 — "Generic types are generally not supported." 그리고 Swift로 내보낼 때 Kotlin 제네릭 타입 파라미터는 상한(upper bound)으로 타입 소거됩니다. 여기서 아이러니가 하나 생깁니다. 제네릭에 관한 한 Swift export는 아직 Objective-C 다리보다 나은 게 아닙니다. Objective-C는 적어도 클래스 위에서는 lightweight generics를 실어 나르는데, Swift export는 상한으로 지워 버리니까요. 앞 절에서 본 Sample<T> 예제의 널 가능성 문제가 해결되는 대신 타입 파라미터 자체가 사라지는 셈입니다.
클래스 제약. Swift export는 Any를 직접 상속하는 final 클래스만 지원합니다. class Foo() 같은 것들이죠. 즉 상속 계층이 있는 도메인 모델이나 sealed 계층을 그대로 내보낼 수 없습니다. 내보내진 클래스는 Swift에서 KotlinRuntime.KotlinBase를 상속한 클래스가 됩니다.
크로스 언어 상속 불가. Swift 클래스가 Kotlin에서 내보낸 클래스나 인터페이스를 직접 서브클래싱할 수 없습니다. Kotlin 쪽에 인터페이스를 정의하고 Swift에서 구현해 넘기는 패턴을 쓰고 있었다면 그대로는 못 옮깁니다.
컬렉션 상속 타입. List, Set, Map을 상속하는 타입은 export 과정에서 무시되고(KT-80416), 그 상속 타입들은 Swift 쪽에서 인스턴스화할 수 없습니다(KT-80417). 두 이슈 모두 2026년 7월 16일 현재 미해결이고, 상태는 Backlog입니다. Backlog는 "다음 릴리스에 잡혀 있다"가 아니라 "잡혀 있지 않다"는 뜻입니다. 참고로 두 티켓의 실제 제목은 문서의 요약보다 구체적입니다 — 각각 "function with receiver of MutableList is not translated"와 "ByteArray is impossible to instantiate"입니다. 후자를 보면 ByteArray 같은 흔한 타입이 걸린다는 게 드러납니다.
IDE 지원 없음. 문서가 명시합니다 — "No IDE migration tips or automation are available." Objective-C 경로에서 옮겨 오는 걸 도와주는 도구가 없습니다. 손으로 합니다.
opt-in의 번거로움. opt-in이 필요한 선언(예: kotlinx.datetime)을 쓰면 모듈 레벨에 명시적 optIn 컴파일러 옵션을 별도 블록으로 추가해야 합니다.
operator 제한. operator 수식자가 붙은 함수 지원은 현재 제한적이라고 문서가 적어 뒀습니다.
Int가 Int32가 되는 문제
제약 목록에는 안 들어 있지만, 매핑 표를 실제로 읽어 보면 눈에 걸리는 게 있습니다.
| Kotlin | Swift |
|---|---|
Int | Int32 |
Long | Int64 |
Char | Unicode.UTF16.CodeUnit |
Any | KotlinBase 클래스 |
Unit | Void |
Nothing | Never |
Kotlin의 Int는 Swift의 Int가 아니라 Int32로 갑니다. 타입 관점에서는 정확합니다 — Kotlin Int는 32비트가 맞고, Swift의 Int는 64비트 플랫폼에서 64비트니까 이걸 Int로 매핑하면 거짓말이 됩니다. 문서 예제가 보여 주는 결과는 이렇습니다.
// Kotlin
class MyClass {
val property: Int = 0
}
// Swift
public class MyClass : KotlinRuntime.KotlinBase {
public var property: Swift.Int32 { get }
}
정확하지만, Swift 쪽 호출부 입장에서는 관용적이지 않습니다. Swift 코드에서 정수를 다루는 기본형은 Int이고, 배열 인덱스도 Int이고, SwiftUI API도 Int를 받습니다. Kotlin에서 넘어온 Int32를 쓰려면 경계마다 변환이 붙습니다. KotlinInt 박싱이 사라진 자리에 Int32 변환이 들어온 셈입니다 — 훨씬 싼 거래인 건 분명하지만, "Swift에서 관용적으로 호출한다"는 목표가 아직 완전히 달성되진 않았다는 표시이기도 합니다. Char가 Unicode.UTF16.CodeUnit이 되는 것도 같은 종류의 정직한 불편입니다.
2.4.0의 나머지 Kotlin/Native 변화
Swift export만 본 김에, 같은 릴리스에서 Kotlin/Native에 들어간 것들도 짧게 정리해 둡니다. 이쪽이 오히려 지금 당장 여러분 빌드에 영향을 줍니다.
CMS GC가 기본이 됨. Kotlin 2.0.20에서 실험적으로 들어왔던 동시 마크 앤 스위프(CMS) GC가 2.4.0부터 기본값이 됐습니다. 기존 기본값이던 PMCS(parallel mark concurrent sweep)는 힙의 객체를 마킹하는 동안 애플리케이션 스레드를 멈춰야 했지만, CMS는 마킹 단계를 애플리케이션 스레드와 동시에 돌립니다. 릴리스 노트는 이것이 GC 일시정지 시간과 앱 반응성을 크게 개선한다고 적고, 근거로 Compose Multiplatform UI 앱 벤치마크를 듭니다 — JetBrains 자체 측정이고, 릴리스 노트 본문에는 구체적 수치가 제시돼 있지 않습니다. 문제가 생기면 gradle.properties에 kotlin.native.binary.gc=pmcs로 되돌릴 수 있습니다.
Apple 타깃 최소 버전 상향. 이건 조용한 파괴적 변경입니다. iOS와 tvOS가 14.0에서 15.0으로, macOS가 11.0에서 12.0으로, watchOS가 7.0에서 8.0으로 올랐습니다. 더 낮은 버전을 지원해야 한다면 freeCompilerArgs에 -Xoverride-konan-properties=minVersion.ios=14.0 같은 옵션을 걸어야 합니다.
LLVM 21. Kotlin/Native의 LLVM이 19에서 21로 올라갔습니다. 릴리스 노트는 코드에 영향을 주지 않아야 한다고 적고 있습니다.
devirtualization 메모리 감소. 링크 릴리스 태스크가 특히 큰 프로젝트에서 메모리를 너무 많이 먹던 문제에 개선이 들어갔습니다. 릴리스 노트의 표현은 조심스럽게 옮길 필요가 있습니다 — "EAP 사용자 중 한 명의 벤치마크에 따르면" 링크 릴리스 태스크의 메모리 소비가 절반으로 줄어 최소 13GB를 절약했다고 합니다. 즉 JetBrains가 전한 사용자 1인의 자체 측정치이고, 프로젝트 규모·환경 조건은 공개되지 않았습니다. 여러분 프로젝트에서 같은 폭이 나온다는 보장은 전혀 없습니다.
Swift 패키지 import(Experimental). Gradle 설정에서 Swift 패키지를 iOS 앱의 의존성으로 선언할 수 있게 됐습니다. swiftPMDependencies 블록에 swiftPackage(url = ..., version = ..., products = ...) 형태로 적습니다. 로드맵 항목(KT-53877)은 아직 Open이고, 이 기능의 등급은 Alpha가 아니라 Experimental입니다.
그래서, 지금 써야 하나
판단은 이렇게 갈립니다.
지금 켜 볼 만한 경우
- 새로 시작하는 KMP 프로젝트이고, direct integration 모노레포 구성이고, CocoaPods 의존성이 없다.
- 공유 모듈의 공개 API가 단순하다 — 최상위 함수, final 데이터 클래스, enum,
suspend함수,Flow. 제네릭 컨테이너나 상속 계층을 Swift로 내보내지 않는다. - Swift 경계면이 계속 바뀌어도 감당할 수 있다. 파괴적 변경은 예상되는 게 아니라 예고된 것입니다.
- 피드백을 남길 의사가 있다. Alpha 단계에서 JetBrains가 검증하려는 "가설"에 표를 던지는 셈입니다.
켜면 안 되는 경우
- 공유 모듈을 XCFramework로 배포하거나 CocoaPods를 쓴다 — 애초에 안 됩니다. 선택지가 아니라 사실입니다.
- 공개 API에 제네릭이 있다. 상한으로 소거되고, 문서가 "일반적으로 지원되지 않는다"고 적어 둔 영역입니다.
- Swift 쪽에서 Kotlin 인터페이스를 구현하는 패턴을 쓴다 — 크로스 언어 상속이 없습니다.
- 이미 Objective-C 경로가 돌아가고 있고 제품 일정이 있다. 지금 옮겨서 얻는 건 문법 미학과
AsyncSequence이고, 잃는 건 안정성 보장과 IDE 지원입니다. Beta 약속조차 아직 없는 등급으로 프로덕션 경계면을 옮길 이유가 없습니다.
현실적인 자세는 이렇습니다. Swift export는 방향이 옳고, 2.4.0의 코루틴·Flow 지원으로 처음으로 "실제 API 표면을 감당할 수 있는" 모양이 됐습니다. 그러니 브랜치 하나 파서 켜 보고, 여러분의 공유 모듈 공개 API가 저 제약 목록에 몇 번 걸리는지 세어 보십시오. 그 숫자가 여러분 팀의 마이그레이션 준비도이자, 다음 릴리스 노트를 읽을 때 무엇을 찾아야 하는지 알려 주는 체크리스트입니다. 프로덕션 브랜치를 옮기는 건 그다음 이야기고, 최소한 Beta 이후의 이야기입니다.
마치며
Kotlin Multiplatform이 iOS에서 오래 안고 있던 문제는 언어가 둘이 아니라 셋이었다는 것입니다. Objective-C 다리는 Kotlin의 타입을 Objective-C가 표현 가능한 부분집합으로 투영했고, 그 손실이 KotlinInt 박싱, 제네릭 소거, 컴플리션 핸들러로 나타났습니다. Swift export는 그 다리를 걷어냅니다. 2.2.20에서 Experimental로 등장해, 2.3.0에서 enum과 vararg를 챙기고, 2.4.0에서 코루틴과 Flow를 얻으며 Alpha가 됐습니다.
동시에 정직하게 볼 것도 분명합니다. JetBrains 자신의 정의로 Alpha는 프로덕션 레디가 되어야 하는지를 아직 시험 중인 단계이고, 가설이 틀리면 중단될 수도 있는 단계입니다. direct integration 전용이라는 제약은 세 릴리스 동안 문장 하나 안 바뀐 채 남아 있고, 제네릭은 오히려 옛 다리보다 못하며, IDE 지원은 없습니다. 공개 로드맵에는 Beta 항목이 없습니다.
그래서 결론은 이렇게 정리됩니다. Swift export는 KMP의 iOS 이야기가 끝난다는 신호가 아니라, 제대로 시작됐다는 신호입니다. 지금 할 일은 마이그레이션이 아니라 측정입니다 — 여러분의 API가 저 벽에 몇 번 부딪히는지 세어 두고, 2026년 8월 로드맵 갱신을 기다리십시오.
참고 자료
- What's new in Kotlin 2.4.0 — Kotlin/Native, Swift export goes Alpha
- Interoperability with Swift using Swift export — 기능, 매핑 표, Current limitations
- Stability of Kotlin components — Alpha/Beta/Stable 등급 정의
- Interoperability with Swift/Objective-C — 프리미티브 박싱, 제네릭 제약, suspend 매핑
- What's new in Kotlin 2.3.0 — enum·vararg export 개선
- What's new in Kotlin 2.2.20 — Swift export 최초 기본 제공(Experimental)
- iOS integration methods — direct integration과 나머지 네 경로
- Kotlin roadmap (2026년 2월 기준, 다음 갱신 2026년 8월)
- Kotlin 2.4.0 릴리스 태그 (2026-06-03)
- Kotlin/swift-export-sample — 설정이 끝난 공식 샘플
- KT-47610 — Objective-C 경로의 suspend/async 개선(Open)
- KT-80305 — Support coroutines in Swift Export (2.4.0-Beta2에서 Fixed)
- 모던 Kotlin 2026 — K2, Compose Multiplatform, Ktor 전반(관련 글)
Kotlin Swift Export Reaches Alpha — It Is Tearing Down the Objective-C Bridge, but Do Not Cross Yet
- Introduction — The Tax KMP Pays on iOS
- What the Objective-C Bridge Actually Breaks
- What Swift Export Does
- What 2.4.0 Changed — the Promotion, and Coroutines
- What "Alpha" Means in JetBrains's Dictionary
- What It Still Cannot Do
- The Problem of Int Becoming Int32
- The Rest of 2.4.0's Kotlin/Native Changes
- So, Should You Use It Now
- Closing
- References
Introduction — The Tax KMP Pays on iOS
Kotlin Multiplatform's sales pitch has always been clean: write your business logic once, and Android and iOS share it. On the Android side that actually holds — same language, same toolchain.
The problem has always been the iOS boundary. What Kotlin/Native produces is not something Swift can read directly. Kotlin emits an Objective-C framework, and Swift retranslates that Objective-C header into an API it can see. In other words, wedged between Kotlin and Swift is a third language nobody asked for. Both Kotlin and Swift are modern languages that encode null-safety in their type systems, and the interpreter sitting between them is a language from 1984.
This translation cost is not abstract. As you'll see below, types get flattened, names get mangled, and coroutines regress into callbacks. Swift export is an attempt to remove this bridge outright, and it reached Alpha in Kotlin 2.4.0 (released June 3, 2026).
Reaching Alpha is good news. But once you check what "Alpha" precisely means in JetBrains's dictionary, the urge to file a migration ticket right now will fade. This post lays out both sides.
What the Objective-C Bridge Actually Breaks
First, we need to see why this feature exists at all — these are losses that Kotlin's own Objective-C interop documentation admits to.
Primitive boxing. In the documentation's own words, a box of kotlin.Int is represented in Swift as a KotlinInt class instance, and these classes derive from NSNumber. The reverse does not happen automatically — the docs state explicitly that NSNumber is not automatically converted to a Kotlin primitive when used as a Swift/Objective-C parameter or return type. The reason is that NSNumber does not statically carry information about whether the wrapped value is a Byte, a Boolean, or a Double, so it must be cast manually. Passing a single nullable integer produces a heap-allocated wrapper, and manual casting code on the Swift side.
Generic loss. Objective-C's "lightweight generics" are too narrow to hold Kotlin's generics. The docs pin down two constraints. First, generics can only be defined on classes, not on interfaces (Objective-C/Swift protocols) or functions. Second, nullability gets flattened. Taking the docs' own example, this Kotlin code
class Sample<T>() {
fun myVal(): T
}
looks like this in Swift:
class Sample<T>() {
fun myVal(): T?
}
T became T?, because the generated Objective-C header has no choice but to declare myVal as nullable. The workaround the docs recommend is to put an explicit T : Any upper bound so the header can mark it non-null — which is to say, twist your Kotlin API design to accommodate the Objective-C header's limitations.
Coroutines become callbacks. Kotlin's suspend functions appear in the generated Objective-C header as functions that take a callback — in Swift terms, a completion handler. Since Swift 5.5 there's been a path to call them as async functions, but the docs describe that path as "highly experimental" with specific constraints, and point to KT-47610 for details. As of the writing of this post, July 16, 2026, that issue is still Open. It's filed under the Language Design and Native subsystems, tagged ObjC Export. This issue, open for years now, condenses exactly why a separate path called Swift export was needed.
Name mangling. This is the motivation the Swift export docs state for themselves — getting rid of Objective-C's confusing underscores and mangled names. If you've ever seen top-level functions show up attached to some class like SharedKt, you know exactly what this means.
To sum up, the Objective-C bridge cannot carry a Kotlin API across as-is — it projects onto the subset that Objective-C can express. The loss was unavoidable by design.
What Swift Export Does
Swift export's idea is simple: remove the intermediate language.
The official documentation's opening line says it precisely — Swift export exports Kotlin source directly so it can be called idiomatically from Swift, and gets rid of the Objective-C header. The compiler generates the swiftmodule file, the static .a library, the header file, and the modulemap file directly, and copies them into the app's build directory.
Configuration is a single Gradle block.
// build.gradle.kts
kotlin {
iosArm64()
iosSimulatorArm64()
swiftExport {
// root module name
moduleName = "Shared"
// strip the package prefix from the generated Swift code
flattenPackage = "com.example.sandbox"
// export configuration for an external module
export(project(":subproject")) {
moduleName = "Subproject"
flattenPackage = "com.subproject.library"
}
}
}
And in Xcode's Build Phases, you swap the Run Script's task from embedAndSignAppleFrameworkForXcode to embedSwiftExportForXcode. The script the docs give is ./gradlew :<module-name>:embedSwiftExportForXcode.
Some of the losses from the previous section genuinely go away. Here's what the docs list.
- Better primitive nullability. Unlike Objective-C interop, which had to box types like
Int?into aKotlinIntwrapper, Swift export converts nullability information directly. - Multi-module support. One Kotlin module maps to one Swift module.
- Package preservation. Kotlin packages are explicitly preserved to avoid name collisions (translated into nested Swift enums).
- Overloads. Kotlin's overloaded functions can be called from Swift without ambiguity.
- Type aliases.
typealiascarries over into Swift as-is.
And two more things were added in Kotlin 2.3.0 (December 16, 2025). Kotlin enum class stopped going out as a plain Swift class the way it used to, and started mapping to a genuine native Swift enum; and vararg functions started mapping directly to Swift's variadic parameters.
What 2.4.0 Changed — the Promotion, and Coroutines
The Kotlin 2.4.0 release notes call out two things as the core of this change.
First, structured concurrency. Kotlin's suspend functions and suspend function types now go out as Swift's idiomatic async — not as completion handlers.
// Kotlin
suspend fun hello(): String {
delay(1000)
return "Hello Swift! This is Kotlin."
}
// Swift
let msg = try await hello()
Second, Flow export. This one matters more in practice. As the docs point out precisely, up until now the only way to expose kotlinx.coroutines.flow's Flow interface to Swift was a third-party solution. Now Flow goes out mapped to Swift's AsyncSequence by default, with type information preserved.
// Kotlin
fun flowOfStrings(): Flow<String> = flowOf("hello", "any", "world")
// Swift
var actual: [String] = []
// String type is correctly inferred from Kotlin
for try await element in flowOfStrings().asAsyncSequence() {
actual.append(element)
}
Anyone who has actually run KMP in production knows the weight of that one line. "How do I plug a Kotlin-side state stream into SwiftUI" has always been a question every team adopting KMP had to face, and the answer up to now was always a third-party wrapper.
It's also worth noting this is on by default. Swift export itself is opt-in, but within it, Flow export doesn't need to be separately switched on.
What the promotion's baseline actually was is interesting too. The item filed on the Kotlin roadmap under the label "Swift Export: Alpha release" is KT-80305, and the ticket's actual title is "Support coroutines in Swift Export." It was closed as Fixed on March 24, 2026, and landed in 2.4.0-Beta2. In other words, the threshold for Alpha was, in effect, "do coroutines work." Put the other way around, this also means that as long as coroutines didn't work, JetBrains itself judged that this feature could not carry the real API surface of an iOS app.
What "Alpha" Means in JetBrains's Dictionary
Here's where we need to be honest. In many projects Alpha is roughly a marketing word for "coming soon," but Kotlin has this grade defined in its docs. Carrying over the components-stability documentation's definition verbatim:
Alpha means "we are testing whether this should become production-ready." And the explanation that follows is the key part: there's intent to make it a product, and the shape is still being finalized while validating user value and market fit; the feature set is still incomplete and breaking changes are expected; and "we may significantly change or discontinue the feature" if the hypothesis does not hold up.
Comparing this against the definition of the grade right above it, Beta, makes the gap clear. Beta is "you can use it, we'll do our best to minimize migration issues for you" — that carries a promise. Alpha carries no such promise. The Kotlin docs group Experimental, Alpha, and Beta together and call them pre-stable.
And one more sentence the docs add is worth quoting directly — the stability grade says nothing about how quickly that component will ship as Stable. In other words, reaching Alpha does not mean Beta or Stable is anywhere close.
The Swift export documentation itself carries the same tone: "currently in Alpha and still incomplete, so breaking changes are expected."
The currently published Kotlin roadmap shows its last update as February 2026, with the next update scheduled for August 2026. The Swift export item on that roadmap runs only up to the Alpha release, and no item targeting Beta or Stable is listed. Nor does the release notes for 2.4.20-Beta1 (June 24, 2026), the EAP after 2.4.0, mention any Swift export change. Until the next roadmap update, there is no basis in public material to estimate a Beta date. Better to say there isn't one than to invent a date that doesn't exist.
One more thing — JetBrains itself did not heavily promote Swift export in this release. The Kotlin/Native summary line in the 2.4.0 release blog post reads, in full: "Support for Swift packages as dependencies, updates on Swift export, and the CMS GC enabled by default." "Updates on Swift export" — the fact that the Alpha promotion wasn't pulled out as a headline is itself a signal.
What It Still Cannot Do
Gathering the constraints scattered across the docs' "Current limitations" section and its inline notes gives us this list. I've attached how each one actually hurts on a real project.
Direct integration only. This is the biggest wall. Swift export only works in projects that use direct integration to connect their iOS framework into an Xcode project. Per the iOS integration methods documentation, there are five integration paths — direct integration, a local Swift package, local podspec CocoaPods (these three are "local"), and SwiftPM + XCFramework, CocoaPods + XCFramework ("remote"). Swift export supports only one of these. On top of that, direct integration itself is the approach used when a KMP project "does not import a CocoaPods dependency." To sum up:
- Organizations that package their shared module as an XCFramework and throw it into a separate iOS repo — not applicable.
- Projects that pull in CocoaPods dependencies on the KMP side — not applicable.
- A monorepo built with IntelliJ IDEA's KMP plugin or the web wizard — applicable.
This constraint isn't new, either. The same sentence was already in the release notes for Kotlin 2.2.20, when Swift export first shipped by default, and it's still there unchanged in the docs as of 2.4.0, where it reached Alpha. Across three releases, this wall hasn't moved.
Generics. The docs' note here is short and blunt — "Generic types are generally not supported." When exporting to Swift, a Kotlin generic type parameter is type-erased to its upper bound. There's an irony here: as far as generics go, Swift export is not yet better than the Objective-C bridge. Objective-C at least carries lightweight generics over classes, whereas Swift export erases them to the upper bound. In exchange for solving the nullability problem seen in the Sample<T> example earlier, the type parameter itself disappears.
Class constraints. Swift export only supports final classes that directly inherit from Any — things like class Foo(). That means domain models with an inheritance hierarchy, or sealed hierarchies, cannot be exported as-is. An exported class becomes, in Swift, a class that inherits from KotlinRuntime.KotlinBase.
No cross-language inheritance. A Swift class cannot directly subclass a class or interface exported from Kotlin. If you've been using the pattern of defining an interface on the Kotlin side and implementing it on the Swift side, that pattern can't move over as-is.
Collection-inheriting types. Types that inherit List, Set, or Map are ignored during export (KT-80416), and those inheriting types cannot be instantiated on the Swift side (KT-80417). Both issues are unresolved as of July 16, 2026, and their status is Backlog. Backlog means "not scheduled for a release," not "scheduled for the next one." For reference, the two tickets' actual titles are more specific than the docs' summary — they read, respectively, "function with receiver of MutableList is not translated" and "ByteArray is impossible to instantiate." The latter shows that even a common type like ByteArray runs into this.
No IDE support. The docs state it plainly — "No IDE migration tips or automation are available." There's no tooling to help you move off the Objective-C path. You do it by hand.
opt-in friction. If you use a declaration that requires opt-in (e.g. kotlinx.datetime), you have to add an explicit optIn compiler option in a separate block at the module level.
operator restrictions. The docs note that support for functions marked with the operator modifier is currently limited.
The Problem of Int Becoming Int32
This isn't on the constraints list, but if you actually read the mapping table, something jumps out.
| Kotlin | Swift |
|---|---|
Int | Int32 |
Long | Int64 |
Char | Unicode.UTF16.CodeUnit |
Any | KotlinBase class |
Unit | Void |
Nothing | Never |
Kotlin's Int doesn't map to Swift's Int — it maps to Int32. From a type-correctness standpoint this is accurate — Kotlin's Int really is 32-bit, and Swift's Int is 64-bit on 64-bit platforms, so mapping it to Int would be a lie. Here's what the docs' example produces.
// Kotlin
class MyClass {
val property: Int = 0
}
// Swift
public class MyClass : KotlinRuntime.KotlinBase {
public var property: Swift.Int32 { get }
}
Correct, but not idiomatic from the Swift call site's point of view. In Swift code, the default type for handling integers is Int, array indices are Int, and SwiftUI APIs take Int. To use an Int32 coming from Kotlin, you get a conversion at every boundary. Where KotlinInt boxing used to sit, Int32 conversion now sits — a much cheaper trade, no question, but also a sign that the goal of "calling idiomatically from Swift" isn't fully achieved yet. Char becoming Unicode.UTF16.CodeUnit is the same kind of honest inconvenience.
The Rest of 2.4.0's Kotlin/Native Changes
Since we're already looking at Swift export, here's a quick rundown of what else landed in Kotlin/Native in the same release. This part will actually hit your build sooner.
CMS GC is now the default. The concurrent mark-and-sweep (CMS) GC, which arrived experimentally in Kotlin 2.0.20, is the default starting with 2.4.0. The previous default, PMCS (parallel mark concurrent sweep), had to pause application threads while marking objects on the heap, whereas CMS runs its marking phase concurrently with application threads. The release notes state this significantly improves GC pause times and app responsiveness, citing benchmarks on Compose Multiplatform UI apps as evidence — this is JetBrains's own measurement, and the release notes body does not give concrete figures. If you run into trouble, you can revert with kotlin.native.binary.gc=pmcs in gradle.properties.
Apple target minimum versions raised. This is a quiet breaking change. iOS and tvOS went from 14.0 to 15.0, macOS from 11.0 to 12.0, and watchOS from 7.0 to 8.0. If you need to support a lower version, you have to set an option like -Xoverride-konan-properties=minVersion.ios=14.0 in freeCompilerArgs.
LLVM 21. Kotlin/Native's LLVM went from 19 to 21. The release notes state this should not affect your code.
Reduced devirtualization memory. An improvement landed for the problem where the link-release task ate too much memory, especially on very large projects. The release notes' wording needs to be carried over carefully — "according to one EAP user's benchmark," the link-release task's memory consumption was cut in half, saving at least 13GB. That is, it's a self-reported figure from a single user relayed by JetBrains, and the project's scale and environment conditions were not disclosed. There is no guarantee at all that your project will see the same magnitude of improvement.
Swift package import (Experimental). You can now declare a Swift package as a dependency of an iOS app from Gradle configuration. It's written in a swiftPMDependencies block, in the form swiftPackage(url = ..., version = ..., products = ...). The roadmap item (KT-53877) is still Open, and this feature's grade is Experimental, not Alpha.
So, Should You Use It Now
The decision splits like this.
Worth switching on now, if
- You're starting a new KMP project, it's a direct-integration monorepo setup, and you have no CocoaPods dependency.
- Your shared module's public API is simple — top-level functions, final data classes, enums,
suspendfunctions,Flow. You are not exporting generic containers or inheritance hierarchies to Swift. - You can absorb the Swift boundary continuing to shift. Breaking changes aren't a risk here — they're announced.
- You're willing to leave feedback. Turning it on at the Alpha stage is effectively casting a vote on the "hypothesis" JetBrains is trying to validate.
Don't turn it on, if
- You ship your shared module as an XCFramework, or use CocoaPods — this simply isn't possible. That's not a choice, it's a fact.
- Your public API has generics. They get erased to the upper bound, and this is an area the docs say is "generally not supported."
- You use the pattern of implementing a Kotlin interface on the Swift side — there's no cross-language inheritance.
- Your Objective-C path already works and you're on a product timeline. What you'd gain by moving now is syntactic elegance and
AsyncSequence; what you'd lose is stability guarantees and IDE support. There's no reason to move a production boundary onto a grade that doesn't even carry Beta's promise yet.
The practical stance is this: Swift export is pointed in the right direction, and with 2.4.0's coroutine and Flow support, it has, for the first time, taken a shape that can carry a real API surface. So cut a branch, switch it on, and count how many times your shared module's public API hits the constraints listed above. That number is both your team's migration readiness and a checklist of what to look for the next time you read a release notes page. Moving a production branch is a separate conversation, and at minimum, one for after Beta.
Closing
The problem Kotlin Multiplatform has long carried on iOS is that there weren't two languages involved — there were three. The Objective-C bridge projected Kotlin's types onto the subset Objective-C could express, and that loss showed up as KotlinInt boxing, generic erasure, and completion handlers. Swift export tears that bridge down. It appeared as Experimental in 2.2.20, picked up enums and varargs in 2.3.0, and reached Alpha in 2.4.0 by gaining coroutines and Flow.
At the same time, there's plenty to look at honestly. By JetBrains's own definition, Alpha is a stage where it's still being tested whether this should become production-ready, and one where the feature could be discontinued if the hypothesis doesn't hold. The direct-integration-only constraint has sat unchanged, sentence for sentence, across three releases; generics are, if anything, worse off than under the old bridge; and there's no IDE support. There's no Beta item on the public roadmap.
So the conclusion lands here: Swift export is not a signal that KMP's iOS story is ending — it's a signal that it's properly beginning. What to do right now isn't migration, it's measurement — count how many times your API runs into that wall, and wait for the August 2026 roadmap update.
References
- What's new in Kotlin 2.4.0 — Kotlin/Native, Swift export goes Alpha
- Interoperability with Swift using Swift export — features, mapping table, Current limitations
- Stability of Kotlin components — definitions of Alpha/Beta/Stable grades
- Interoperability with Swift/Objective-C — primitive boxing, generic constraints, suspend mapping
- What's new in Kotlin 2.3.0 — enum/vararg export improvements
- What's new in Kotlin 2.2.20 — Swift export ships by default for the first time (Experimental)
- iOS integration methods — direct integration and the other four paths
- Kotlin roadmap (as of February 2026, next update August 2026)
- Kotlin 2.4.0 release tag (2026-06-03)
- Kotlin/swift-export-sample — the official pre-configured sample
- KT-47610 — suspend/async improvements on the Objective-C path (Open)
- KT-80305 — Support coroutines in Swift Export (Fixed in 2.4.0-Beta2)
- Modern Kotlin 2026 — K2, Compose Multiplatform, Ktor and more (related post)