iOS

Server-Driven UI

GREEN.1229 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가 무엇인지를 살펴봤는데요.

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

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