나/이슈

Devfest GDG Songdo 2023 후기

Lou Park 2023. 12. 10. 21:41

송도 컨벤시아

올해의 마지막 행사라고 생각하고 신청한 GDG Songdo 2023이다.

송도라 좀 멀긴하지만 발표 주제도 다양하고 알차보여서 신청했다.

 

난 도전적인 문제를 해결하고 싶은데, 안드로이드 개발을 하면서 화면만 뽑아대니 회의감이 들었던 시절도 있었다. 화면빼고 정말 문제들만 남는 DevOps나 백엔드로 일하고 싶었다.

하지만 최근에 문득 생각이 든게...

 

"잘하는 걸 즐기지 못한다는 건 인생을 하드모드로 살아가는 것"이라는 생각이 들었다.

정말 길을 걷다가 뿅-하고 떠올랐다. 잘하는 걸 더 잘해서 잘하는 자체로 재미있게 즐겨도 좋지않을까?

 

그래서 이번 발표는 안드로이드/KMP 관련 발표만 있는 gradle clean방에서 썩어보았다.

모두 양질의 발표로 정말 유용하게 잘 들었지만, 코드샘플이 있었던 몇 가지 발표에 대한 메모들을 적어보려한다.

 

유광무 : KMP로 번역기 만들기

기억은 확실히 나지 않지만 어떤 대회에서 출품하기 위해 1주일만에 번역기 앱인 Transer를 개발하셨고, 해당 프로젝트를 구현하면서 고민했던 점들을 공유해주셨다. PROCESS_TEXT를 이용해서 다른 앱 사용 중에도 번역이 가능하도록 한 아이디어가 좋았다! 회사에서 만드는 제품에도 어디에 적용하면 좋을까...잠깐 고민을 했다.

 

SQLDelight

그리고 SQLDelight라는 라이브러리의 존재도 여기서 처음 알았다. 이후 발표에서도 소개되었는데 KMP쪽에서는 Room마냥 Local DB를 다룰때 보편적으로 쓰이는 듯 하다. sq파일에 Query를 적어주면 빌드시에 Kotlin API로 만들어준다. Query는 Flow로도 받을 수 있고...

 

produceState

다음은 코드에서 produceState / awaitDispose가 잠깐 나왔는데 처음보는 것들이라 간략히 찾아봤다.

 

유광무님은 ViewModel을 iOS나 Desktop등 비 Android 플랫폼에서 사용하기 위해서 이를 사용했다. awaitDispose는 produceState 블록 안에서 Dispose할때 쓰는 것이고, produceState가 조금 더 중요하다.

 

아래는 Github에서 랜덤하게 가져온 하나의 예시 코드다. produceState는 State<T>를 반환한다.

이렇듯 Flow, LiveData, RxJava, Repository처럼 Composable이 아닌 것들, 즉 외부의 상태를 가져와 Compose 상태로 변환할때 사용할 수 있다. 일반적으로 많이들 쓰는 Flow<T>.collectAsState의 내부 구현 역시 produceState로 되어있고, 이 예시코드와 얼추 비슷하다.

val issPosition by produceState(initialValue = IssPosition(0.0, 0.0), repo) {
    repo.pollISSPosition().collect { value = it }
}

 

UseCase

아직 클린 아키텍쳐가 체화되지 않아서...켁켁 남들이 해놓은 것을 답습하고 있는데 종종 UseCase에서 다른 UseCase를 호출하고 싶을때가 있었다. 생각해보면 Domain Layer에 있으니 당연히 그걸 가져다 쓰면되는데 좀 생각을 덜했나보다... UseCase 구현 시 다른 UseCase를 이렇게 사용할 수 있다는 예시로서 이 코드가 눈에 들어왔다.

class TranslateUseCase(
    private val translationRepository: TranslationRepository,
    private val detectLanguageUseCase: DetectLanguageUseCase,
    private val getPreferencesUseCase: GetPreferencesUseCase
) {
    suspend operator fun invoke(q: String) =
        detectLanguageUseCase(q).language.let { language ->
            val (source, target) = getPreferencesUseCase().firstOrNull()
                ?: throw NullPointerException("Preferences is null")

            makeSourceTargetPair(language, target.language, source.language).let { (target, source) ->
                translationRepository.translate(q = q, target = target, source = source)
            }
        }

    private fun makeSourceTargetPair(language: String, target: String, source: String): Pair<String, String> =
        when(language) {
            "und" -> target to source
            target -> source to target
            !in listOf(target, source) -> source to language
            else -> target to source
        }
}

 

 

이상훈 : KMP 개발을 위한 알아두면 좋은 라이브러리 소개 / DI 프레임워크 찍먹하기

KMP 개발시에 많이들 쓰는 라이브러리들을 하나씩 설명해주셨다. KMP의 기초를 오늘 이 행사에서 처음 접해서 대부분은 이런게 있구나~하고 넘어갔는데 상태관리 라이브러리라고 해야할지...Orbit Multiplatform 예시코드가 좀 기억에 남았다. Orbit 공식 문서에 이 코드가 있어서 지금 같이 정리를 해보려고한다.

data class CalculatorState(
    val total: Int = 0
)

sealed class CalculatorSideEffect {
    data class Toast(val text: String) : CalculatorSideEffect()
}

먼저 상태와 SideEffect를 이렇게 정의를 해두었다.

 

