Flutter에서만 되던 hot-reload, Jetpack Compose는 어떻게 구현했을까?

Jetpack Compose hot-reload 비밀 파해치기

Ji Sungbin
성빈랜드
6 min readJul 11, 2022

--

[View in English]

Photo by Mikołaj on Unsplash

저는 예전부터 플러터에 있는 hot-reload 기능이 너무 부러웠습니다. 이 부러움을 구글도 느꼈는지 작년 말에 컴포즈가 등장하면서 hot-reload 기능이 같이 소개됐습니다. 안드로이드 프로그래밍에 있어서 첫 hot-reload 의 등장입니다.

hot-reload 가 된다는건 개발 과정에서 시간을 단축할 수 있기에 정말 큰 장점이고, 이 장점 만으로도 컴포즈를 시작할 이유가 충분히 있다고 저는 생각합니다. 그렇다면 이런 hot-reload 를 컴포즈에서는 어떻게 구현했을까요? 이번 글에서는 이에 대해 알아보려고 합니다.

사실 예전에 “Jetpack Compose 컴파일러가 부리는 마법 완전히 파해치기” 에서 살짝 다뤘던 적이 있습니다.

예전에 말했던 대로 모든 리터럴들이 MutableState 로 추출되고, 이 State 를 추적하면서 구현됩니다. 정말 모든 파일에 있는 리터럴들이 추출되기에 컴포즈와 아예 관련이 없는 파일에 있는 리터럴들도 전부 MutableState 로 추출됩니다. 어떤식으로 추출되는지 알아보겠습니다.

아래와 같이 “Bye World” 리터럴을 사용하는 earth 함수가 있습니다.

위 함수는 컴파일 과정에서 아래와 같이 바뀝니다.

위 변환된 코드를 해석해 보면

  1. “LiveLiterals${filename}” 으로 오브젝트 생성 (5번 라인)
  2. 초기 값을 받는 변수 생성 (6번 라인)
  3. 리터럴을 담을 MutableState 객체 생성 (7번 라인)
  4. 리터럴 가져오는 함수 생성 (8~19번 라인)

이렇게 4가지 작업으로 이루어져 있습니다. 각각 변수와 함수의 네이밍은 해당 리터럴의 위치와 사용되는 함수의 시그니처 위주로 조합되서 고유하게 결정됩니다.

현재까지 이런 변환은 Int, String, Color, Dp, Boolean 만 지원되며 이렇게 리터럴들만 지원되기에 정확하게는 hot-reload 가 아닌 live-literal 로 불립니다.

위 변환된 코드의 4번 과정을 다시 보도록 하겠습니다. 4번 과정중 10~17번 라인을 보면 초기화된 MutableState 객체가 없다면 liveLiteral 라는 함수를 이용하여 초기화 해주고, 이후 MutableState 를 반환해 주고 있습니다. 여기에서 초기화에 사용되는 liveLiteral 함수는 무엇이며 리터럴 값 업데이트는 어디서 진행되고 있는 걸까요?

liveLiteral 함수 먼저 보겠습니다. 이는 androidx.compose.runtime.internal 에 위치한 LiveLiteral.kt 파일에 정의돼 있습니다.

String 과 MutableState 를 받는 HashMap 변수에 key 와 value 를 그대로 넣어 주는걸로 구현돼 있습니다. 이렇게 liveLiteral 함수는 비교적 간단하게 구현됩니다.

리터럴 값 업데이트는 updateLiveLiteralValue 함수를 통해 진행됩니다.

이 함수가 호출되기 위해선 안드로이드 스튜디오 에디터의 입력을 받고 해당 값으로 호출해야 하기에 위 변환된 코드에서는 이 함수가 사용되지 않았습니다(사용이 불가능합니다). 즉, 이 함수는 안드로이드 스튜디오에서 에디터에 입력된 값으로 직접 호출됩니다.

이렇게 LiveLiteral 에 의해 State 가 변경돼 리컴포지션이 진행되면 안드로이드 스튜디오는 해당 리터럴이 사용된 최소한의 범위만 리컴포지션을 진행하기 위해 findEffectiveRecomposeScope 함수를 사용합니다.

이 과정 덕분에 최적하게 LiveLiteral 을 이용할 수 있게 됩니다. 가끔 LiveLiteral 이 작동하지 않는 이유가 모종의 이유로 위 함수에서 null 이 반환돼서 그렇지 않을까 저는 생각하고 있습니다. (추측입니다)

성능 최적화

LiveLiteral 이 컴포즈가 사용되는 부분에만 적용되면 좋겠지만, 컴포즈가 사용되지 않은 파일들까지 다 적용되니 정말 많은 파일들에서 리터럴들이 State 로 추출됩니다. 만약 리터럴이 100개가 있다면 이 100개의 리터럴 모두 State 의 추적을 받는 것이기에 성능에 정말 심각한 영향을 끼칩니다. 따라서 성능 최적화를 위해선 LiveLiteral 를 사용하지 않을 파일들에 대해서 LiveLiteral 기능을 꺼줘야 합니다.

간단하게 @NoLiveLiterals 어노테이션을 붙여서 LiveLiteral 을 비활성화 할 수 있습니다.

프러퍼티, 함수, 클래스, 파일에 적용할 수 있으며 적용한 타겟 범위 만큼 비활성화 됩니다. 저는 파일 단위로 LiveLiteral 를 비활성화해서(@file:NoLiveLiterals) 컴포즈를 사용하고 있습니다.

또한 릴리즈 모드로 빌드를 하면 모든 파일에 대해 LiveLiteral 이 비활성화 됩니다. 벤치마킹을 할 때 릴리즈 모드로 하라는 이유가 이러한 이유 때문입니다.

디버그 모드에서도 모든 파일에 대해 LiveLiteral 를 비활성화 할 수 있는 컴포즈 플러그인 옵션이 있습니다. liveLiterals 옵션을 false 로 주면 LiveLiteral 기능 자체가 비활성화 되지만, 계속 중복되는 플러그인 옵션 이라고 오류가 떠서 그냥 넘어 가도록 하겠습니다..🥲 추후 성공하는대로 이 글을 업데이트 하도록 하겠습니다.

관련 질문: https://stackoverflow.com/questions/73172453/how-can-i-use-the-liveliterals-plugin-option

끝!

이번 글에선 컴포즈가 hot-reload(live-literal) 를 구현한 방법에 대해 보았습니다. 오랜만에 비교적 쉬운 글이였습니다. 읽어주셔서 감사합니다.

[목차로 돌아가기]

안드로이드 개발자 분들을 위한 카카오톡 오픈 채팅방을 운영하고 있습니다.

--

--

Ji Sungbin
성빈랜드

Experience Engineers for us. I love development that creates references.