ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Server-Driven UI
    iOS 2025. 3. 7. 18:08

    안녕하세요. 그린입니다 🍏

    이번 포스팅에서는 Server-Driven UI에 대해 톺아보겠습니다 🙋🏻


    Server-Driven UI?

    서버 드리븐 UI라는건 앱의 UI를 서버에서 동적으로 정의하고, 클라이언트는 이걸 해석해서 화면을 구성하는 방식을 말해요 😃

    즉, 화면을 구성하는 컴포넌트 요소들을 JSON 등의 형식으로 서버에서 내려주면 클라이언트가 이를 해석해서 UI를 동적으로 렌더링합니다.

    결국 지향하는 목표는 앱 배포 없이 UI 변경이 자유롭고, A/B 테스트 등에 용이하다는 특징이 있죠!

     

    그럼 서버에서 어떻게 내려주는건지 한번 살펴볼께요.

     


    JSON 기반 UI 정의

    서버는 UI 요소들을 JSON 형태로 정의해서 클라이언트에 전달해줍니다.

     

    아래 서버 리스폰스 예를 볼까요?

     

    {
      "title": "메인 화면",
      "views": [
        { "type": "text", "text": "GREEN" },
        { "type": "button", "text": "Click Here", "action": "navigate", "destination": "/details" }
      ]
    }

     

    이런식으로 내려온다면, 클라이언트가 이 JSON을 해석해서 UI 컴포넌트로 변환해 렌더링을 해주는것이 핵심입니다.

     

    그럼 이 서버 드리븐 UI의 장점과 단점은 무엇이 있는지부터 살펴보고 예시를 구현해볼께요.

     


    Server-Driven UI의 장점 & 단점


    장점

    1️⃣ 배포 없이 UI 변경 가능

    - 앱 업데이트를 하지 않아도 서버에서 UI에 대한 리스폰스만 변경하여 내려준다면 바로 UI가 변경되어 나타나기에 운영 및 유지보수에 용이합니다.

    2️⃣ A/B 테스트 역량 향상

    - 사용자별로 A/B 테스트를 통해 실험을 할때 UI를 달리 제공해주기가 쉬워 비지니스적인 부분을 위해 빠르게 실험해볼 수 있습니다.

    3️⃣ 다양한 플랫폼에서 동일 UI 제공

    - iOS, Android, Web 등 여러 플랫폼에서 이 데이터를 기반으로 UI를 구성하기에 동일한 UI 구조를 서버 드리븐이 아닐때보다 잘 유지할 수 있습니다.

     


    단점

    1️⃣ 클라이언트 코드 복잡성 증가

    - JSON을 해석하고 동적으로 UI를 생성하는 로직이 들어가면서 앱 구조가 다소 복잡해질 수는 있습니다.

    2️⃣ 애니메이션 관련

    - 복잡한 UI/UX 구성 시 서버에서 내려주는 데이터로는 한계가 있어 어느정도는 그 뷰 컴포넌트에서 처리해야 되는 부분도 있습니다.

    사실 단점이라기보다는 서버 드리븐으로 UI 레이아웃 및 컴포넌트의 기본 구성 정도를 구현하는것이고 나머지 애니메이션이나 인터렉션 부분은 클라이언트에서 직접 추가하는게 맞다고 생각해요.

    3️⃣ 디버깅의 어려움

    - 서버에서 내려주는 데이터에 문제가 생긴다면 앱의 UI가 당연히 깨질 수 있어, 서버를 의존하지 않고는 디버깅이 힘들 수 있습니다.

     

    그럼 실제로 SwiftUI에서 간단하게 어떤 개발 방식인지 구현해볼까요?

     


    SwiftUI에서 Server-Driven UI 구현


    Model

    우선 데이터 모델을 정의해봅시다.

     

    import Foundation
    
    struct UIComponent: Decodable, Identifiable {
        let id = UUID()
        let type: String
        let text: String?
        let action: String?
        let destination: String?
    }
    
    struct UIScreenData: Decodable {
        let title: String
        let views: [UIComponent]
    }

     

    내려오는 데이터는 위에서 나타낸 JSON response라고 가정합니다.

     


    ViewModel

    그 다음 뷰모델 객체를 구현해봅니다.

     

    import Foundation
    import Combine
    
    class GreenViewModel: ObservableObject {
        @Published var screenData: UIScreenData?
    
        func fetchUI() {
            guard let url = URL(string: "https://green.blabla.com") else { return }
            
            URLSession.shared.dataTask(with: url) { data, _, error in
                guard let data = data else { return }
                do {
                    let decodedData = try JSONDecoder().decode(UIScreenData.self, from: data)
                    DispatchQueue.main.async {
                        self.screenData = decodedData
                    }
                } catch {
                    print("JSON Parsing Error: \(error)")
                }
            }.resume()
        }
    }

     

    간단하게 데이터를 받아와서 screenData 퍼블리셔 프로퍼티에 바인딩하는 그림입니다.

    즉, 데이터를 받아 입히면 자동으로 UI가 업데이트 되죠!

     


    View

    이제 핵심인 UI를 구현해볼까요?

     

    import SwiftUI
    
    struct GreenView: View {
        @StateObject private var viewModel = GreenViewModel()
    
        var body: some View {
            VStack {
                if let screenData = viewModel.screenData {
                    Text(screenData.title)
                        .font(.largeTitle)
                        .padding()
    
                    ForEach(screenData.views) { component in
                        createView(from: component)
                    }
                } else {
                    ProgressView("Loading...")
                }
            }
            .onAppear {
                viewModel.fetchUI()
            }
            .padding()
        }
    
        @ViewBuilder
        private func createView(from component: UIComponent) -> some View {
            switch component.type {
            case "text":
                if let text = component.text {
                    Text(text)
                        .font(.body)
                        .padding()
                }
            case "button":
                if let text = component.text {
                    Button(action: {
                        handleAction(component)
                    }) {
                        Text(text)
                            .font(.headline)
                            .padding()
                            .background(Color.blue)
                            .foregroundColor(.white)
                            .cornerRadius(8)
                    }
                }
            default:
                EmptyView()
            }
        }
    
        private func handleAction(_ component: UIComponent) {
            if let action = component.action, action == "navigate", let destination = component.destination {
                print("Navigating to \(destination)")
            }
        }
    }

     

    요렇게 구현해볼 수 있습니다.

    뷰모델의 프로퍼티를 가지고 UI를 구성해주죠.

     

    ForEach에서 views 필드로 구분되어 있어 반복을 돌면서 createView를 호출해요.

    그럼, createView에서는 해당 뷰 필드 타입에 따라 분기되어 해당하는 뷰를 데이터로 구현할 수 있습니다.

     

    그럼 이제 서버에서 데이터 순서가 변경되거나 뷰 타입이 변경되어도 앱의 코드를 고칠 필요가 없어집니다.

    다만, 새로운 뷰 타입이 추가되거나 할때는 당연히 필요하죠!

     

    여기서는 아주 간단하게 View에 넣어서 구현을 최소화해서 소개했지만, 자유롭게 Factory 패턴을 이용하거나 별도 Manager를 이용하는 등 프로젝트에 더 맞게 구현을 확장해볼 수 도 있습니다.

     

    또한, 만약 지금 Stack으로 쌓이는 구조인데, 중간 빈 여백이 들어가야 하거나 하는것도 서버에서 뷰 타입을 지정해서 내려주면 그에 맞게 처리도 할 수 있죠.

     

    즉, 서버와 얼마나 규약을 잘 정의하는가에 따라 퀄리티가 달라질거라 생각해요 🤔

     


    Server-Driven UI 적용 시 고려할 부분

    1️⃣ JSON 설계 유연성

    - 새로운 UI 요소를 쉽게 추가할 수 있도록 구조를 잘 설계해야 합니다.

    2️⃣ 캐싱 및 성능 최적화

    - API 요청을 최소화하고, UI를 빠르게 업데이트할 수 있는 로직의 구현이 필요합니다.

    3️⃣ 네트워크 오류 시

    - 실제로 서버 장애가 발생하면 아예 UI를 제공해주지 않기에 대응할 수 있는 UI를 제공하여 앱이 정상적으로 동작할 수 있도록 유연함을 가져가야 합니다.

     


    마무리

    아주 간단하게 이번 포스팅에서는 Server-Driven UI가 무엇인지를 살펴봤는데요.

    핵심은 앱 배포 없이 서버에 의존하는것에 있습니다.

    목적에 맞다면 이 방법을 적용해보는건 어떨까요?

Designed by Tistory.