개발하면서 SideEffect는 다 개별로 처리했는데 이렇게 모아둔게 나름의 충격이었다. ViewModel에서 String 접근 문제만 해결하면 정말 깔끔하게 SideEffect를 처리할 수 있는 방법일 것 같다.

class CalculatorViewModel: ContainerHost<CalculatorState, CalculatorSideEffect>, ViewModel() {

    // Include `orbit-viewmodel` for the factory function
    override val container = container<CalculatorState, CalculatorSideEffect>(CalculatorState())

    fun add(number: Int) = intent {
        postSideEffect(CalculatorSideEffect.Toast("Adding $number to ${state.total}!"))

        reduce {
            state.copy(total = state.total + number)
        }
    }
}

만들어진 상태와 SideEffect는 ContainerHost라는 인터페이스에 따르도록 한다.

interface Container<STATE : Any, SIDE_EFFECT : Any>

Orbit MVI 시스템의 심장이라고하는군...

 

상태(Input)가 바뀌면 SideEffect(Output)이 생긴다. ViewModel 예시코드처럼 SideEffect를 postSideEffect로 뱉을 수 있다.

class CalculatorActivity: AppCompatActivity() {

    // Example of injection using koin, your DI system might differ
    private val viewModel by viewModel<CalculatorViewModel>()

    override fun onCreate(savedState: Bundle?) {
        ...
        addButton.setOnClickListener { viewModel.add(1234) }

        // Use the one-liner from the orbit-viewmodel module to observe when
        // Lifecycle.State.STARTED
        viewModel.observe(state = ::render, sideEffect = ::handleSideEffect)

        // Or observe the streams directly
        lifecycleScope.launch {
            lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
                launch {
                    viewModel.container.stateFlow.collect { render(it) }
                }
                launch {
                    viewModel.container.sideEffectFlow.collect { handleSideEffect(it) }
                }
            }
        }
    }

    private fun render(state: CalculatorState) {
        ...
    }

    private fun handleSideEffect(sideEffect: CalculatorSideEffect) {
        when (sideEffect) {
            is CalculatorSideEffect.Toast -> toast(sideEffect.text)
        }
    }
}

Activity나 Fragment에서는 이렇게 사용할 수 있다.

 

kotlinx.datetime

솔직히 어느 언어든 시간 다루는건 좀 복잡하다고 생각했다. JS의 Moment나 python의 datetime, timedelta는 솔직히 편하긴하다. Java/Kotlin은 GG치고 손놓고 있었는데 괜찮은 녀석이 등장했다. kotlinx-datetime은 시간 계산과 표기, 파싱을 편하게 도와주는 라이브러리다. 주의할점은 Kotlin 1.9.0 이하 버전과는 호환되지 않으며, Android Min API가 26이하라면 AGP 4.0 이상을 사용해야하고 core library desugaring을 활성화시켜주어야한다.

 

노현석 : 우리모두 삽질한다

채팅방, Github에서 애니캐로만 봤던 프루님을 실제로 봤다. 연예인 보는 기분...ㅋㅋㅋ 저렇게 잘하시는분이 삽질 콘텐츠를 들고와서 나름의 위안이 되는 부분도 있었다. (?) 문제들을 보면 나도 겪을 수 있을법한 것들이 많았는데 다른 사람이 문제를 풀어가는 모습을 트래킹하는 재미도 있었다. Glide에서 이미지 비율이 1:1이 아닐때 circleCrop()의 기본 동작으로인해 이미지가 흐려질 수 있다는 삽질기는 언젠가 나도 유의해서 쓸 수 있겠다는 생각...

 

같은 시간을 줘도 많은 일을 해내는 사람들을 보면 반복되는 작업을 어떻게 쳐낼수 있을까 고민을 많이하는 것 같다. 울 팀장님도 그렇고...ㅋㅋㅋ 그리고 이것저것 뚝딱뚝딱 만든다. feature module을 생성하는 Android Studio Plugin 상상만해봤는데 시연보니까 정말 편해보인다. 저것까지는 아니어도 LiveTemplate / FileTemplate을 몇 가지 만들어서 팀원들과 공유해봐야겠다.

 

강경완 : Compose Animation

말 그대로 Compose Animation에 관련한 발표였다! 평소에 Animation을 구현하면서도 어떤 방법으로 구현할지 선택에 대한 고민이 있었는데 덜어주신듯... 발표자료가 아직 안올라온거같은데 대략 이런 의사결정 구조가 있었다.

레이아웃 전체가 애니메이션 O?
├── 등장 / 퇴장 O --> AnimatedVisibility
└── 등장 / 퇴장 X --> AnimatedContent
레이아웃 전체가 애니메이션 X?  
└── animate*AsState

 

머릿 속에서 잊혀지고 있던 updateTransition에 대해서도 다시 상기하고...

더 로우레벨로 애니메이션을 구현할 수 있는 Animatable의 존재도 알게되었다.

 

그리고 마지막에 Let's Try++에 나오는 예시 코드가 있는데 rememberDraggableState를 사용해서 사용자의 드래그가 임계치에 닿지 않으면 패널을 닫지않고 다시 애니메이팅을 하는 부분이 인상적이었다. 구현하기는 다소 복잡하더라도 종료같은 부분에서 임계치를 두는 부분은 UX에 좋다. 자료 올라오면 회사에 적용할 수 있는 부분은 없는지 다시 보고싶다.