저날 교육외근 신청으로 Naver에서 주최한 Deview행사를 참여했었다.(사실 내가 하는 현업과는 크게 상관없었지만 Naver
잖아, 한잔해~)
Kotlin
Multiplatform 적용기SNOW
AI FILTER미디어 플레이어인 Prizm Player 운영/개발하며 부족했던 리소스를 커버하고 확장성을 위해 Kotlin으로 멀티플랫폼으로 전향하며 겪은 일화를 바탕으로 원리, 구현법, 이슈 등을 공유하는 세션이었으며, 코틀린으로 멀티플랫폼 지원이 된다는 점이 아주 매력적으로 느껴졌었다.
결론만 말하면 비즈니스로직만 코틀린으로 공유 가능하고 Android, iOS 의 UI 코드는 각 플랫폼 코드를 사용해야한다!
코틀린, JVM말고도 구현가능한 방식은 많다. 그리고 각 플랫폼 주력 언어로 빌드되기 때문에 유의미한 성능 저하가 없었다. 대신, 다른 MultiPlatform 방식보다는 꽤 불친절한 방식임.(직접 구현해. 근데! 언어는 Kotlin이야)
하지만 다른 방식은 너무 친절했기 때문에 실무에는 쓰이기 어렵지 않겠냐는 역발상으로 선택하게 됐다고 함.(다 알 필요 없고 세팅 이정도만 하면 나머지는 내가 다 만들어줄게!)
동작 원리: Kotlin Shared 코드를 작성하고, 이 코드가 컴파일되는데 자동적으로 플랫폼에 대한 코드로 컴파일되는 것이다. 예를 들면, JVM 위에서 코드가 컴파일되면, 그 컴파일된 코드는 Android나 JVM 플랫폼 위에서 동작하는 것이다.
CI/CD
프로세스를 해치지 않아야함공통화
가능할 것선택권은 두 가지 안이 있었다고 함.
이미 코틀린으로 작성되어있던 안드로이드 코드 위주로 참고(아주 많은 도움이 되었다고 함)
타입스크립트, 스위프트 등으로 작성된 코드도 필요시 활용이 됐었고 공유 코드를 극대화하기 위한 pure Kotlin
을 지향했다고 함. 그렇지 않은 경우, expect/actual
로 각 플랫폼 기능 연동 적용.
만약에 플랫폼별로 코드를 구현하려면 아래과 같이 분기처리를 해야할 것이다.
if(platform == "iOS"){
}else if(platform =="android") {
}
하지만 코틀린
에서는 expect/actual
키워드를 제공한다. 예를 들면, 공통으로 사용하는 모듈에서 expect
로 선언
해놓고
expect fun getPlatform(): String
각 플랫폼에서 actual
로 연동하면 된다.
actual fun getPaltform():String = “Android ${Build.VERSION.RELEASE}“
actual fun getPlatform():String = UIDevice.currentDevice.run {“$systemName $systemVersion“}
actual fun getPlatform(): String = window.navigator.userAgent.toPlatform()
private fun String.toPlatform(): String = …
일단 expect
키워드는 함수 이름이나 클래스 앞에 선언이 가능하다. 플랫폼별로 expect
되는 함수나 클래스에 선언한다.
actual
키워드는 플랫폼 별
로 common
의 코드에서 expect
로 선언한 동일한 함수나 클래스 이름으로 선언한다. 매칭되는 expect
정의에 실제 구현이 필요하다.
expect
로 정의되었던 모든 클래스나 함수는 플랫폼별로 actual
정의를 가져야한다.
expect 함수나 클래스는 구현되지 않는다.
external modifier
를 통한 javascript
코드 접근
kotlin-stdlib-js
에 선언되어있는 것을 활용하거나 직접 정의[kotlin-stdlib-js ]
/** * Exposes the [console API](https://developer.mozilla.org/en/DOM/console ) to Kotlin. */
@Suppress("NOT_DOCUMENTED")
public external interface Console {
public fun dir(o: Any): Unit
public fun error(vararg o: Any?): Unit
public fun info(vararg o: Any?): Unit
public fun log(vararg o: Any?): Unit
public fun warn(vararg o: Any?): Unit
}
/** * Exposes the [console API](https://developer.mozilla.org/en/DOM/console ) to Kotlin. */
public external val console: Console
console.log(“Hello, World“)
JS()function
을 통해 JavaScript
코드 직접 호출도 가능
const shakaPlayer = js(“new Shaka.Player(element)“)
fun <T> buildObject(block: T.() → Unit) : T =
(js(“{}“).unsafeCast<T>()).apply(block)
NPM
을 통해 원하는 패키지를 Kotlin
코드로 불러와 사용할 수 있음.
Kotlin
코드에 external
도 별도 정의해야함.
external
는 C++
로 작성된 코드를 JNI
를 통해 호출하거나 JavaScript
코드를 호출
할 때 사용한다.
external fun foo (...) {...}
근데 매번 external
로 직접 정의할 필요 없이 Dukat사용하면 external
코드가 자동생성된다.
String
변환 기능을 제공한다.SDK를 개발할 때는 라이브러리 사용에 더욱 더 신중해야함.
플랫폼 별 테스트 실행 gradle task
./gradlew :core:testDebugUnitTest –tests “path.to.CharsetsTest.testUtf8“
./gradlew :core:iosSimulatorArm64Test --tests “path.to.CharsetsTest.testUtf8“
./gradlew :core:jsBrowserTest --tests “path.to.CharsetsTest.testUtf8“
iOS에서 테스트시 Simulator를 찾지 못하는 경우가 있다고 함.
그럴 땐 Simulator
기기 하드 코딩 이슈라고 한다. Kotlin 1.8 미만
에서는 -xcrun simctl list
를 활용하여 iOS Simulator deviceId
주입 후 터미널에 xcrun simctl list devices available
명령어로 조회 가능하다.
Kotlin/JS
는 계층 모듈 구조가 아직 불가함. → 배포 타겟에 따라 별도 gradle
모듈을 만듦.Kotlin Multiplatform
에서 iOS빌드 시 모듈별로 코드 복사gradle
모듈에서 멀티모듈의 종속성을 갖고 하나의 바이너리로 묶어서 배포 Kotlin
버전이 더 낮을 경우Kotlin Metadata
가 호환되지 않아 에러 발생Kotlin/JS 및 Kotlin/Native
개선사항이 많기 때문에 낮게만 유지할 수 없음api version
및 Language version
을 낮게 유지하여 빌드 시 낮은 버전에서 컴파일monorepo
root/app
→ root/app-android/app
CODEOWNERS
: 특정 디렉토리/파일에 대해 소유권을 부여하여 PR 강제 리뷰어 지정]sparse-checkout
: 특정 디렉토리/파일만 체크아웃할 수 있는 기능Kotlin Compiler
버전 의존성compiler plugin
을 사용하는 라이브러리에 의존성이 있으면 문제 발생 가능Coroutines/Compose/Kotlinx Serialization/Kotlin SymbolProcessing
의존성
이 다른 경우낮은
버전인 경우expect / actual
선언으로 별도 ui구현custom composable
제작ViewModel(expect)
→ actual(android, desktop, …)
→ ViewModelImpl(서비스 공통 비즈니스로직)
↔︎ View
interface RemoteBaseViewModel {
val coroutineScope: CoroutineScope
}
expect abstract class RemoteMainViewModel() : RemoteBaseViewModel {
override val coroutineScope: CoroutineScope
}
class RemoteMainViewModelImpl() : RemoteMainViewModel() {
private val _isEditMode = MutableStateFlow(false)
val isEditMode: StateFlow<Boolean> get() = _isEditMode
private val _decks = SynchronizedStateList<RemoteDeckModel>()
val decks: List<RemoteDeckModel> get() = _decks.list
.
.
.
private override fun loadShortcuts() {
coroutineScope.launch {repo.loadBroadcasts()}
.
.
.
}
}
이 세션 저 세션 듣다가 본받아야 할 점(?)인지는 모르겠으나 결국 거인의 어깨위에 누가 제일 높게 올라서냐의 싸움인 것 같더라. 세상에서 수요가 제일 많은 언어나 플랫폼의 의존 및 업데이트를 빠르게 따라가는 서비스 일수록 개발자가 능력을 인정받을 수 있는 기회가 넓어진다는 것을 다시 한번 깨닫게 되었고
1일차 세션 중에
CFCs Reactive
SSR환경의 micro-frontEnd 구현과 캐시전략
이 두가지는 발표자료를 보며 공부해보고 싶은 아이템이었는데 직접 보지 못하여 아쉬웠다.