Upgrade to PRO for Only $50/Yearโ€”Limited-Time Offer! ๐Ÿ”ฅ

[KR] Server Driven Compose With Firebase

Avatar for Jaewoong Jaewoong
September 28, 2024

[KR] Server Driven Compose Withย Firebase

Avatar for Jaewoong

Jaewoong

September 28, 2024
Tweet

More Decks by Jaewoong

Other Decks in Programming

Transcript

  1. Server Driven Compose With Firebase skydoves @github_skydoves Lead Android Developer

    Advocate @ Stream Jaewoong Eum Google Developer Expert
  2. 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
  3. 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 ๋ ˆ์ด์•„์›ƒ์˜ ๋ชจ๋“  ํ˜•ํƒœ ๋ฐ ๋™์ž‘ ๊ตฌ์กฐ๋ฅผ ํด๋ผ์ด์–ธํŠธ์—์„œ ์ •์˜ "๋ฌด์—‡์„, ์–ด๋–ป๊ฒŒ" ๊ทธ๋ฆด์ง€๋Š” ํด๋ผ์ด์–ธํŠธ์˜ ์ฑ…์ž„
  4. Data-Driven UI Data-Driven UI์˜ ๋‹จ์  โ€ข ๋А๋ฆฐ ์—…๋ฐ์ดํŠธ ์ฃผ๊ธฐ: ์‹œ๊ฐ„์ด

    ๋งŽ์ด ์†Œ์š”๋˜๋Š” ์ถœ์‹œ ๋ฐ ๊ฒŒ์‹œ(๋ฆฌ๋ทฐ ๋ฐ ์‹ฌ์‚ฌ) ๊ณผ์ • ๋•Œ๋ฌธ์— ์ค‘์š”ํ•œ ์—…๋ฐ์ดํŠธ ์ œ๊ณต์ด ์ง€์—ฐ๋œ๋‹ค. โ€ข ๋А๋ฆฐ ์‚ฌ์šฉ์ž ์ฑ„ํƒ: ์‚ฌ์šฉ์ž๊ฐ€ ์—…๋ฐ์ดํŠธ๋ฅผ ์ˆ˜๋™์œผ๋กœ ๋‹ค์šด๋กœ๋“œํ•˜๊ณ  ์„ค์น˜ํ•ด์•ผ ํ•˜๋ฏ€๋กœ ์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ๊ณผ ๋ฒ„๊ทธ ์ˆ˜์ •์˜ ์ฑ„ํƒ์ด ๋А๋ ค์ง. ๊ฐ•์ œ ์—…๋ฐ์ดํŠธ๋Š” ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์— ์—ญํšจ๊ณผ๊ฐ€ ์ƒ๊ธธ ๊ฐ€๋Šฅ์„ฑ์ด ๋†’์•„์ง„๋‹ค. โ€ข ๋А๋ฆฐ ๊ธฐ๋Šฅ ์‹คํ—˜: ์—…๋ฐ์ดํŠธ ์ฃผ๊ธฐ๊ฐ€ ๋А๋ฆฌ๊ธฐ ๋•Œ๋ฌธ์— ํŒ€์ด ํ…Œ์ŠคํŠธํ•˜๊ณ ์ž ํ•˜๋Š” ํŠน์ • ๊ธฐ๋Šฅ์„ ์‹คํ—˜ํ•˜๊ณ  ๋ฐ˜๋ณตํ•˜๊ธฐ๊ฐ€ ์–ด๋ ต๋‹ค. โ€ข ๋А๋ฆฐ ํ”ผ๋“œ๋ฐฑ ๋ฃจํ”„: ์—…๋ฐ์ดํŠธ ์ฃผ๊ธฐ์™€ ์ฑ„ํƒ ์†๋„๊ฐ€ ๋А๋ ค์„œ ์‚ฌ์šฉ์ž ํ”ผ๋“œ๋ฐฑ์„ ์ˆ˜์ง‘ํ•˜๊ณ  ์‹ ์†ํ•˜๊ฒŒ ๋ณ€๊ฒฝ ์‚ฌํ•ญ์„ ๋ฐ˜์˜ํ•˜๊ธฐ๊ฐ€ ์–ด๋ ต๋‹ค.
  5. Web-Driven UI Hybrid App์˜ ๋‹จ์  โ€ข ๋Ÿฌ๋‹ ์ปค๋ธŒ: ๊ธฐ์กด์˜ ๋„ค์ดํ‹ฐ๋ธŒ

    ์•ฑ ๊ฐœ๋ฐœ์ž๋“ค์ด ์ƒˆ๋กญ๊ฒŒ ์›น์•ฑ์ด๋ผ๋Š” ๋ถ„์•ผ๋ฅผ ์œ„ํ•ด ์ƒˆ๋กœ์šด ์–ธ์–ด์™€ ๊ฐœ๋ฐœ์ง€์‹์„ ์Šต๋“ํ•ด์•ผ ํ•˜๊ณ , ์ง๋ฌด ์ „ํ™˜์— ์ค€ํ•˜๋Š” ๋†’์€ ๋Ÿฌ๋‹์ปค๋ธŒ๊ฐ€ ์กด์žฌํ•œ๋‹ค. โ€ข ์„ฑ๋Šฅ ์ €ํ•˜: ๋„ค์ดํ‹ฐ๋ธŒ ๊ธฐ๋ฐ˜์˜ ๋ Œ๋”๋ง ์‹œ์Šคํ…œ์ด ์•„๋‹ˆ๋ผ WebView(Chromium)์™€ ๊ฐ™์€ web ๋ธŒ๋ผ์šฐ์ € ์—”์ง„์„ ํ†ตํ•ด ๋ Œ๋”๋ง ํ•ด์•ผ ํ•˜๋ฏ€๋กœ, ๋„ค์ดํ‹ฐ๋ธŒ๋งŒํผ์˜ ๋†’์€ ์„ฑ๋Šฅ์€ ๊ธฐ๋Œ€ํ•˜๊ธฐ ์–ด๋ ต๋‹ค. โ€ข ์ธํ„ฐ๋„ท ์˜์กด์„ฑ: ์ผ๋ถ€ Progressive Web App (PWAs)๋Š” ์˜คํ”„๋ผ์ธ ์บ์‹ฑ์„ ์ง€์›ํ•˜์ง€๋งŒ, ๋Œ€๋ถ€๋ถ„์˜ ์›น์•ฑ์€ ์ธํ„ฐ๋„ท ์—ฐ๊ฒฐ์— ๊ฐ•ํ•œ ์˜์กด์„ฑ์„ ์ง€๋‹ˆ๊ณ  ์žˆ์–ด ๋„คํŠธ์›Œํฌ ํ™˜๊ฒฝ์ด๋ผ๋Š” ์ œ์•ฝ์ด ๋”ฐ๋ผ์˜จ๋‹ค. โ€ข ๋””๋ฐ”์ด์Šค ๊ธฐ๋Šฅ ์ ‘๊ทผ ์ œํ•œ: GPS, ์นด๋ฉ”๋ผ, NFC ๋“ฑ ๋””๋ฐ”์ด์Šค์˜ ํ•˜๋“œ์›จ์–ด ๊ธฐ๋Šฅ์— ๋Œ€ํ•œ ์ •๋ฐ€ํ•œ ์ ‘๊ทผ ๋ฐ ์ฒ˜๋ฆฌ๊ฐ€ ๋ถˆ๊ฐ€๋Šฅํ•˜๋‹ค. Progressive Web App์—์„œ ๊ธฐ๋Šฅ์„ ์ง€์›ํ•˜์ง€๋งŒ ์—ฌ์ „ํžˆ ์ œํ•œ๋œ ๊ธฐ๋Šฅ๋งŒ์„ ์ œ๊ณตํ•œ๋‹ค. ๋˜ํ•œ, Javascript Interface๋ฅผ ํ†ตํ•œ WebView์™€์˜ ๋ณต์žกํ•œ ํ”„๋กœํ† ์ฝœ์ด ์š”๊ตฌ๋œ๋‹ค.
  6. Server-Driven UI Backend @Composable fun TextUi() { .. } @Composable

    fun ImageUi() { .. } @Composable fun ListUi() { .. } Client
  7. Server-Driven UI Backend ๋ ˆ์ด์•„์›ƒ์˜ ๋ชจ๋“  ํ˜•ํƒœ ๋ฐ ๋™์ž‘ ๊ตฌ์กฐ๋ฅผ ์„œ๋ฒ„์—์„œ

    ์ •์˜ ์„œ๋ฒ„๋Š” "๋ฌด์—‡์„" ๊ทธ๋ฆด์ง€ ์ฑ…์ž„์ง€๊ณ  ํด๋ผ์ด์–ธํŠธ๋Š” "์–ด๋–ป๊ฒŒ" ๊ทธ๋ฆด์ง€ ์ฑ…์ž„ Client @Composable fun TextUi() { .. } @Composable fun ImageUi() { .. } @Composable fun ListUi() { .. }
  8. Server-Driven UI Server-Driven UI์˜ ์žฅ์  โ€ข ๋” ๋น ๋ฅธ ๊ธฐ๋Šฅ ์‹คํ—˜:

    ์•ฑ ์—…๋ฐ์ดํŠธ ์—†์ด ์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ(๋ ˆ์ด์•„์›ƒ)์„ ์‰ฝ๊ฒŒ ์ˆ˜์ •ํ•˜๊ณ  ๋ฐฐํฌํ•  ์ˆ˜ ์žˆ์–ด ํ”ผ๋“œ๋ฐฑ ๋ฃจํ”„๊ฐ€ ๋น ๋ฅด๊ณ , ์‹คํ—˜ ๊ธฐ๋Šฅ๋“ค์„ ์‹ ์†ํ•˜๊ฒŒ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ๋‹ค. โ€ข ์ผ๊ด€๋œ UI: ์•ˆ์ •๋œ ์ปดํฌ๋„ŒํŠธ ๋””์ž์ธ ์‹œ์Šคํ…œ์„ ๊ตฌ์ถ•ํ•˜๋ฉด, ํ•ต์‹ฌ ์‚ฌ์–‘์ด ์œ ์ง€๋˜๋Š” ํ•œ ์—ฌ๋Ÿฌ ์•ฑ ๋ฒ„์ „์—์„œ ์ผ๊ด€๋œ UI์™€ ๋™์ž‘์„ ์ œ๊ณตํ•œ๋‹ค. โ€ข ๋„ค์ดํ‹ฐ๋ธŒ ์„ฑ๋Šฅ: ์„œ๋ฒ„ ์ฃผ๋„ UI์˜ ์œ ์—ฐ์„ฑ์„ ์œ ์ง€ํ•˜๋ฉด์„œ๋„ ์›น์•ฑ ๋ฐ ํ•˜์ด๋ธŒ๋ฆฌ๋“œ ์•ฑ๊ณผ ๋น„๊ตํ–ˆ์„ ๋•Œ ๋„ค์ดํ‹ฐ๋ธŒ ์„ฑ๋Šฅ์„ ์œ ์ง€ํ•˜๋ฉด์„œ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ Œ๋”๋งํ•  ์ˆ˜ ์žˆ๋‹ค. โ€ข ๋ชจ๋ฐ”์ผ ๊ฐœ๋ฐœ์ž์˜ ๋ถ€๋‹ด ๊ฐ์†Œ: ๋ ˆ์ด์•„์›ƒ ์„ค๊ณ„๋Š” ์ฃผ๋กœ ์ œํ’ˆ ๊ด€๋ฆฌ์ž์™€ ๋””์ž์ด๋„ˆ๊ฐ€ ์ •์˜ํ•˜๊ณ , "๋ฌด์—‡์„ ํ• ์ง€"๋Š” ๋ฐฑ์—”๋“œ์˜ ์ฑ…์ž„์ด๋ฏ€๋กœ, ๋ชจ๋ฐ”์ผ ๊ฐœ๋ฐœ์ž๋Š” ๊ฐœ๋ณ„ ์ปดํฌ๋„ŒํŠธ ๊ฐœ๋ฐœ์— ์ง‘์ค‘ํ•  ์ˆ˜ ์žˆ๋‹ค.
  9. Realtime Database โ€ข ์‰ฌ์šด ์„œ๋ฒ„ ๊ตฌ์ถ•: ๋ฐฑ์—”๋“œ์— ๋Œ€ํ•œ ์ง€์‹์ด ์—†๋Š”

    ๊ฐœ๋ฐœ์ž๋„ ๋น ๋ฅด๊ฒŒ ์„œ๋ฒ„ ๊ตฌ์ถ•์ด ๊ฐ€๋Šฅํ•˜๊ณ , ๋Œ€์‹œ๋ณด๋“œ์˜ UI๋ฅผ ํ†ตํ•ด ์†์‰ฝ๊ฒŒ ์กฐ์ž‘์ด ๊ฐ€๋Šฅํ•˜๋‹ค. Firebase Realtime Database
  10. Realtime Database โ€ข ์‰ฌ์šด ์„œ๋ฒ„ ๊ตฌ์ถ•: ๋ฐฑ์—”๋“œ์— ๋Œ€ํ•œ ์ง€์‹์ด ์—†๋Š”

    ๊ฐœ๋ฐœ์ž๋„ ๋น ๋ฅด๊ฒŒ ์„œ๋ฒ„ ๊ตฌ์ถ•์ด ๊ฐ€๋Šฅํ•˜๊ณ , UI๊ฐ€ ์ œ๊ณต๋˜๋Š” ๋Œ€์‹œ๋ณด๋“œ๋ฅผ ํ†ตํ•ด ์†์‰ฝ๊ฒŒ ์กฐ์ž‘์ด ๊ฐ€๋Šฅํ•˜๋‹ค. โ€ข JSON ์‘๋‹ต: JSON ํŒŒ์ผ์„ ์ง์ ‘ import/exportํ•ด์„œ ์‘๋‹ต ์ƒ์„ฑ์ด ๊ฐ€๋Šฅํ•˜๋‹ค. Firebase Realtime Database
  11. Realtime Database โ€ข ์‰ฌ์šด ์„œ๋ฒ„ ๊ตฌ์ถ•: ๋ฐฑ์—”๋“œ์— ๋Œ€ํ•œ ์ง€์‹์ด ์—†๋Š”

    ๊ฐœ๋ฐœ์ž๋„ ๋น ๋ฅด๊ฒŒ ์„œ๋ฒ„ ๊ตฌ์ถ•์ด ๊ฐ€๋Šฅํ•˜๊ณ , UI๊ฐ€ ์ œ๊ณต๋˜๋Š” ๋Œ€์‹œ๋ณด๋“œ๋ฅผ ํ†ตํ•ด ์†์‰ฝ๊ฒŒ ์กฐ์ž‘์ด ๊ฐ€๋Šฅํ•˜๋‹ค. โ€ข JSON ์‘๋‹ต: JSON ํŒŒ์ผ์„ ์ง์ ‘ import/exportํ•ด์„œ ์‘๋‹ต ์ƒ์„ฑ์ด ๊ฐ€๋Šฅํ•˜๋‹ค. โ€ข ์‹ค์‹œ๊ฐ„: ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์‘๋‹ต ๋ณ€๊ฒฝ์„ ์‹ค์‹œ๊ฐ„์œผ๋กœ ๊ด€์ฐฐํ•  ์ˆ˜ ์žˆ๋Š” ํด๋ผ์ด์–ธํŠธ SDK๊ฐ€ ์ œ๊ณต๋˜์–ด, ๊ฐ’์˜ ๋ณ€ํ™”๋ฅผ ์‹ค์‹œ๊ฐ„์œผ๋กœ ๋ฐ˜์˜ํ•˜๊ณ  ๋ˆˆ์œผ๋กœ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค. Firebase Realtime Database
  12. 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<String, Any?> { return mapOf( "uid" to uid, "author" to author, "title" to title, ..
  13. 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<TimelineUi>( path = { dataSnapshot -> dataSnapshot.child("timeline") }, decodeProvider = { jsonString -> json.decodeFromString(jsonString) }, ).flatMapLatest { result -> .. }
  14. 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" } ] } }
  15. 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
  16. 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
  17. 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 ์ปดํฌ๋„ŒํŠธ์— ๋Œ€ํ•ด์„œ ๋™์ผํ•œ ์ถ”์ƒํ™” ์ ์šฉ
  18. 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 )
  19. 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<ImageUi>, ) : 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 ์ธํ„ฐํŽ˜์ด์Šค๋กœ ์ปดํฌ๋„ŒํŠธ ์œ ํ˜• ๋‹จ์ผํ™”
  20. 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() ) }
  21. 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. ์„œ๋ฒ„์ฃผ๋„ ๊ฐœ๋ฐœ์— ์ ํ•ฉ
  22. 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) ) }
  23. @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๋กœ ์ผ๋ จ์˜ ์‘๋‹ต์„ ๊ตฌ์„ฑํ•œ๋‹ค.
  24. Component Design Systems @Serializable data class TimelineUi( val version: Int,

    val top: TimelineTopUi, val center: TimelineCenterUi, val bottom: TimelineBottomUi ) : UiComponent val timelineUi: StateFlow<List<UiComponent>> = 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()
  25. Component Design Systems @Serializable data class TimelineUi( val version: Int,

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

    val top: TimelineTopUi, val center: TimelineCenterUi, val bottom: TimelineBottomUi ) : UiComponent val timelineUi: StateFlow<List<UiComponent>> = repository.fetchTimelineUi() .flatMapLatest { result -> flowOf(result.getOrNull()?.buildUiComponentList()) } .filterNotNull() .stateIn() UiComponent UiComponent UiComponent UiComponent UiComponent UiComponent UiComponent UiComponent UiComponent List<UiComponent> top center bottom TimelineUi ImageUi TextUi List<imageUi> TextUi List<imageUi>
  27. 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<List<UiComponent>> = 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 { .. }
  28. 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 ์ธํ„ฐํŽ˜์ด์Šค๋กœ ๋‹จ์ผ ์ถ”์ƒํ™” โ€ข ์ฃผ์–ด์ง„ UiComponent ๋ฆฌ์ŠคํŠธ๋ฅผ Column/Row/LazyList ๋“ฑ์„ ์‚ฌ์šฉํ•˜์—ฌ ์†Œ๋น„
  29. 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() } } }
  30. 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() } } }
  31. Action Handler { "banner": { "url": "https://github.com/skydoves", "size": { "width":

    0, "height": 250 }, "scaleType": "crop", "handler": { "type": "click", "actions": { "navigate": "to" } } } } Navigation, ๋”ฅ๋งํฌ ๋“ฑ ํ–‰๋™์— ๋Œ€ํ•œ ๋™์ž‘ ์ฒ˜๋ฆฌ Click, touch ๋“ฑ ์ปดํฌ๋„ŒํŠธ์— ๋Œ€ํ•œ ํ–‰๋™์„ ์ •์˜
  32. Action Handler @Serializable data class Handler( val type: String, val

    actions: Map<String, String> ) 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์™€ ๊ฐ™์€ ์ตœ์ƒ์œ„ ์ถ”์ƒํ™” ์ธํ„ฐํŽ˜์ด์Šค์— ์ •์˜ํ•˜๋Š” ๊ฒƒ๋„ ์ข‹์€ ๋ฐฉ๋ฒ•
  33. 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
  34. Component Versioning ์ƒˆ๋กœ์šด ๊ณ ๋ฏผ ์‚ฌํ•ญ โ€ข ๋””์ž์ธ ์‹œ์Šคํ…œ์˜ ์ŠคํŽ™์ด ํฌ๊ฒŒ

    ๋ฐ”๋€๋‹ค๋ฉด? โ€ข ์•ฑ ๋ฒ„์ „ ๋ณ„๋กœ ๋‹ค๋ฅธ ๋””์ž์ธ์ด ๋ฐ˜์˜๋˜์–ด์•ผ ํ•œ๋‹ค๋ฉด? โ€ข ์ด๋ฒคํŠธ ๋ฐ ๋ฆฌ์›Œ๋“œ ๋“ฑ ์œ ์—ฐํ•œ ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•ด ์—ฌ๋Ÿฌ ์ข…๋ฅ˜์˜ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ํ•„์š”ํ•˜๋‹ค๋ฉด?
  35. Component Versioning { "timeline": { "version": 1, "top": { ..

    } } } @Serializable data class TimelineUi( val version: Int, val top: TimelineTopUi, val center: TimelineCenterUi, val bottom: TimelineBottomUi ) : UiComponent
  36. 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!") } } } } ๋””์ž์ธ ์‹œ์Šคํ…œ์˜ ๋ฒ„์ €๋‹ ๋ฒ”์œ„ ์„ค์ • ์ƒํ™ฉ์— ๋”ฐ๋ผ ํ™”๋ฉด ๋ณ„๋กœ ๋ฒ„์ „ ์ฒ˜๋ฆฌ๋ฅผ ํ•  ์ˆ˜๋„ ์žˆ๊ณ , ์ปดํฌ๋„ŒํŠธ ๋ณ„๋กœ ํ•  ์ˆ˜๋„ ์žˆ๋‹ค.
  37. 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() ) } }
  38. 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 ) .. } }
  39. Fallback ์ƒˆ๋กœ์šด ๊ณ ๋ฏผ ์‚ฌํ•ญ โ€ข ์ž˜๋ชป๋œ ๋ ˆ์ด์•„์›ƒ ์ •๋ณด๊ฐ€ ๋‚ด๋ ค์˜จ๋‹ค๋ฉด? โ—ฆ

    ์กด์žฌํ•˜์ง€ ์•Š๋Š” ์ปดํฌ๋„ŒํŠธ ๋ฒ„์ „ ์ •๋ณด โ—ฆ ์˜ค๋ฅ˜๊ฐ€ ์žˆ๋Š” ๋ ˆ์ด์•„์›ƒ ๋ฐ์ดํ„ฐ โ€ข ๋ ˆ์ด์•„์›ƒ ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ๋ฐ ์‹คํŒจํ–ˆ๋‹ค๋ฉด?
  40. 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 ์ฒ˜๋ฆฌ๊ฐ€ ํ•„์š”ํ•˜๋‹ค.
  41. 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 }
  42. Recap: Advantages Server-Driven UI์˜ ์žฅ์  โ€ข ๋” ๋น ๋ฅธ ๊ธฐ๋Šฅ ์‹คํ—˜:

    ์•ฑ ์‹ฌ์‚ฌ ๋ฐ ์—…๋ฐ์ดํŠธ ์—†์ด ์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ(๋ ˆ์ด์•„์›ƒ)์„ ์‰ฝ๊ฒŒ ์ˆ˜์ •ํ•˜๊ณ  ๋ฐฐํฌํ•  ์ˆ˜ ์žˆ์–ด ํ”ผ๋“œ๋ฐฑ ๋ฃจํ”„๊ฐ€ ๋น ๋ฅด๊ณ , ๋ฐ˜๋ณต ์ž‘์—…์ด ์‹ ์†ํ•˜๊ฒŒ ์ด๋ฃจ์–ด์ง„๋‹ค. (PM/PO์™€ ๋ชจ๋ฐ”์ผ ๊ฐœ๋ฐœ์ž ๋‘˜ ๋‹ค ํ–‰๋ณต) โ€ข ์ผ๊ด€๋œ UI: ์•ˆ์ •๋œ ์ปดํฌ๋„ŒํŠธ ๋””์ž์ธ ์‹œ์Šคํ…œ์„ ๊ตฌ์ถ•ํ•˜๋ฉด, ํ•ต์‹ฌ ์‚ฌ์–‘์ด ์œ ์ง€๋˜๋Š” ํ•œ ์—ฌ๋Ÿฌ ์•ฑ ๋ฒ„์ „์—์„œ ์ผ๊ด€๋œ UI์™€ ๋™์ž‘์„ ์ œ๊ณตํ•œ๋‹ค. โ€ข ๋„ค์ดํ‹ฐ๋ธŒ ์„ฑ๋Šฅ: ์›น์•ฑ๊ณผ ๊ฐ™์€ UI์˜ ์œ ์—ฐ์„ฑ์„ ์œ ์ง€ํ•˜๋ฉด์„œ๋„ ๋„ค์ดํ‹ฐ๋ธŒ ์„ฑ๋Šฅ์œผ๋กœ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ Œ๋”๋งํ•  ์ˆ˜ ์žˆ๋‹ค. โ€ข ๋ชจ๋ฐ”์ผ ๊ฐœ๋ฐœ์ž์˜ ๋ถ€๋‹ด ๊ฐ์†Œ: "๋ฌด์—‡์„ ํ• ์ง€"๋Š” ๋ฐฑ์—”๋“œ์—์„œ ์•Œ๋ ค์ฃผ๋ฏ€๋กœ, "์–ด๋–ป๊ฒŒ ๋ณด์—ฌ์ค„์ง€"์—๋งŒ ์ง‘์ค‘ํ•  ์ˆ˜ ์žˆ๋‹ค.
  43. Recap: Downside Server-Driven UI์˜ ๋‹จ์  โ€ข ์ง€์—ฐ ์‹œ๊ฐ„ ์ฆ๊ฐ€: ํด๋ผ์ด์–ธํŠธ๊ฐ€

    ๋ฐฑ์—”๋“œ์—์„œ ๋ ˆ์ด์•„์›ƒ ์ •๋ณด๊นŒ์ง€ ๋ฐ›์•„์™€์•ผ ํ•˜๊ธฐ ๋•Œ๋ฌธ์—, data-driven UI์— ๋น„ํ•ด์„œ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋žœ๋”๋งํ•˜๊ณ  ํ‘œ์‹œํ•˜๋Š” ๋ฐ ์‹œ๊ฐ„์ด ๋” ๊ฑธ๋ฆด์ˆ˜ ์žˆ๋‹ค. โ€ข ๋ณต์žก์„ฑ ๋ฐ ๋น„์šฉ ์ฆ๊ฐ€: ํŒ€ ์ „์ฒด๊ฐ€ ๋ ˆ์ด์•„์›ƒ ๋ฐ์ดํ„ฐ๋ฅผ ๋ Œ๋”๋งํ•˜๋Š” ๋ฐฉ์‹, ์ปดํฌ๋„ŒํŠธ ๋ฒ„์ „ ๊ด€๋ฆฌ์— ๋Œ€ํ•ด ๋ช…ํ™•ํ•œ ์—ญํ•  ๋ถ„๋‹ด๊ณผ ๋ ˆ์ด์•„์›ƒ ์‹œ์Šคํ…œ์„ ๊ตฌ์ถ•ํ•ด์•ผํ•œ๋‹ค. ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด ๋ฐฑ์—”๋“œ ํŒ€์ด ๋ ˆ์ด์•„์›ƒ์„ ๊ตฌ์„ฑํ•˜๋Š” ๋ถ€๋‹ด์„ ๋– ์•ˆ๊ฒŒ ๋˜๊ฑฐ๋‚˜, ํŒ€์˜ ์ „๋ฐ˜์ ์ธ ์†Œํ†ต ๋น„์šฉ์ด ์ฆ๊ฐ€ํ•œ๋‹ค. โ€ข Fallback ์ฒ˜๋ฆฌ: ๋ ˆ์ด์•„์›ƒ ๋ฐ์ดํ„ฐ๊ฐ€ ๋‹ค๋ฅธ ํŒ€ (PM/๋””์ž์ด๋„ˆ/๋ฐฑ์—”๋“œ)๋ฅผ ๊ฑฐ์ณ ์ƒ์„ฑ๋˜๋ฏ€๋กœ, ๋ฐ์ดํ„ฐ์— ๊ฒฐํ•จ์ด ๋ฐœ์ƒํ•  ์ˆ˜๋„ ์žˆ๋‹ค. ์ด์— ๋Œ€ํ•œ ์ถฉ๋ถ„ํ•œ fallback ์ฒ˜๋ฆฌ๊ฐ€ ๋˜์–ด์•ผํ•œ๋‹ค.
  44. Recap: Suitable Case Server-Driven UI๊ฐ€ ์ ํ•ฉํ•œ ์ƒํ™ฉ โ€ข ํ™ˆ ํ™”๋ฉด:

    ํ™ˆ (ํ”ผ๋“œ, ํƒ€์ž„๋ผ์ธ) ํ™”๋ฉด๊ณผ ๊ฐ™์ด ์œ ์ €๊ฐ€ ์•ฑ ์ง„์ž… ์‹œ ๊ฐ€์žฅ ๋จผ์ € ๋…ธ์ถœ๋˜๋ฉฐ ์ž์ฃผ ๋ณ€๊ฒฝ์ด ํ•„์š”ํ•œ ํ™”๋ฉด์— ์ ์šฉํ•˜๋ฉด ํฐ ํšจ๊ณผ๋ฅผ ๋ณผ ์ˆ˜ ์žˆ๋‹ค. โ€ข ์„ธ์…˜ ํƒ€์ž„์ด ๋†’์€ ํ™”๋ฉด: ๋ผ์ด๋ธŒ ๋ฐฉ์†ก ๋ฐ ์—”ํ„ฐํ…Œ์ธ๋จผํŠธ์™€ ๊ฐ™์ด ์‚ฌ์šฉ์ž๊ฐ€ ์•ฑ์—์„œ ๋จธ๋ฌด๋Š” ์„ธ์…˜ ํƒ€์ž„์ด ๊ฐ€์žฅ ๋†’์€ ํ™”๋ฉด์— ์‚ฌ์šฉํ•˜๋ฉด ๋” ๋†’์€ ์‚ฌ์šฉ ํšจ๊ณผ๋ฅผ ๋ณผ ์ˆ˜ ์žˆ๋‹ค.
  45. Thank you! Lead Android Developer Advocate @ GetStream Jaewoong Eum

    (์—„์žฌ์›…) Google Developer Expert skydoves @github_skydoves