Upgrade to Pro — share decks privately, control downloads, hide ads and more …

플레이어 SDK 개발자의 Kotlin Multiplatform 도입기

Sponsored · Your Podcast. Everywhere. Effortlessly. Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.

플레이어 SDK 개발자의 Kotlin Multiplatform 도입기

네이버 공통 플레이어 SDK에 KMP를 도입한 경험과 고민했던 내용을 공유합니다.
Droid Knights 2024 발표 자료입니다.

https://festa.io/events/4990

Avatar for mojs | 모진섭

mojs | 모진섭

June 11, 2024
Tweet

Other Decks in Programming

Transcript

  1. ❏ 다양한 서비스의 여러 플랫폼을 지원 ❏ 서비스 기술 지원

    및 유지 보수가 팀 리소스의 많은 부분을 차지 ❏ 사업적으로 중요한 신규 과제를 원활하게 진행하기 어려움 팀 리소스 문제
  2. ❏ 기능 1개당 플랫폼 별 N벌 개발 ❏ 개발자와 비용이

    늘어 신규 플랫폼에 대한 확대 필요 시 의사 결정이 어려움 ❏ 각 플랫폼 개발자의 스펙에 대한 이해가 미묘하게 다름 ❏ 기능의 다양성을 만들어냄 -> 커뮤니케이션 비용 상승 중복 개발
  3. ❏ 당시 KMM (현 KMP) alpha 릴리즈 ❏ Android +

    iOS ❏ 때마침 Google Cast의 Custom Web Receiver 개발이 필요했음 ❏ Kotlin/JS로 개발하면서 점진적으로 공유 코드를 늘려 확장해보기로 함 ❏ 동시에 다른 플랫폼도 Prototype으로 검증 Kotlin Multiplatform?!
  4. external class CastReceiverContext { ... fun start(options: CastReceiverOptions = definedExternally):

    CastReceiverContext fun stop() companion object { fun getInstance(): CastReceiverContext } }
  5. /** * Manages loading of underlying libraries and initializes underlying

    cast receiver SDK. * @see https://developers.google.com/cast/docs/reference/web_receiver/cast.framework.CastReceiverContext */ export class CastReceiverContext { /** * Returns the CastReceiverContext singleton instance. */ static getInstance(): CastReceiverContext; ... /** * Initializes system manager and media manager; so that receiver app can receive requests from senders. */ start(options?: CastReceiverOptions): CastReceiverContext; /** * Shutdown receiver application. */ stop(): void; }
  6. external open class CastReceiverContext { ... open fun start(options: CastReceiverOptions

    = definedExternally): CastReceiverContext open fun stop() companion object { fun getInstance(): CastReceiverContext } }
  7. class Formatter(val name: String) { fun format(n: Int): String {

    return "$name: $n" } fun format(o: Any): String { return "$name: $o" } }
  8. @JsExport class Formatter(val name: String) { fun format(n: Int): String

    { return "$name: $n" } fun format(o: Any): String { return "$name: $o" } }
  9. @JsExport class Formatter(val name: String) { @JsName("formatInt") fun format(n: Int):

    String { return "$name: $n" } fun format(o: Any): String { return "$name: $o" } }
  10. function Formatter(name) { this.name_1 = name; } Formatter.prototype.get_name_woqyms_k$ = function

    () { return this.name_1; }; Formatter.prototype.formatInt = function (n) { return this.name_1 + ': ' + n; }; Formatter.prototype.format = function (o) { return this.name_1 + ': ' + toString(o); }; Formatter.$metadata$ = classMeta('Formatter'); Object.defineProperty(Formatter.prototype, 'name', { configurable: true, get: Formatter.prototype.get_name_woqyms_k$ });
  11. function Formatter(name) { this.name_1 = name; } Formatter.prototype.get_name_woqyms_k$ = function

    () { return this.name_1; }; Formatter.prototype.formatInt = function (n) { return this.name_1 + ': ' + n; }; Formatter.prototype.format = function (o) { return this.name_1 + ': ' + toString(o); }; Formatter.$metadata$ = classMeta('Formatter'); Object.defineProperty(Formatter.prototype, 'name', { configurable: true, get: Formatter.prototype.get_name_woqyms_k$ });
  12. function Formatter(name) { this.name_1 = name; } Formatter.prototype.get_name_woqyms_k$ = function

    () { return this.name_1; }; Formatter.prototype.formatInt = function (n) { return this.name_1 + ': ' + n; }; Formatter.prototype.format = function (o) { return this.name_1 + ': ' + toString(o); }; Formatter.$metadata$ = classMeta('Formatter'); Object.defineProperty(Formatter.prototype, 'name', { configurable: true, get: Formatter.prototype.get_name_woqyms_k$ });
  13. @JsExport class Formatter(@get:JsName("key") val name: String) { @JsName("formatInt") fun format(n:

    Int): String { return "$name: $n" } fun format(o: Any): String { return "$name: $o" } }
  14. function Formatter(name) { this.name_1 = name; } Formatter.prototype.key = function

    () { return this.name_1; }; Formatter.prototype.formatInt = function (n) { return this.name_1 + ': ' + n; }; Formatter.prototype.format = function (o) { return this.name_1 + ': ' + toString(o); }; Formatter.$metadata$ = classMeta('Formatter'); Object.defineProperty(Formatter.prototype, 'name', { configurable: true, get: Formatter.prototype.key });
  15. function Formatter(name) { this.name_1 = name; } Formatter.prototype.key = function

    () { return this.name_1; }; Formatter.prototype.formatInt = function (n) { return this.name_1 + ': ' + n; }; Formatter.prototype.format = function (o) { return this.name_1 + ': ' + toString(o); }; Formatter.$metadata$ = classMeta('Formatter'); Object.defineProperty(Formatter.prototype, 'name', { configurable: true, get: Formatter.prototype.key });
  16. class Formatter { constructor(name) { return new.target.new_Formatter_p3ihin_k$(name); } static new_Formatter_p3ihin_k$(name,

    $box) { var $this = (0,_kotlin_kotlin_stdlib_mjs__WEBPACK_IMPORTED_MODULE_0__.createThis2j2avj17cvnv2)(this, $box); $this.name = name; return $this; } key() { return this.name; } formatInt(n) { return this.name + ': ' + n; } format(o) { return this.name + ': ' + (0,_kotlin_kotlin_stdlib_mjs__WEBPACK_IMPORTED_MODULE_0__.toString1pkumu07cwy4m)(o); } }
  17. sealed interface State sealed interface Event interface Player { val

    state: StateFlow<State> val events: Flow<Event> }
  18. sealed interface State sealed interface Event interface Source interface Player

    { val state: StateFlow<State> val events: Flow<Event> suspend fun load(source: Source) }
  19. class MediaPlayerImpl : Player { private val player = MediaPlayer.create(context,

    uri, surfaceHolder) override fun play() { player.start() } }
  20. class ExoPlayerImpl : Player { private val player = ExoPlayer.Builder(context).build().apply

    { setVideoSurfaceHolder(surfaceHolder) setMediaItem(MediaItem.fromUri(uri)) prepare() } override fun play() { player.playWhenReady = true } }
  21. class AVPlayerImpl: Player { private let player: AVPlayer = {

    let playerItem = AVPlayerItem(url: url) let player = AVPlayer(playerItem: playerItem) playerLayer.player = player return player }() func play() { player.play() } }
  22. // PlatformPlayer.android.kt actual class PlatformPlayer : Player { private val

    player = ExoPlayerImpl() override fun play() { player.play() } }
  23. class AVPlayerImpl : Player { private val player: AVPlayer =

    createPlayer() private fun createPlayer(): AVPlayer { val playerItem = AVPlayerItem(uRL = url) val player = AVPlayer(playerItem = playerItem) playerLayer.player = player return player } override fun play() { player.play() } }
  24. // PlatformPlayer.ios.kt actual class PlatformPlayer : Player { private val

    player = AVPlayerImpl() override fun play() { player.play() } }
  25. ❏ HLS 스트리밍 중 미디어 관련 정보를 metadata로 전달 ❏

    이것에 활용되는 ID3 metadata를 파싱하기 위한 ID3 tag 파서 구현 필요 ❏ 텍스트 관련 ID3 Frame 중 첫 번째 Byte는 encoding을 나타냄 Timed Metadata for HLS
  26. ❏ kotlin-stdlib는 ByteArray -> UTF-8 String 변환 기능 제공 ❏

    그 외 다른 encoding에 대한 지원은 없었음 Kotlin 문자열 디코딩
  27. /** * Decodes a string from the bytes in UTF-8

    encoding in this array. * * Malformed byte sequences are replaced by the replacement char `\uFFFD`. */ @SinceKotlin("1.4") @WasExperimental(ExperimentalStdlibApi::class) public expect fun ByteArray.decodeToString(): String
  28. ❏ kotlinx-io는 obsolete 였음 ❏ 게다가 모든 플랫폼에 대한 UTF-16,

    ISO-8859-1 디코딩 지원하지 않았음 ❏ 이 부분을 직접 구현하기로 함 Kotlin 문자열 디코딩
  29. internal fun Charset.toNSStringEncoding(): NSStringEncoding = when (this) { Charsets.UTF_8 ->

    NSUTF8StringEncoding Charsets.UTF_16 -> NSUTF16StringEncoding Charsets.UTF_16BE -> NSUTF16BigEndianStringEncoding Charsets.UTF_16LE -> NSUTF16LittleEndianStringEncoding Charsets.ISO_8859_1 -> NSISOLatin1StringEncoding else -> throw UnsupportedEncodingException("Charset '${this.name}' is not supported.") } internal actual fun CharsetDecoder.decodeToStringImpl( input: ByteArray, fromIndex: Int, toIndex: Int, ): String { val encoding = _charset.toNSStringEncoding() val inputArray = if (fromIndex == 0 && toIndex == input.size) input else input.copyOfRange(fromIndex, toIndex) val data = inputArray.toNSData() @Suppress("CAST_NEVER_SUCCEEDS") return NSString.create(data, encoding) as? String ?: throw IOException("Failed to decode bytes. charset: $charset") }
  30. ❏ 하나의 테스트 코드만 작성해도 모든 플랫폼 테스트 가능 ❏

    플랫폼별 동작을 맞추기 용이 하나의 테스트 코드를 멀티플랫폼으로
  31. ❏ KMP 적용 범위는 선택하기 나름 ❏ 일부 코어 로직만

    공유 ❏ UI 상태 관리를 포함한 비즈니스 로직 공유 ❏ UI까지 Kotlin으로 작성 꼭 모든 것을 공유하지 않아도 된다
  32. ❏ Compose Multiplatform ❏ UI까지 Kotlin으로 작성하여 여러 플랫폼 지원

    ❏ Kotlin/wasm (O), Kotlin/JS (X) ❏ 플레이어 SDK에도 UI는 있다 ❏ UI 컴포넌트, 데모앱 모든 것을 공유할 수도 있다
  33. ❏ Android는 Compose, iOS는 Swift UI ❏ Web은 Kotlin/JS를 쓰거나

    직접 Typescript로 구현 ❏ 선택의 폭이 넓다 UI만 각 플랫폼 native 방식으로 구현할 수 있다
  34. ❏ 각 플랫폼 특화된 개발자들이 파트별로 구성 ❏ 기능 1개당

    플랫폼별 N벌 개발 더 효율적인 팀 리소스 운영
  35. ❏ 멀티플랫폼을 도입해도 여전히 플랫폼 특화 개발자 필요 ❏ 하지만

    공유 코드에 리소스 집중 가능 ❏ 플랫폼별 코드 중복을 줄이고 통합 더 효율적인 팀 리소스 운영
  36. ❏ 설계 및 스펙 차이로 플랫폼 간 커뮤니케이션 ❏ 각

    플랫폼에서 개별적으로 다른 팀과 논의 더 효율적인 커뮤니케이션
  37. ❏ KMP 프로젝트 구성을 위한 새로운 툴 ❏ 기존 빌드

    시스템은 모듈, 플랫폼, 종속성 변경이 있을 때 세팅이 오래걸림 ❏ YAML을 활용하여 선언적으로 프로젝트를 구성할 수 있음 ❏ 아직 초기 인큐베이팅 단계 Amper
  38. ❏ Frederick Brooks가 1986년에 발표한 논문 ❏ ʻ소프트웨어 개발의 본질적인

    어려움을 해결할 단 하나의 기적적인 해결책은 없다’ ❏ KMP 또한 좋은 도구지만, 모든 문제를 해결해주진 못함 No Silver Bullet
  39. ❏ 멀티플랫폼 도입에 회의적인 팀원은 분명 있다 ❏ 누군가는 새로운

    환경을 즐기지만, 그렇지 않은 사람도 있다 ❏ 리더 설득하기? ❏ 우리 팀은 운 좋게도 먼저 제안을.. 팀원 설득하기
  40. ❏ 팀 리소스 문제로 시작했던 KMP 도입 ❏ KMP 도입

    자체에도 많은 리소스가 필요 ❏ 업무 우선순위에 따라 뒤로 밀리는 일이 부지기수 다시 팀 리소스 문제
  41. ❏ 기존 플레이어 구조를 개선하고 싶었으나 호환성 때문에 시도하기 어려움

    ❏ KMP 도입하면서 다 뜯어고쳐보자?! ❏ 개발 볼륨이 너무 커짐 + 다시 돌아온 리소스 문제 더 잘 하려는 욕심..?
  42. ❏ 기존 프로젝트에서 마이그레이션 한다면 적용 범위에 대한 고민 필수

    ❏ 공유 코드를 활용한 작은 제품 출시 ❏ 작은 공통 모듈 만들어 적용해보기 작게 시작하자
  43. ❏ 멀티플랫폼 꼭 필요한가요? ❏ 여기서 No를 외친다면 필요 없음

    ❏ 프로젝트의 크기, 팀원 구성, 공감대에 따라 선택 KMP 도입해도 괜찮은가?
  44. ❏ 팀원 구성 ❏ Android 개발자 등 Kotlin에 익숙한 팀원이

    적극적으로 참여 필요 ❏ 그래야만 원활한 진행 가능 ❏ Kotlin에 익숙한 개발자가 적은 경우 진행이 더딜 가능성이 높음 KMP 도입해도 괜찮은가?
  45. ❏ 새로운 기술에 대한 열린 자세 ❏ Android 외 타

    플랫폼 개발자도 Kotlin에 대해 알아가고 싶은 마음이 있어야 ❏ 반대로 Android 개발자도 타 플랫폼에 열린 자세 ❏ 시작은 Kotlin만으로 가능하겠지만, 결국 각 플랫폼 전문 지식 필요 ❏ Kotlin은 이를 잘 엮어주는 역할 KMP 도입해도 괜찮은가?