Naver Deview 2023 교육외근 후기

HumbleMincho·2024년 7월 17일
2
post-thumbnail

저날 교육외근 신청으로 Naver에서 주최한 Deview행사를 참여했었다.(사실 내가 하는 현업과는 크게 상관없었지만 Naver잖아, 한잔해~)

수강한 세션리스트

  1. 자바스크립트 화이트박스 암호와 크롬 라인 메신저의 보안 강화
  2. 웨일 브라우저 오픈 소스 생존기
  3. 그 여자 APP, 그 남자 SDK Kotlin Multiplatform 적용기
  4. SNOW AI FILTER
  5. Remember Me: 맞춤 케어를 위한 기억하기 챗봇

인상적인 세션 파헤치기 - Kotlin Multipaltform 적용기

개요

미디어 플레이어인 Prizm Player 운영/개발하며 부족했던 리소스를 커버하고 확장성을 위해 Kotlin으로 멀티플랫폼으로 전향하며 겪은 일화를 바탕으로 원리, 구현법, 이슈 등을 공유하는 세션이었으며, 코틀린으로 멀티플랫폼 지원이 된다는 점이 아주 매력적으로 느껴졌었다.


1. iOS와 Web을 Kotlin으로 구현이 가능한가? 왜 코틀린 멀티플랫폼인가?

결론만 말하면 비즈니스로직만 코틀린으로 공유 가능하고 Android, iOS 의 UI 코드는 각 플랫폼 코드를 사용해야한다!
코틀린, JVM말고도 구현가능한 방식은 많다. 그리고 각 플랫폼 주력 언어로 빌드되기 때문에 유의미한 성능 저하가 없었다. 대신, 다른 MultiPlatform 방식보다는 꽤 불친절한 방식임.(직접 구현해. 근데! 언어는 Kotlin이야)
하지만 다른 방식은 너무 친절했기 때문에 실무에는 쓰이기 어렵지 않겠냐는 역발상으로 선택하게 됐다고 함.(다 알 필요 없고 세팅 이정도만 하면 나머지는 내가 다 만들어줄게!)
동작 원리: Kotlin Shared 코드를 작성하고, 이 코드가 컴파일되는데 자동적으로 플랫폼에 대한 코드로 컴파일되는 것이다. 예를 들면, JVM 위에서 코드가 컴파일되면, 그 컴파일된 코드는 Android나 JVM 플랫폼 위에서 동작하는 것이다.


2. 멀티플랫폼 전환 결정을 위해 던진 다섯 개의 질문

  • 기존의 개발 경험/도구/설정을 최대한 유지
  • 기구축된 CI/CD 프로세스를 해치지 않아야함
  • UI개발도 공통화 가능할 것
  • 러닝 커브는 최소화해야함
  • 이득이 고통보다 클 것(단순하네)

3. SDK에서 개발한 방식

선택권은 두 가지 안이 있었다고 함.

  1. 기존 프로젝트의 기능 일부를 Kotlin Multiplatform으로 전환
    a. 검증된 기존 프로젝트 기반으로 하기에 리스크가 적음
    b. 플랫폼 별 서로 다른 설계 및 스펙으로 이를 맞추는 데에도 상당한 리소스
  2. 신규 Kotlin Multiplatform 프로젝트에 기존 기능 도입(선택)
    a. 차라리 각 플랫폼의 노하우를 취합하여 신규 설계하는 것이 나을 것 같음.
    b. 그동안 아쉬웠으나 호환성 이슈로 개선하지 못했던 API들을 개선해볼 계기

기존 구현을 참고하여 새로운 설계에 맞추어 개발

이미 코틀린으로 작성되어있던 안드로이드 코드 위주로 참고(아주 많은 도움이 되었다고 함)
타입스크립트, 스위프트 등으로 작성된 코드도 필요시 활용이 됐었고 공유 코드를 극대화하기 위한 pure Kotlin을 지향했다고 함. 그렇지 않은 경우, expect/actual로 각 플랫폼 기능 연동 적용.

expect/actual이란?

만약에 플랫폼별로 코드를 구현하려면 아래과 같이 분기처리를 해야할 것이다.

if(platform == "iOS"){
}else if(platform =="android") {
}

하지만 코틀린에서는 expect/actual 키워드를 제공한다. 예를 들면, 공통으로 사용하는 모듈에서 expect선언해놓고

expect fun getPlatform(): String

각 플랫폼에서 actual로 연동하면 된다.

[Android]

actual fun getPaltform():String = “Android ${Build.VERSION.RELEASE}

[iOS]

actual fun getPlatform():String = UIDevice.currentDevice.run {“$systemName $systemVersion“}

[JS]

actual fun getPlatform(): String = window.navigator.userAgent.toPlatform()
private fun String.toPlatform(): String =

일단 expect 키워드는 함수 이름이나 클래스 앞에 선언이 가능하다. 플랫폼별로 expect되는 함수나 클래스에 선언한다.

actual 키워드는 플랫폼 별common의 코드에서 expect 로 선언한 동일한 함수나 클래스 이름으로 선언한다. 매칭되는 expect 정의에 실제 구현이 필요하다.

expect로 정의되었던 모든 클래스나 함수는 플랫폼별로 actual 정의를 가져야한다.

expect 함수나 클래스는 구현되지 않는다.


4. Kotlin/JS

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

[kotlin]

console.log(“Hello, World“)

[javascript]

JS()function을 통해 JavaScript코드 직접 호출도 가능

const shakaPlayer = js(new Shaka.Player(element))

[kotlin]

fun <T> buildObject(block: T.() → Unit) : T =
(js({}).unsafeCast<T>()).apply(block) 

