Compose Multiplatform: 절망편 - Case Study1: 네이티브 상태 옵저버 구현 - Case Study2: KMP 미지원 외부 라이브러리(Firebase) 연동 + Compose Multiplatform 아키텍처 설계 - Case Study3: iOS 모듈에 CocoaPods 설치 - KotlinConf 2025에서 JetBrains 개발자에게 피드백 받은 썰 +) 팀이 얻은 러닝 +) 구체적인 코드 레벨의 고민
개발을 위한 기술 스택 선택 필요 Compose Multiplatform: 희망편 서X수님 (Android 개발자) 정X준님 (Android 개발자) 나 (Android 개발자) 우아한테크코스 인하우스 앱 (내부 교육생 출결 관리 시스템) 3개월 내에 출시 필요 우리는 iOS 개발자가 없음 백엔드 개발자도 Kotlin 스택 우아한테크코스 앱 개발 (내부 교육생 출결 관리 시스템) 2개월 내에 출시 필요 팀에 iOS 개발자가 없음 백엔드 개발자도 Kotlin 스택 우리팀 모바일 개발자 구성
개발을 위한 기술 스택 선택 필요 서X수님 (Android 개발자) 정X준님 (Android 개발자) 나 (Android 개발자) 우리팀 모바일 개발자 구성 우아한테크코스 인하우스 앱 (내부 교육생 출결 관리 시스템) 3개월 내에 출시 필요 우리는 iOS 개발자가 없음 백엔드 개발자도 Kotlin 스택 우아한테크코스 앱 개발 (내부 교육생 출결 관리 시스템) 2개월 내에 출시 필요 팀에 iOS 개발자가 없음 백엔드 개발자도 Kotlin 스택 이거 완전 Compose Multiplatform하기 딱 좋은 환경인 걸? Compose Multiplatform: 희망편
출시, 운영 - 약 2개월간의 개발 끝에 MVP 버전 iOS/Android 각 플랫폼에 출시 완료 - Compose UI 100% - Firebase Distribution, TestFlight 등으로 배포 - 150명의 교육생 대상으로 운영 - 올해 5월에 Compose Multiplatform 1.8.0 출시되며 iOS도 Stable 단계로 격상
Notifications) 푸시 알림이 기기로 전달되는 상황. 사용자가 이를 탭하면 애플리케이션은 특정 화면으로 이동하는 등 공유 모듈에 위치한 비즈니스 로직을 트리거 해야 한다. 하지만 푸시 알림은 네이티브 코드가 필수적으로 필요하므로 네이티브 의존성 없이 다루기는 어렵다. Compose Multiplatform: 절망편 expect/actual API를 활용하기 어려운 상황
Update Check) 앱 스토어 또는 플레이 스토어에서 필수 업데이트가 있는지 확인해야 한다. 이는 플랫폼마다 다르게 처리되는 비동기 네트워크 호출을 수반한다. 플랫폼 코드는 이 작업을 백그라운드에서 수행하고 완료되면 콜백 또는 리스너를 통해 commonMain에 결과를 전달해야 한다. 하지만 commonMain의 단순 함수 호출로는 비동기적 지연 응답을 처리할 수 없다. Compose Multiplatform: 절망편 expect/actual API를 활용하기 어려운 상황
Firebase Remote Config) 네이티브 Firebase SDK를 사용하여 Remote Config 상태를 가져온다. 상태가 변경되면 공유된 Compose UI를 리컴포지션하여 변경사항 반영해야 한다. 코드가 Firebase 상태를 요청하는 것이 아니라, 상태 변경 시 Firebase SDK가 코드에 알려야 한다. 즉, 공유 코드는 능동적인 호출자가 아닌, 수동적인 리스너 역할을 해야 한다. Compose Multiplatform: 절망편 expect/actual API를 활용하기 어려운 상황 출처: Observer Pattern from refactoring.guru
data class ViewState(val requiredAppVersionCode: Long = 0) private val state = MutableStateFlow(ViewState()) fun mainViewController(onFinish: () -> Unit): UIViewController { return ComposeUIViewController { // This subscribes to the StateFlow state. val viewState by state.collectAsStateWithLifecycle() if (viewState.requiredAppVersionCode > currentAppVersionCode) { ... } App() } } // This is called from SwiftUI. fun updateRequiredAppVersionCode(code: Long) { state.value = state.value.copy(requiredAppVersionCode = code) } } [변경 후]
data class ViewState(val requiredAppVersionCode: Long = 0) private val state = MutableStateFlow(ViewState()) fun mainViewController(onFinish: () -> Unit): UIViewController { return ComposeUIViewController { // This subscribes to the StateFlow state. val viewState by state.collectAsStateWithLifecycle() if (viewState.requiredAppVersionCode > currentAppVersionCode) { ... } App() } } // This is called from SwiftUI. fun updateRequiredAppVersionCode(code: Long) { state.value = state.value.copy(requiredAppVersionCode = code) } } [변경 후] • StateFlow 는 UI State의 Single Source Of Truth 역할 • mainViewController()는 UIViewController를 리턴하는 Swift에서의 진입점 • collectAsStateWithLifecycle() 는 StateFlow 를 Compose의 상태로 변환하며 UI가 항상 최신값을 노출하도록 함
data class ViewState(val requiredAppVersionCode: Long = 0) private val state = MutableStateFlow(ViewState()) fun mainViewController(onFinish: () -> Unit): UIViewController { return ComposeUIViewController { // This subscribes to the StateFlow state. val viewState by state.collectAsStateWithLifecycle() if (viewState.requiredAppVersionCode > currentAppVersionCode) { ... } App() } } // This is called from SwiftUI. fun updateRequiredAppVersionCode(code: Long) { state.value = state.value.copy(requiredAppVersionCode = code) } } [변경 후] 1. updateRequiredAppVersionCode()가 SwiftUI에서 트리거되면 2. Kotlin 코드에서 StateFlow를 업데이트한 후, UI 리컴포지션이 트리거됨
연동 [요구사항] • Firebase Crashlytics 연동 • 모니터링용 Logging 인터페이스 구현 • Firebase는 KMP SDK를 지원하지 않으므로 플랫폼별 네이티브 SDK 설치 필요 Firebase 등 KMP를 지원하지 않는 외부 라이브러리를 연동해야 하는 상황
recordException(error: NSError) } // shared/iosMain/Log.ios.kt actual object Log { private var crashlyticsDelegate: FirebaseCrashlyticsDelegate? = null fun initCrashlyticsDelegate(delegate: FirebaseCrashlyticsDelegate) { crashlyticsDelegate = delegate } // ... } 1. commonMain 또는 shared 모듈에서 인터페이스 정의 - 공유 코드에 필요한 함수 정의하는 간단한 Kotlin 인터페이스 생성
func recordException(error: Error) { Crashlytics.crashlytics().record(error: error) } } 1. commonMain 또는 shared 모듈에서 인터페이스 정의 - 공유 코드에 필요한 함수 정의하는 간단한 Kotlin 인터페이스 생성 2. androidMain 및 iosMain 소스 세트(또는 네이티브 Swift 및 Kotlin 코드)에서 실제 Firebase SDK를 네이티브 코드로 래핑하여 이 인터페이스를 구현하는 클래스를 생성
{ func application(_ application: UIApplication,did…Options launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { FirebaseApp.configure() Log.shared.doInitCrashlyticsDelegate(delegate: Default…Delegate()) } // ... } 1. commonMain 또는 shared 모듈에서 인터페이스 정의 - 공유 코드에 필요한 함수 정의하는 간단한 Kotlin 인터페이스 생성 2. androidMain 및 iosMain 소스 세트(또는 네이티브 Swift 및 Kotlin 코드)에서 실제 Firebase SDK를 네이티브 코드로 래핑하여 이 인터페이스를 구현하는 클래스를 생성 3. 애플리케이션 시작 시 네이티브 코드(예: iOS의 AppDelegate)가 구현체의 인스턴스를 생성하여 shared 모듈에 전달, 이후 shared 모듈은 이 구현체에 대한 참조를 들고 있고, 앱 전반에서 사용됨
{ func application(_ application: UIApplication,did…Options launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { FirebaseApp.configure() Log.shared.doInitCrashlyticsDelegate(delegate: Default…Delegate()) } // ... } 1. commonMain 또는 shared 모듈에서 인터페이스 정의 - 공유 코드에 필요한 함수 정의하는 간단한 Kotlin 인터페이스 생성 2. androidMain 및 iosMain 소스 세트(또는 네이티브 Swift 및 Kotlin 코드)에서 실제 Firebase SDK를 네이티브 코드로 래핑하여 이 인터페이스를 구현하는 클래스를 생성 3. 애플리케이션 시작 시 네이티브 코드(예: iOS의 AppDelegate)가 구현체의 인스턴스를 생성하여 shared 모듈에 전달, 이후 shared 모듈은 이 구현체에 대한 참조를 들고 있고, 앱 전반에서 사용됨 여기서의 shared는 Kotlin 싱글톤 object와 Swift/Objective-C의 interop(상호 운용) API
연동 [요구사항] • Firebase Crashlytics 연동 • 모니터링용 Logging 인터페이스 구현 • Firebase는 KMP SDK를 지원하지 않으므로 플랫폼별 네이티브 SDK 설치 필요 Firebase 등 KMP를 지원하지 않는 외부 라이브러리를 연동해야 하는 상황 = 해결 완료 TODO: Koin 등 DI 라이브러리 통합
연동 저수준 shared 모듈에서의 문제점 ⚠ iOS용으로 별도의 프레임워크(.xcframework)로 컴파일된 shared 모듈과 composeApp 모듈이 서로 다른 메모리 영역에서 작동하는 것으로 보인다. 명시적인 문서는 찾을 수 없었지만, Delegate 객체를 전달하고 해시 코드를 로깅했을 때 구현체의 전달부와 참조부가 각각 다른 값이였다. -> 인스턴스의 메모리 주소가 다르다. // A function in our `shared` module to receive the delegate fun initCrashlyticsDelegate(delegate: FirebaseCrashlyticsDelegate) { crashlyticsDelegate = delegate // When called from iosApp, this prints one hash code NSLog("Delegate hash in 'shared': ${delegate.hashCode()}") } // Later, when accessing this delegate from a ViewModel in `composeApp`... // This would print a DIFFERENT hash code, implying it's not the same instance. NSLog("Delegate hash in 'composeApp': ${crashlyticsDelegate.hashCode()}")
연동 결론3: 다른 대안 살펴보기 - Firebase 팀에게 KMP 지원해 달라고 직접 목소리 내기: https://firebase.uservoice.com/forums/948424-general/suggestions/46591717-support-k otlin-multiplatform-kmp-in-the-sdks - 비공식 Firebase Kotlin SDK 사용: https://github.com/GitLiveApp/firebase-kotlin-sdk - KMP를 지원하는 다른 로깅 SDK 선택 (예: Sentry): https://docs.sentry.io/platforms/kotlin/guides/kotlin-multiplatform ⬆ 블로그에서 링크 바로 접근하기
덴마크 코펜하겐에서 열린 KotlinConf 2025에서 직접 JetBrains 부스를 찾아가다 Compose Multiplatform 커뮤니티 내에서 확립된 모범 사례와 심층적인 아키텍처 논의 부족 -> 이를 만든 개발자들, 즉 JetBrains의 개발자들로부터 직접적인 피드백을 수집하는 것이 좋겠다!
덴마크 코펜하겐에서 열린 KotlinConf 2025에서 직접 JetBrains 부스를 찾아가다 Compose Multiplatform 커뮤니티 내에서 확립된 모범 사례와 심층적인 아키텍처 논의 부족 -> 이를 만든 개발자들, 즉 JetBrains의 개발자들로부터 직접적인 피드백을 수집하는 것이 좋겠다! 1. 나: JetBrains 부스 방문해서 써드파티 라이브러리 구현 방법 질문
덴마크 코펜하겐에서 열린 KotlinConf 2025에서 직접 JetBrains 부스를 찾아가다 Compose Multiplatform 커뮤니티 내에서 확립된 모범 사례와 심층적인 아키텍처 논의 부족 -> 이를 만든 개발자들, 즉 JetBrains의 개발자들로부터 직접적인 피드백을 수집하는 것이 좋겠다! 1. 나: JetBrains 부스 방문해서 써드파티 라이브러리 구현 방법 질문 2. JetBrains 개발자1: standard C-interop 접근 방식 설명
덴마크 코펜하겐에서 열린 KotlinConf 2025에서 직접 JetBrains 부스를 찾아가다 Compose Multiplatform 커뮤니티 내에서 확립된 모범 사례와 심층적인 아키텍처 논의 부족 -> 이를 만든 개발자들, 즉 JetBrains의 개발자들로부터 직접적인 피드백을 수집하는 것이 좋겠다! 1. 나: JetBrains 부스 방문해서 써드파티 라이브러리 구현 방법 질문 2. JetBrains 개발자1: standard C-interop 접근 방식 설명 3. JetBrains 개발자2: “Firebase SDK는 해당 접근 방식을 따를 수 없다”
덴마크 코펜하겐에서 열린 KotlinConf 2025에서 직접 JetBrains 부스를 찾아가다 Compose Multiplatform 커뮤니티 내에서 확립된 모범 사례와 심층적인 아키텍처 논의 부족 -> 이를 만든 개발자들, 즉 JetBrains의 개발자들로부터 직접적인 피드백을 수집하는 것이 좋겠다! 1. 나: JetBrains 부스 방문해서 써드파티 라이브러리 구현 방법 질문 2. JetBrains 개발자1: standard C-interop 접근 방식 설명 3. JetBrains 개발자2: “Firebase SDK는 해당 접근 방식을 따를 수 없다” 4. 나: 우리팀 아키텍처 설명, “리뷰해 주세요^^”
덴마크 코펜하겐에서 열린 KotlinConf 2025에서 직접 JetBrains 부스를 찾아가다 Compose Multiplatform 커뮤니티 내에서 확립된 모범 사례와 심층적인 아키텍처 논의 부족 -> 이를 만든 개발자들, 즉 JetBrains의 개발자들로부터 직접적인 피드백을 수집하는 것이 좋겠다! 1. 나: JetBrains 부스 방문해서 써드파티 라이브러리 구현 방법 질문 2. JetBrains 개발자1: standard C-interop 접근 방식 설명 3. JetBrains 개발자2: “Firebase SDK는 해당 접근 방식을 따를 수 없다” 4. 나: 우리팀 아키텍처 설명, “리뷰해 주세요^^” 5. JetBrains 개발자1, 2: “이것이 현재 가능한 최선의 실용적 해결책이다. 지금 당장 이것보다 더 나은 방법이 생각나지 않는다”
덴마크 코펜하겐에서 열린 KotlinConf 2025에서 직접 JetBrains 부스를 찾아가다 Compose Multiplatform 커뮤니티 내에서 확립된 모범 사례와 심층적인 아키텍처 논의 부족 -> 이를 만든 개발자들, 즉 JetBrains의 개발자들로부터 직접적인 피드백을 수집하는 것이 좋겠다! 1. 나: JetBrains 부스 방문해서 써드파티 라이브러리 구현 방법 질문 2. JetBrains 개발자1: standard C-interop 접근 방식 설명 3. JetBrains 개발자2: “Firebase SDK는 해당 접근 방식을 따를 수 없다” 4. 나: 우리팀 아키텍처 설명, “리뷰해 주세요^^” 5. JetBrains 개발자1, 2: “이것이 현재 가능한 최선의 실용적 해결책이다. 지금 당장 이것보다 더 나은 방법이 생각나지 않는다” 6. JetBrains 개발자2: “너 지금 여기 있을게 아니라 구글 부스 가서 KMP 공식 지원 해달라고 요구해라”
덴마크 코펜하겐에서 열린 KotlinConf 2025에서 직접 JetBrains 부스를 찾아가다 Compose Multiplatform 커뮤니티 내에서 확립된 모범 사례와 심층적인 아키텍처 논의 부족 -> 이를 만든 개발자들, 즉 JetBrains의 개발자들로부터 직접적인 피드백을 수집하는 것이 좋겠다! 1. 나: JetBrains 부스 방문해서 써드파티 라이브러리 구현 방법 질문 2. JetBrains 개발자1: standard C-interop 접근 방식 설명 3. JetBrains 개발자2: “Firebase SDK는 해당 접근 방식을 따를 수 없다” 4. 나: 우리팀 아키텍처 설명, “리뷰해 주세요^^” 5. JetBrains 개발자1, 2: “이것이 현재 가능한 최선의 실용적 해결책이다. 지금 당장 이것보다 더 나은 방법이 생각나지 않는다” 6. JetBrains 개발자2: “너 지금 여기 있을게 아니라 구글 부스 가서 KMP 공식 지원 해달라고 요구해라” 7. 다같이: 깔깔깔~ 😂
덴마크 코펜하겐에서 열린 KotlinConf 2025에서 직접 JetBrains 부스를 찾아가다 Compose Multiplatform 커뮤니티 내에서 확립된 모범 사례와 심층적인 아키텍처 논의 부족 -> 이를 만든 개발자들, 즉 JetBrains의 개발자들로부터 직접적인 피드백을 수집하는 것이 좋겠다! 1. 나: JetBrains 부스 방문해서 써드파티 라이브러리 구현 방법 질문 2. JetBrains 개발자1: standard C-interop 접근 방식 설명 3. JetBrains 개발자2: “Firebase SDK는 해당 접근 방식을 따를 수 없다” 4. 나: 우리팀 아키텍처 설명, “리뷰해 주세요^^” 5. JetBrains 개발자1, 2: “이것이 현재 가능한 최선의 실용적 해결책이다. 지금 당장 이것보다 더 나은 방법이 생각나지 않는다” 6. JetBrains 개발자2: “너 지금 여기 있을게 아니라 구글 부스 가서 KMP 공식 지원 해달라고 요구해라” 7. 다같이: 깔깔깔~ 😂 결론: KMP 커뮤니티는 발전중이고, 개발자들이 계속 좋은 방법을 연구해야 한다.
자잘한 버그는 있었지만, UI 관련 로직은 100% 공통 코드 (약 97%의 코드를 Kotlin으로 작성) - All Kotlin Project (Server + Client) - 팀 TODO: 단일 Kotlin 프로젝트 구축 - 서버와 클라이언트 모두 하나의 언어 -> 충분한 메리트 - iOS Stable로 격상 (Compose 1.8.0) - 사소한 UI 어긋남, 텍스트 크기 차이를 제외하곤 안정적 CMP 8개월 운영 결론
자잘한 버그는 있었지만, UI 관련 로직은 100% 공통 코드 (약 97%의 코드를 Kotlin으로 작성) - All Kotlin Project (Server + Client) - 팀 TODO: 단일 Kotlin 프로젝트 구축 - 서버와 클라이언트 모두 하나의 언어 -> 충분한 메리트 - iOS Stable로 격상 (Compose 1.8.0) - 사소한 UI 어긋남, 텍스트 크기 차이를 제외하곤 안정적 CMP 8개월 운영 결론 Compose Multiplatform은 Android 개발자가 가장 낮은 러닝 커브로 멀티플랫폼을 개발할 수 있는 스택
희망편 - Compose Multiplatform: 절망편 - Case Study1: 네이티브 상태 옵저버 구현 - Case Study2: KMP 미지원 외부 라이브러리(Firebase) 연동 + Compose Multiplatform 아키텍처 설계 - Case Study3: iOS 모듈에 SPM/CocoaPods 설치 - KotlinConf 2025에서 JetBrains 개발자에게 피드백 받은 썰 +) 팀이 얻은 러닝 +) 구체적인 코드 레벨의 고민