Slide 1

Slide 1 text

Server Driven Compose With Firebase skydoves @github_skydoves Lead Android Developer Advocate @ Stream Jaewoong Eum Google Developer Expert

Slide 2

Slide 2 text

Approaches to Server-Driven UI

Slide 3

Slide 3 text

Data-Driven UI Client Backend data

Slide 4

Slide 4 text

Data-Driven UI Client Backend data Bind data into components

Slide 5

Slide 5 text

Data-Driven UI Client Backend data Bind data into components Render Data-Driven UI

Slide 6

Slide 6 text

Data-Driven UI { "title": "Server Driven Compose", "items": [ { "title": "City", "url": "https://github.com/skydoves" }, { "title": "Suits", "url": "https://github.com/skydoves" } ] } @Composable fun TimelineContent() { Text(..) LazyColumn { } } Backend Client

Slide 7

Slide 7 text

Data-Driven UI { "clientIdentiferData": { "clientID": "89012", "webPropertyID": "89012-01", "campaignID": "004" }, "journeyKeyData": { "campaignCreationPhase": "Teach", "purchaseJourneyPhase": "Awareness", "topicCategory": "Organizer", "topicSubCategory": "Event Team", "topicSubSubCategory": "Null" }, "uploadedData": [{ "dataCategoryLabel": "name of organization", "dataStrings": [{ "string": "UBM" }] }, { "dataCategoryLabel": "our mission", "dataStrings": [{ "string": "We Serve the Needs of B2B Buyers" }] }, { "dataCategoryLabel": "our history", "dataStrings": [{ "string": "UBM was fonded in 1948", "string2": "It is was purchased by Informa in 2017", "string3": "We Serve the Needs of B2B Buyers" }] }, { Backend Client 레이아웃의 모든 형태 및 동작 구조를 클라이언트에서 정의 "무엇을, 어떻게" 그릴지는 클라이언트의 책임

Slide 8

Slide 8 text

Data-Driven UI Data-Driven UI의 단점 ● 느린 업데이트 주기: 시간이 많이 소요되는 출시 및 게시(리뷰 및 심사) 과정 때문에 중요한 업데이트 제공이 지연된다. ● 느린 사용자 채택: 사용자가 업데이트를 수동으로 다운로드하고 설치해야 하므로 새로운 기능과 버그 수정의 채택이 느려짐. 강제 업데이트는 사용자 경험에 역효과가 생길 가능성이 높아진다. ● 느린 기능 실험: 업데이트 주기가 느리기 때문에 팀이 테스트하고자 하는 특정 기능을 실험하고 반복하기가 어렵다. ● 느린 피드백 루프: 업데이트 주기와 채택 속도가 느려서 사용자 피드백을 수집하고 신속하게 변경 사항을 반영하기가 어렵다.

Slide 9

Slide 9 text

Web-Driven UI Backend Hybrid App Client data WebView browser Hybrid Application

Slide 10

Slide 10 text

Web-Driven UI Hybrid App의 단점 ● 러닝 커브: 기존의 네이티브 앱 개발자들이 새롭게 웹앱이라는 분야를 위해 새로운 언어와 개발지식을 습득해야 하고, 직무 전환에 준하는 높은 러닝커브가 존재한다. ● 성능 저하: 네이티브 기반의 렌더링 시스템이 아니라 WebView(Chromium)와 같은 web 브라우저 엔진을 통해 렌더링 해야 하므로, 네이티브만큼의 높은 성능은 기대하기 어렵다. ● 인터넷 의존성: 일부 Progressive Web App (PWAs)는 오프라인 캐싱을 지원하지만, 대부분의 웹앱은 인터넷 연결에 강한 의존성을 지니고 있어 네트워크 환경이라는 제약이 따라온다. ● 디바이스 기능 접근 제한: GPS, 카메라, NFC 등 디바이스의 하드웨어 기능에 대한 정밀한 접근 및 처리가 불가능하다. Progressive Web App에서 기능을 지원하지만 여전히 제한된 기능만을 제공한다. 또한, Javascript Interface를 통한 WebView와의 복잡한 프로토콜이 요구된다.

Slide 11

Slide 11 text

Server-Driven UI Backend domain data + components

Slide 12

Slide 12 text

Server-Driven UI Backend Consume Client Render domain data + components Server-Driven UI

Slide 13

Slide 13 text

Server-Driven UI Backend

Slide 14

Slide 14 text

Server-Driven UI Backend @Composable fun TextUi() { .. } @Composable fun ImageUi() { .. } @Composable fun ListUi() { .. } Client

Slide 15

Slide 15 text

Server-Driven UI Backend 레이아웃의 모든 형태 및 동작 구조를 서버에서 정의 서버는 "무엇을" 그릴지 책임지고 클라이언트는 "어떻게" 그릴지 책임 Client @Composable fun TextUi() { .. } @Composable fun ImageUi() { .. } @Composable fun ListUi() { .. }

Slide 16

Slide 16 text

Server-Driven UI Server-Driven UI의 장점 ● 더 빠른 기능 실험: 앱 업데이트 없이 새로운 기능(레이아웃)을 쉽게 수정하고 배포할 수 있어 피드백 루프가 빠르고, 실험 기능들을 신속하게 수행할 수 있다. ● 일관된 UI: 안정된 컴포넌트 디자인 시스템을 구축하면, 핵심 사양이 유지되는 한 여러 앱 버전에서 일관된 UI와 동작을 제공한다. ● 네이티브 성능: 서버 주도 UI의 유연성을 유지하면서도 웹앱 및 하이브리드 앱과 비교했을 때 네이티브 성능을 유지하면서 컴포넌트를 렌더링할 수 있다. ● 모바일 개발자의 부담 감소: 레이아웃 설계는 주로 제품 관리자와 디자이너가 정의하고, "무엇을 할지"는 백엔드의 책임이므로, 모바일 개발자는 개별 컴포넌트 개발에 집중할 수 있다.

Slide 17

Slide 17 text

Firebase Realtime Database

Slide 18

Slide 18 text

Firebase Realtime Database

Slide 19

Slide 19 text

Realtime Database ● 쉬운 서버 구축: 백엔드에 대한 지식이 없는 개발자도 빠르게 서버 구축이 가능하고, 대시보드의 UI를 통해 손쉽게 조작이 가능하다. Firebase Realtime Database

Slide 20

Slide 20 text

Realtime Database ● 쉬운 서버 구축: 백엔드에 대한 지식이 없는 개발자도 빠르게 서버 구축이 가능하고, UI가 제공되는 대시보드를 통해 손쉽게 조작이 가능하다. ● JSON 응답: JSON 파일을 직접 import/export해서 응답 생성이 가능하다. Firebase Realtime Database

Slide 21

Slide 21 text

Realtime Database ● 쉬운 서버 구축: 백엔드에 대한 지식이 없는 개발자도 빠르게 서버 구축이 가능하고, UI가 제공되는 대시보드를 통해 손쉽게 조작이 가능하다. ● JSON 응답: JSON 파일을 직접 import/export해서 응답 생성이 가능하다. ● 실시간: 데이터베이스 응답 변경을 실시간으로 관찰할 수 있는 클라이언트 SDK가 제공되어, 값의 변화를 실시간으로 반영하고 눈으로 확인할 수 있다. Firebase Realtime Database

Slide 22

Slide 22 text

Server-Driven UI Consume Client Render domain data + components Server-Driven UI Firebase Realtime Database

Slide 23

Slide 23 text

Server-Driven UI dependencies { implementation(platform("com.google.firebase:firebase-bom:33.2.0")) implementation("com.google.firebase:firebase-database") }

Slide 24

Slide 24 text

Realtime Database Android SDK의 문제점 ● Java 기반 코드: SDK가 전부 Java 코드로 작성되어 여전히 Callback 기반의 API를 제공한다. ● Serialization 커스텀 불가: 공식 SDK와 커뮤니티의 라이브러리 모두 snapshot data에 대하여 커스텀 serialization 옵션을 제공하지 않는다. 따라서, nullability를 지원하지 않고 fallback 처리를 유동적으로 하는 것이 어렵다. 반드시 default값을 정의해야 한다. Firebase Realtime Database val listener = object : ValueEventListener { override fun onDataChange(snapshot: DataSnapshot) { val value = snapshot.child("post") // .. } override fun onCancelled(error: DatabaseError) { // .. } } @IgnoreExtraProperties data class Post( var uid: String? = "", var author: String? = "", var title: String? = "", ) { @Exclude fun toMap(): Map { return mapOf( "uid" to uid, "author" to author, "title" to title, ..

Slide 25

Slide 25 text

Firebase Realtime Database

Slide 26

Slide 26 text

Firebase Realtime Database dependencies { implementation("com.github.skydoves:firebase-database-ktx:0.2.0") } private val database = Firebase.database(BuildConfig.REALTIME_DATABASE_URL).reference private val json = Json { isLenient = true ignoreUnknownKeys = true prettyPrint = true } val timelineUi = database.flow( path = { dataSnapshot -> dataSnapshot.child("timeline") }, decodeProvider = { jsonString -> json.decodeFromString(jsonString) }, ).flatMapLatest { result -> .. }

Slide 27

Slide 27 text

Firebase Realtime Database

Slide 28

Slide 28 text

Component Design in Jetpack Compose

Slide 29

Slide 29 text

Component Design Systems { "title": { "text": "Server Driven Compose", "size": 26, "fontWeight": "bold" }, "list": { "layout": "grid", "itemSize": { "width": 150, "height": 150 }, "items": [ { "title": "City", "url": "https://github.com/skydoves", "scaleType": "crop" }, { "title": "Suits", "url": "https://github.com/skydoves", "scaleType": "crop" } ] } }

Slide 30

Slide 30 text

Component Design Systems { "title": { "text": "Server Driven Compose", "size": 26, "fontWeight": "bold" }, "list": { "layout": "grid", "itemSize": { "width": 150, "height": 150 }, "items": [ { "title": "City", "url": "https://github.com/skydoves", "scaleType": "crop" }, { "title": "Suits", "url": "https://github.com/skydoves", "scaleType": "crop" } ] } } Text Image

Slide 31

Slide 31 text

Component Design Systems { "title": { "text": "Server Driven Compose", "size": 26, "fontWeight": "bold" }, "list": { "layout": "grid", "itemSize": { "width": 150, "height": 150 }, "items": [ { "title": "City", "url": "https://github.com/skydoves", "scaleType": "crop" }, { "title": "Suits", "url": "https://github.com/skydoves", "scaleType": "crop" } ] } } Text Image List

Slide 32

Slide 32 text

Component Design Systems { "title": { "text": "Server Driven Compose", "size": 26, "fontWeight": "bold" }, "list": { "layout": "grid", "itemSize": { "width": 150, "height": 150 }, "items": [ { "title": "City", "url": "https://github.com/skydoves", "scaleType": "crop" }, { "title": "Suits", "url": "https://github.com/skydoves", "scaleType": "crop" } ] } } Text Image List UiComponent 모든 UI 컴포넌트에 대해서 동일한 추상화 적용

Slide 33

Slide 33 text

Component Design Systems @Serializable sealed interface UiComponent

Slide 34

Slide 34 text

Component Design Systems @Serializable sealed interface UiComponent @Serializable data class TextUi( val text: String, val size: Int, val fontWeight: String ) : UiComponent @Serializable data class ImageUi( val url: String, val size: DpSizeUi = DpSizeUi(0, 0), val scaleType: String, val contentDescription: String = "", ) : UiComponent @Serializable data class DpSizeUi( val width: Int, val height: Int )

Slide 35

Slide 35 text

Component Design Systems @Serializable sealed interface UiComponent @Serializable data class TextUi( val text: String, val size: Int, val fontWeight: String ) : UiComponent @Serializable data class ImageUi( val url: String, val size: DpSizeUi = DpSizeUi(0, 0), val scaleType: String, val contentDescription: String = "", ) : UiComponent @Serializable data class DpSizeUi( val width: Int, val height: Int ) @Serializable data class ListUi( val layout: String, val itemSize: DpSizeUi, val items: List, ) : UiComponent fun String.toLayoutType(): LayoutType { return if (this == "grid") LayoutType.GRID else if (this == "column") LayoutType.COLUMN else LayoutType.ROW } enum class LayoutType(val value: String) { GRID("grid"), COLUMN("column"), ROW("row") } UiComponent 인터페이스로 컴포넌트 유형 단일화

Slide 36

Slide 36 text

Component Design Systems @Serializable data class TextUi( val text: String, val size: Int, val fontWeight: String ) : UiComponent @Composable fun ConsumeTextUi( textUi: TextUi, modifier: Modifier = Modifier ) { Text( modifier = modifier, text = textUi.text, color = ServerDrivenTheme.colors.textHighEmphasis, fontSize = textUi.size.sp, fontWeight = textUi.fontWeight.toFontWeight() ) }

Slide 37

Slide 37 text

Component Design Systems @Serializable data class TextUi( val text: String, val size: Int, val fontWeight: String ) : UiComponent @Composable fun ConsumeTextUi( textUi: TextUi, modifier: Modifier = Modifier ) { Text( modifier = modifier, text = textUi.text, color = ServerDrivenTheme.colors.textHighEmphasis, fontSize = textUi.size.sp, fontWeight = textUi.fontWeight.toFontWeight() ) } val mockTextUi1 = TextUi( text = "Title", size = 32, fontWeight = "bold" ) ConsumeTextUi( textUi = mockTextUi1 ) 재사용성이 높다. 1. Preview 작성에 용이 2. UI 테스트 작성에 용이 3. 서버주도 개발에 적합

Slide 38

Slide 38 text

Component Design Systems @Serializable data class ImageUi( val url: String, val size: DpSizeUi = DpSizeUi(0, 0), val scaleType: String, val contentDescription: String = "", ) : UiComponent @Composable fun ConsumeImageUi( imageUi: ImageUi, modifier: Modifier = Modifier, imageOptions: ImageOptions? = null ) { GlideImage( modifier = modifier.size(imageUi.size), imageModel = { imageUi.url }, imageOptions = imageOptions ?: ImageOptions( contentScale = imageUi.scaleType.toContentScale(), contentDescription = imageUi.contentDescription ), previewPlaceholder = painterResource(R.drawable.preview) ) }

Slide 39

Slide 39 text

Component Design Systems

Slide 40

Slide 40 text

@Serializable data class TimelineTopUi( val banner: ImageUi ): UiComponent @Serializable data class TimelineCenterUi( val title: TextUi, val list: ListUi ): UiComponent @Serializable data class TimelineBottomUi( val title: TextUi, val list: ListUi ): UiComponent Component Design Systems @Serializable data class TimelineUi( val top: TimelineTopUi, val center: TimelineCenterUi, val bottom: TimelineBottomUi ) : UiComponent 최상위부터 최하위 객체까지 동일한 UiComponent로 일련의 응답을 구성한다.

Slide 41

Slide 41 text

Component Design Systems @Serializable data class TimelineUi( val version: Int, val top: TimelineTopUi, val center: TimelineCenterUi, val bottom: TimelineBottomUi ) : UiComponent val timelineUi: StateFlow> = repository.fetchTimelineUi() .flatMapLatest { result -> flowOf(result.getOrNull()?.buildUiComponentList()) } .filterNotNull() .stateIn() @Serializable data class TimelineTopUi( val banner: ImageUi (UiComponent) ): UiComponent @Serializable data class TimelineCenterUi( val title: TextUi, (UiComponent) val list: ListUi (UiComponent) ): UiComponent @Serializable data class TimelineBottomUi( val title: TextUi, (UiComponent) val list: ListUi (UiComponent) ): UiComponent UiComponent.buildUiComponentList()

Slide 42

Slide 42 text

Component Design Systems @Serializable data class TimelineUi( val version: Int, val top: TimelineTopUi, val center: TimelineCenterUi, val bottom: TimelineBottomUi ) : UiComponent val timelineUi: StateFlow> = repository.fetchTimelineUi() .flatMapLatest { result -> flowOf(result.getOrNull()?.buildUiComponentList()) } .filterNotNull() .stateIn() UiComponent UiComponent UiComponent UiComponent UiComponent UiComponent UiComponent UiComponent UiComponent TimelineUi top center bottom

Slide 43

Slide 43 text

Component Design Systems @Serializable data class TimelineUi( val version: Int, val top: TimelineTopUi, val center: TimelineCenterUi, val bottom: TimelineBottomUi ) : UiComponent val timelineUi: StateFlow> = repository.fetchTimelineUi() .flatMapLatest { result -> flowOf(result.getOrNull()?.buildUiComponentList()) } .filterNotNull() .stateIn() UiComponent UiComponent UiComponent UiComponent UiComponent UiComponent UiComponent UiComponent UiComponent List top center bottom TimelineUi ImageUi TextUi List TextUi List

Slide 44

Slide 44 text

Component Design Systems @Serializable data class TimelineUi( val version: Int, val top: TimelineTopUi (OrderedUiComponent), val center: TimelineCenterUi (OrderedUiComponent), val bottom: TimelineBottomUi (OrderedUiComponent) ) : UiComponent val timelineUi: StateFlow> = repository.fetchTimelineUi() .flatMapLatest { result -> flowOf(result.getOrNull()?.buildUiComponentList()) } .filterNotNull() .stateIn() UiComponent OrderedUiComponent OrderedUiComponent OrderedUiComponent UiComponent UiComponent UiComponent UiComponent UiComponent @Serializable sealed interface OrderedUiComponent : UiComponent { val order: Int } sortedBy { .. }

Slide 45

Slide 45 text

Component Design Systems @Composable fun UiComponent.Consume( modifier: Modifier = Modifier, ) { when (this) { is TextUi -> ConsumeTextUi( textUi = this, ) is ImageUi -> ConsumeImageUi( imageUi = this, modifier = modifier, ) is ListUi -> ConsumeList( listUi = this, modifier = modifier, ) else -> ConsumeDefaultUi( uiComponent = this, ) } } Column( modifier = Modifier ) { timelineUi.components.forEach { uiComponent -> UiComponent.Consume() } } UiComponent UiComponent UiComponent UiComponent UiComponent UiComponent UiComponent UiComponent UiComponent List ● 응답의 직렬화 이후 모든 컴포넌트를 UiComponent 인터페이스로 단일 추상화 ● 주어진 UiComponent 리스트를 Column/Row/LazyList 등을 사용하여 소비

Slide 46

Slide 46 text

Component Design Systems @Composable fun Timeline(..) { val timelineUi by timelineViewModel.timelineUi.collectAsStateWithLifecycle() Column( modifier = Modifier .fillMaxSize() .padding(12.dp), verticalArrangement = Arrangement.spacedBy(12.dp) ) { timelineUi.components.forEach { uiComponent -> uiComponent.Consume() } } }

Slide 47

Slide 47 text

The Other Screens @Composable fun PostDetails( detailsUi: ScreenUi ) { Column( modifier = Modifier .background(ServerDrivenTheme.colors.background) .fillMaxSize() .padding(12.dp) .verticalScroll(state = rememberScrollState()), verticalArrangement = Arrangement.spacedBy(12.dp) ) { detailsUi.components.forEach { component -> component.Consume() } } }

Slide 48

Slide 48 text

Action Handler Click

Slide 49

Slide 49 text

Action Handler { "banner": { "url": "https://github.com/skydoves", "size": { "width": 0, "height": 250 }, "scaleType": "crop", "handler": { "type": "click", "actions": { "navigate": "to" } } } } Navigation, 딥링크 등 행동에 대한 동작 처리 Click, touch 등 컴포넌트에 대한 행동을 정의

Slide 50

Slide 50 text

Action Handler @Serializable data class Handler( val type: String, val actions: Map ) enum class HandlerType(val value: String) { CLICK("click") } enum class HandlerAction(val value: String) { NAVIGATION("navigation") } enum class NavigationHandler(val value: String) { TO("to") } @Serializable data class ImageUi( val url: String, val handler: Handler? = null ) : UiComponent ● 컴포넌트에 따라 action handler가 존재하지 않을 수 있으므로 optional ● UiComponent와 같은 최상위 추상화 인터페이스에 정의하는 것도 좋은 방법

Slide 51

Slide 51 text

Action Handler @Composable fun ConsumeImageUi( imageUi: ImageUi, modifier: Modifier = Modifier, navigator: (UiComponent) -> Unit = {}, imageOptions: ImageOptions? = null ) { GlideImage( modifier = modifier .consumeHandler( handler = imageUi.handler, navigator = { navigator.invoke(imageUi) } ) .size(imageUi.size) .clip(RoundedCornerShape(8.dp)), .. } @Composable fun Modifier.consumeHandler( handler: Handler?, navigator: () -> Unit ): Modifier { if (handler == null) return this handler.actions.forEach { element -> val action = if (element.key == HandlerAction.NAVIGATION.value && element.value == NavigationHandler.TO.value ) { { navigator } } else { {} } val newModifier = if (handler.type == HandlerType.CLICK.value) { Modifier.clickable { action.invoke() } } else { Modifier } then(newModifier) } return this

Slide 52

Slide 52 text

Jetpack Compose vs XML

Slide 53

Slide 53 text

Component Versioning & Fallback

Slide 54

Slide 54 text

Component Versioning 새로운 고민 사항 ● 디자인 시스템의 스펙이 크게 바뀐다면? ● 앱 버전 별로 다른 디자인이 반영되어야 한다면? ● 이벤트 및 리워드 등 유연한 처리를 위해 여러 종류의 컴포넌트가 필요하다면?

Slide 55

Slide 55 text

Component Versioning { "timeline": { "version": 1, "top": { .. } } } @Serializable data class TimelineUi( val version: Int, val top: TimelineTopUi, val center: TimelineCenterUi, val bottom: TimelineBottomUi ) : UiComponent

Slide 56

Slide 56 text

Component Versioning { "timeline": { "version": 1, "top": { .. } } } @Serializable data class TimelineUi( val version: Int, val top: TimelineTopUi, val center: TimelineCenterUi, val bottom: TimelineBottomUi ) : UiComponent enum class UiVersion(val value: Int) { VERSION_1_0(1), VERSION_2_0(2); companion object { fun toUiVersion(value: Int): UiVersion { return when (value) { VERSION_1_0.value -> VERSION_1_0 VERSION_2_0.value -> VERSION_2_0 else -> throw RuntimeException("undefined version!") } } } } 디자인 시스템의 버저닝 범위 설정 상황에 따라 화면 별로 버전 처리를 할 수도 있고, 컴포넌트 별로 할 수도 있다.

Slide 57

Slide 57 text

Component Versioning @Composable fun ConsumeTextUi( version: UiVersion, textUi: TextUi, modifier: Modifier = Modifier ) { if (version == UiVersion.VERSION_1_0) { Text( modifier = modifier, text = textUi.text, color = ServerDrivenTheme.colors.textHighEmphasis, fontSize = textUi.size.sp, fontWeight = textUi.fontWeight.toFontWeight() ) } else if (version == UiVersion.VERSION_2_0) { Text( modifier = modifier, text = textUi.text, color = ServerDrivenTheme.colors.primary, fontSize = textUi.size.sp, fontWeight = textUi.fontWeight.toFontWeight() ) } }

Slide 58

Slide 58 text

Component Versioning @Composable fun UiComponent.Consume( modifier: Modifier = Modifier, version: UiVersion = UiVersion.VERSION_1_0, navigator: (UiComponent) -> Unit = {} ) { when (this) { is TextUi -> ConsumeTextUi( textUi = this, modifier = modifier, version = version ) is ImageUi -> ConsumeImageUi( imageUi = this, modifier = modifier, version = version, navigator = navigator ) .. } }

Slide 59

Slide 59 text

Component Versioning

Slide 60

Slide 60 text

Fallback 새로운 고민 사항 ● 잘못된 레이아웃 정보가 내려온다면? ○ 존재하지 않는 컴포넌트 버전 정보 ○ 오류가 있는 레이아웃 데이터 ● 레이아웃 정보를 가져오는 데 실패했다면?

Slide 61

Slide 61 text

Fallback @Composable fun UiComponent.Consume( modifier: Modifier = Modifier, version: UiVersion = UiVersion.VERSION_1_0, navigator: (UiComponent) -> Unit = {} ) { when (this) { is TextUi -> ConsumeTextUi( textUi = this, modifier = modifier, version = version ) .. else -> ConsumeDefaultUi( uiComponent = this, version = version ) } } 직렬화 실패 및 데이터 결함 등 여러 이유로 컴포넌트 매칭에 실패한 경우 적절한 fallback 처리가 필요하다.

Slide 62

Slide 62 text

Fallback 컴포넌트 버저닝을 적용하는 경우 PM이나 백엔드 개발자의 실수로 잘못된 버전 정보가 내려오는 것을 대비하여 적절한 fallback 처리가 필요하다. enum class UiVersion(val value: Int) { VERSION_1_0(1), VERSION_2_0(2); companion object { fun toUiVersion(value: Int): UiVersion { return when (value) { VERSION_1_0.value -> VERSION_1_0 VERSION_2_0.value -> VERSION_2_0 else -> throw RuntimeException("undefined version!") or else -> VERSION_1_0 } } } } fun String.toLayoutType(): LayoutType { return if (this == "grid") LayoutType.GRID else if (this == "column") LayoutType.COLUMN else LayoutType.ROW }

Slide 63

Slide 63 text

Recap

Slide 64

Slide 64 text

Recap: Advantages Server-Driven UI의 장점 ● 더 빠른 기능 실험: 앱 심사 및 업데이트 없이 새로운 기능(레이아웃)을 쉽게 수정하고 배포할 수 있어 피드백 루프가 빠르고, 반복 작업이 신속하게 이루어진다. (PM/PO와 모바일 개발자 둘 다 행복) ● 일관된 UI: 안정된 컴포넌트 디자인 시스템을 구축하면, 핵심 사양이 유지되는 한 여러 앱 버전에서 일관된 UI와 동작을 제공한다. ● 네이티브 성능: 웹앱과 같은 UI의 유연성을 유지하면서도 네이티브 성능으로 컴포넌트를 렌더링할 수 있다. ● 모바일 개발자의 부담 감소: "무엇을 할지"는 백엔드에서 알려주므로, "어떻게 보여줄지"에만 집중할 수 있다.

Slide 65

Slide 65 text

Recap: Downside Server-Driven UI의 단점 ● 지연 시간 증가: 클라이언트가 백엔드에서 레이아웃 정보까지 받아와야 하기 때문에, data-driven UI에 비해서 컴포넌트를 랜더링하고 표시하는 데 시간이 더 걸릴수 있다. ● 복잡성 및 비용 증가: 팀 전체가 레이아웃 데이터를 렌더링하는 방식, 컴포넌트 버전 관리에 대해 명확한 역할 분담과 레이아웃 시스템을 구축해야한다. 그렇지 않으면 백엔드 팀이 레이아웃을 구성하는 부담을 떠안게 되거나, 팀의 전반적인 소통 비용이 증가한다. ● Fallback 처리: 레이아웃 데이터가 다른 팀 (PM/디자이너/백엔드)를 거쳐 생성되므로, 데이터에 결함이 발생할 수도 있다. 이에 대한 충분한 fallback 처리가 되어야한다.

Slide 66

Slide 66 text

Recap: Suitable Case Server-Driven UI가 적합한 상황 ● 홈 화면: 홈 (피드, 타임라인) 화면과 같이 유저가 앱 진입 시 가장 먼저 노출되며 자주 변경이 필요한 화면에 적용하면 큰 효과를 볼 수 있다. ● 세션 타임이 높은 화면: 라이브 방송 및 엔터테인먼트와 같이 사용자가 앱에서 머무는 세션 타임이 가장 높은 화면에 사용하면 더 높은 사용 효과를 볼 수 있다.

Slide 67

Slide 67 text

Open-source & Article github.com/skydoves/server-driven-compose getstream.io/blog/server-driven-compose-firebase

Slide 68

Slide 68 text

Learn Android & Kotlin With Dove Letter!

Slide 69

Slide 69 text

Thank you! Lead Android Developer Advocate @ GetStream Jaewoong Eum (엄재웅) Google Developer Expert skydoves @github_skydoves