NPM을 통해 원하는 패키지를 Kotlin코드로 불러와 사용할 수 있음.
Kotlin 코드에 external도 별도 정의해야함.

external이란?

externalC++로 작성된 코드를 JNI를 통해 호출하거나 JavaScript 코드를 호출 할 때 사용한다.

external fun foo (...) {...}

근데 매번 external로 직접 정의할 필요 없이 Dukat사용하면 external 코드가 자동생성된다.


5. kotlin-stdlib는 byteArray → UTF-8 String 변환 기능을 제공한다.

  • 그 외 다른 encoding에 대한 지원은 아직 없다.
  • 게다가 모든 플랫폼에 대해 UTF-16, ISO-8859-1 디코딩을 지원하지 않음.

6. SDK에게 라이브러리 종속성이란?

SDK를 개발할 때는 라이브러리 사용에 더욱 더 신중해야함.

  • 다양한 곳에 적용되면서 버전 충돌(제일 흔함) 등으로 예기치 못한 이슈가 종종 발생

7. 테스트코드

플랫폼 별 테스트 실행 gradle task

[android]

./gradlew :core:testDebugUnitTest –tests “path.to.CharsetsTest.testUtf8“

[iosSimulatorArm64]

./gradlew :core:iosSimulatorArm64Test --tests “path.to.CharsetsTest.testUtf8“

[javaScript]

./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 명령어로 조회 가능하다.


8. Kotlin/Native는 Swift가 아닌 Objective-C로 컴파일함

  • 언제 개선될지 기약 없음
  • 그래서 코틀린으로 작성된 코드를 Swift로 감싸서 배포

9. Kotlin/JS 계층 모듈 구조 미지원

  • Kotlin/JS는 계층 모듈 구조가 아직 불가함. → 배포 타겟에 따라 별도 gradle 모듈을 만듦.

10. iOS 멀티모듈 지원 이슈

  • Kotlin Multiplatform에서 iOS빌드 시 모듈별로 코드 복사
  • 별도 gradle 모듈에서 멀티모듈의 종속성을 갖고 하나의 바이너리로 묶어서 배포

11. Android 배포 시 이슈

  • Kotlin Metadata 버전 이슈
    • SDK를 사용하는 앱의 Kotlin 버전이 더 낮을 경우
    • Kotlin Metadata가 호환되지 않아 에러 발생
    • Kotlin/JS 및 Kotlin/Native 개선사항이 많기 때문에 낮게만 유지할 수 없음
    • 대신 api versionLanguage version을 낮게 유지하여 빌드 시 낮은 버전에서 컴파일

12. 모듈 관리

  • 멀티-모듈 구조로 변경
    • 오래 운영된 앱일수록 싱글모듈로 구성
    • 기능 단위로 멀티모듈화: 코드 속성에 따라 네 가지 모듈로 분류

13. 저장소 관리

  • 플랫폼 별로 저장소를 운영하는 것이 일반적
    • 플랫폼 단위로 조직이 나눠진 경우
    • 운영/커뮤니케이션의 효율성
  • Kotlin Multiplatform 코드는 common code이기 때문에 위치가 애매함
  • 조직마다 상황에 따라 결정이 달라질 수 밖에 없음

해결법: monorepo

  • 멀티플랫폼/앱 모듈을 한 저장소에 모음
    • A monorepo is a single repository containing multiple distict projects, with well-defined relationships.
  • 저장소의 크기가 커지고 타 플랫폼 코드 수정이 가능한 문제점
    • 플랫폼별로 디렉토리 분리: root/approot/app-android/app
    • CODEOWNERS: 특정 디렉토리/파일에 대해 소유권을 부여하여 PR 강제 리뷰어 지정]
    • sparse-checkout: 특정 디렉토리/파일만 체크아웃할 수 있는 기능

14. 의존성 관리

  • Kotlin Compiler 버전 의존성
    • 코틀린은 빠른 속도로 발전하고 있는 언어임
    • compiler plugin을 사용하는 라이브러리에 의존성이 있으면 문제 발생 가능
    • 대표적 라이브러리: Coroutines/Compose/Kotlinx Serialization/Kotlin SymbolProcessing
  • 문제사례
    • 라이브러리 간 버전 의존성이 다른 경우
    • 코틀린보다 낮은 버전인 경우
  • 그냥 처음부터 코틀린 버전(/컴파일러)을 맞추고 개발을 시작하면 문제발생을 최소화할 수 있다.
    • 릴리즈 노트 확인하고 빌드하여 직접 확인
    • gradle이 제공하는 VersionCatalogs확인
      • 동일 프로젝트에서는 번들로 사용하도록 룰 셋
    • gralde plugin 구현

15. UI는 compose multiplatform으로 구현

  • 플랫폼별로 따로 제공되는 위젯이 있거나 별도의 구현방식이 존재한다면, expect / actual 선언으로 별도 ui구현
  • 모든 과정을 통해서도 해결하지 못하는 경우엔 모든 플랫폼이 공통으로 사용가능한 인터페이스를 이용하여 직접 custom composable제작

16. 적용한 ViewModel

  • ViewModel(expect)actual(android, desktop, …)ViewModelImpl(서비스 공통 비즈니스로직) ↔︎ View

[ViewModel]

interface RemoteBaseViewModel {
val coroutineScope: CoroutineScope
}
expect abstract class RemoteMainViewModel() : RemoteBaseViewModel {
override val coroutineScope: CoroutineScope
}

[ViewModeIImpl]

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 구현과 캐시전략

이 두가지는 발표자료를 보며 공부해보고 싶은 아이템이었는데 직접 보지 못하여 아쉬웠다.

profile
javascript, typescript, react-native, react.js, next.js

0개의 댓글