시배's Android

Android | Server Driven UI 구현하기 with Custom KSerializer 본문

Android/Android

Android | Server Driven UI 구현하기 with Custom KSerializer

si8ae 2024. 12. 12. 16:42

Server-Driven UI 

Server-Driven UI는 서버에서 UI를 정의하고 클라이언트에서 이를 동적으로 렌더링하는 방식으로, 서버에서 전달되는 JSON 형식을 파싱하여 적절한 UI를 생성하는 것이 핵심입니다. 이번 글에서는 Kotlin Serialization의 KSerializer를 사용하여 JSON 형식을 커스텀 파싱하는 방법에 대해 다룹니다.

왜 KSerializer가 필요한가?

일반적으로 JSON 데이터를 Kotlin 데이터 클래스로 매핑할 때는 Kotlin Serialization에서 제공하는 기본적인 매핑 기능을 사용할 수 있습니다. 하지만 아래와 같이 type이 JSON 키로 사용되고, 데이터가 해당 키의 값으로 내려오는 구조라면, 이를 효과적으로 처리하기 위해 Custom KSerializer가 필요합니다.

 

Json 데이터 예시 : 

{
    "Row" : {},
    "Column" : {},
    "Text" : {},
    "Image" : {},
}

위 데이터를 파싱하려면 type과 이를 기반으로 한 UI 컴포넌트 매핑이 필요합니다. 이를 위해 Kotlin의 KSerializer를 사용해 맞춤형 디코딩 로직을 구현할 수 있습니다.

목표 Kotlin 구조

@Serializable
sealed interface UiComponentResponse

@Serializable(with = UiComponentResponseWrapperSerializer::class)
data class UiComponentResponseWrapper(
    val type: String,
    val component: UiComponentResponse,
)

UiComponentResponse 다양한 UI 컴포넌트를 나타내는 sealed interface이며, 이를 감싸는 UiComponentResponseWrapper JSON 데이터를 type component 구분하여 파싱할 있도록 설계되었습니다.

Custom KSerializer 구현하기

KSerializer는 Kotlin Serialization에서 커스텀 직렬화/역직렬화 로직을 구현하는 핵심 도구입니다. 아래는 UiComponentResponseWrapper를 처리하기 위한 Custom KSerializer의 구현입니다.

object UiComponentResponseWrapperSerializer : KSerializer<UiComponentResponseWrapper> {
    override val descriptor: SerialDescriptor =
        buildClassSerialDescriptor("UiComponentResponseWrapper") {
            element("type", String.serializer().descriptor)
            element("component", buildClassSerialDescriptor("UiComponentUnion"))
        }

    override fun deserialize(decoder: Decoder): UiComponentResponseWrapper {
        val jsonObject = (decoder as? JsonDecoder)?.decodeJsonElement()?.jsonObject
            ?: throw SerializationException("Expected JSON object")

        val entry = jsonObject.entries.firstOrNull()
            ?: throw SerializationException("Empty JSON object")

        val type = entry.key

        val element = when (type) {
            "Text" -> Json.decodeFromJsonElement<TextResponse>(entry.value)
            "Column" -> Json.decodeFromJsonElement<ColumnResponse>(entry.value)
            "Row" -> Json.decodeFromJsonElement<RowResponse>(entry.value)
            "Box" -> Json.decodeFromJsonElement<BoxResponse>(entry.value)
            "Image" -> Json.decodeFromJsonElement<ImageResponse>(entry.value)
            "HorizontalGrid" -> Json.decodeFromJsonElement<HorizontalGridResponse>(entry.value)
            "VerticalGrid" -> Json.decodeFromJsonElement<VerticalGridResponse>(entry.value)
            "Icon" -> Json.decodeFromJsonElement<IconResponse>(entry.value)
            "Spacer" -> Json.decodeFromJsonElement<SpacerResponse>(entry.value)
            "List" -> Json.decodeFromJsonElement<ListResponse>(entry.value)
            "Divider" -> Json.decodeFromJsonElement<DividerResponse>(entry.value)
            "LazyColumn" -> Json.decodeFromJsonElement<LazyColumnResponse>(entry.value)
            "LazyRow" -> Json.decodeFromJsonElement<LazyRowResponse>(entry.value)
            "TopAppBar" -> Json.decodeFromJsonElement<TopAppBarResponse>(entry.value)
            "HorizontalPager" -> Json.decodeFromJsonElement<HorizontalPagerResponse>(entry.value)
            "VerticalPager" -> Json.decodeFromJsonElement<VerticalPagerResponse>(entry.value)
            else -> throw SerializationException("Unknown component type: $type")
        }

        return UiComponentResponseWrapper(type, element)
    }

    override fun serialize(encoder: Encoder, value: UiComponentResponseWrapper) {
        val jsonObject = buildJsonObject {
            put(value.type, Json.encodeToJsonElement(value.component))
        }
        (encoder as? JsonEncoder)?.encodeJsonElement(jsonObject)
    }
}

주요 구현 포인트

  1. Descriptor 정의
    • UiComponentResponseWrapper의 구조를 정의하여 JSON 직렬화/역직렬화 시 사용합니다.
    • type은 JSON 키, component는 값으로 매핑됩니다.
  2. deserialize
    • JSON 데이터를 디코딩하여 첫 번째 엔트리(type)에 따라 적절한 데이터 클래스를 생성합니다.
    • when 문을 통해 타입별 매핑을 처리합니다.
  3. serialize
    • Kotlin 객체를 JSON으로 변환할 때 typecomponent를 포함하는 JSON 객체를 생성합니다.

예시

Firebase Realtime Database를 활용하여, 저의 사이드 프로젝트인 Cherish 앱의 메인 화면을 JSON 형태로 서버에서 내려받아 Server-Driven UI로 구현해 보았습니다.



https://github.com/koreatlwls/server-driven-ui