Server-Driven UI
안녕하세요. 그린입니다 🍏
이번 포스팅에서는 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가 무엇인지를 살펴봤는데요.
핵심은 앱 배포 없이 서버에 의존하는것에 있습니다.
목적에 맞다면 이 방법을 적용해보는건 어떨까요?