출처: Life is too short to develop only for Android
기존의 앱/웹에서는 서버가 도메인 객체를 JSON으로 인코딩하여 클라이언트에게 보내면, 클라이언트는 이 데이터를 UI로 그리는 방식을 주로 사용해왔다.
하지만 Server Driven UI(SDUI)는 서버에서 UI 구성과 레이아웃을 결정하고, 클라이언트는 이를 받아서 화면을 그린다. 클라이언트에서 하드 코딩된 UI 대신 서버가 레이아웃, 컴포넌트와 내부 데이터에 관련된 지시를 보내준다. 이를 통해 플랫폼 간 신속한 변경과 통합된 사용자 경험을 제공할 수 있게 된다.
UI를 자주 바꿔야하는 애플리케이션이 있다고 가정해보자. 애플리케이션 개발에 있어서 새로운 가설을 검증하기 위해 빠르게 신기능을 도입하고, 여러 UI로 A/B 테스트를 진행해보는 것은 중요하다.
이러한 상황에서 클라이언트 개발자들은 매번 새로운 UI를 개발하고, 서버 개발자들은 이전 버전과의 호환성을 고려하며(또는 버전 분기 처리를 하며) 새로운 기능을 위한 API를 개발해야 한다.
🧐 무슨 문제가 있는거죠?
Server Driven UI 구조라고 가정하고, 위 예시 이미지 상단의 배너, Grid Layout과 검색 컴포넌트를 아래와 같이 JSON 데이터로 표현해보았다.
[
{
"type": "bannerCarousel",
"props": {
'items": [
// ...
{
"id": 99,
"image": "https://example.com/banner.png",
"url": "https://somewhere.com"
}
]
},
},
{
"type": "category",
"props": {
"categories": [
{
"title": "익스프레스",
"description": "어떤 음식이든 빠르게 먹고 싶을 땐",
"image": "https://example.com/express.png"
},
{
"title": "포장",
"description": null,
"image": "https://example.com/takeout.png"
},
{
"title": "선물하기",
"description": null,
"image": "https://example.com/gift.png"
}
]
},
"design": {
"grid": {
"row": 2,
"col": 2
},
// ...
}
},
{
"type": "searchBar",
"props": {
"placeholder": "음식점명이나 메뉴명으로 검색하세요.",
"onChange": () => {}
},
"design": {
// ...
}
},
{
"type": "menu",
"props": {
"items": [
{ "title": "전체", "image": "https://example.com/all.png" },
{ "title": "중국집", "image": "https://example.com/chinese.png" },
// ...
{ "title": "더보기", "image": "https://example.com/more.png" }
]
},
"design": {
"grid": {
"row": 3,
"col": 5
},
// ...
}
},
]
Slot
으로 만들어두면 되기 때문에 예측 불가능한 데이터 구조에도 대응하기 용이하다.💫 Slot이란?
출처: React에서 Slot 패턴과 Coumpound Component 패턴 사용
컴포넌트 내부 특정 위치에 자식 콘텐츠를 삽입할 수 있도록 자리를 미리 지정하고, 이후에 자식 컴포넌트를 동적으로 삽입할 수 있게 하는 컴포넌트 디자인 구조이다.
border-radius
정도의 속성만 서버에서 받아서 그려줄 수 있게 되는 것이다.Airbnb는 이러한 SDUI의 장점을 적극 활용하여 그들만의 시스템인 Ghost Platform(GP) 을 만들었다. 이를 통해 Airbnb는 백엔드에서 데이터를 제어하고 모든 클라이언트에서 동시에 데이터가 표시되는 방식을 제어할 수 있게 되었다. 화면 레이아웃, 섹션의 배열 방식, 내부에 표시되는 데이터와 사용자와의 interaction 작업까지 웹, iOS, Android 환경에서 모두 단일 백엔드 응답으로 제어되도록 GraphQL의 universal 스키마를 사용할 수 있게 구현되었다고 한다.
출처: Airbnb 기술 블로그
Section과 Screen
GP Response의 GraphQL 스키마는 아래와 같이 구현되어 있다.
interface GPResponse {
sections: [SectionContainer]
screens: [ScreenContainer]
# ... Other metadata, logging data or feature-specific logic
}
section
배열 내의 배치를 설명하는 데이터이다.미래에는,,,
추후 Airbnb는 Nested Section 관련 기능을 추가하고, Figma 또는 WISYWIG 기능을 통해 no-code로 UI를 수정할 수 있는 형태로의 확장을 목표하고 있다고 한다. 더 자세한 GP에 대한 내용이 궁금하다면 원문을 참조할 수 있다.
위 이미지는 Freepik에서 가져온 Mockup Asset이다. 이 중 첫 번째 화면인 Main Screen을 기준으로 코드를 작성해보자.
const data = {
user: {
name: "Jacob",
profileImage: "https://example.com/profile.jpg",
subtext: "Find causes you care about and make an impact.",
},
categories: [
{ name: "Health" },
{ name: "Education" },
{ name: "Environment" },
{ name: "Animals" },
{ name: "Community" },
],
issues: [
{
id: 1,
title: "Save the Rainforest",
image: "https://example.com/rainforest.jpg",
raised: 5000,
goal: 10000,
progress: 50,
highlight: true,
},
{
id: 2,
title: "Support Local Schools",
image: "https://example.com/schools.jpg",
raised: 800,
goal: 5000,
progress: 16,
highlight: false,
},
{
id: 3,
title: "Help Stray Animals",
image: "https://example.com/animals.jpg",
raised: 2000,
goal: 3000,
progress: 66,
highlight: true,
},
],
};
이러한 데이터를 클라이언트 측에서 UI 상에 표현하면 아래와 같을 것이다. (스타일은 별도로 CSS를 통해 구현될 것이다.)
import React from "react";
const Home: React.FC = () => {
// 데이터 바인딩
const { user, categories, issues } = data;
return (
<div>
{/* Header Section */}
<div>
<h2>{`Welcome, ${user.name}`}</h2>
<p>{user.subtext}</p>
<img
src={user.profileImage}
alt="Profile"
/>
</div>
{/* Category Section */}
<div>
<h3>Categories</h3>
{categories.map((category, index) => (
<button
key={index}
onClick={() => navigate(`/${category.name}`)}
>
{category.name}
</button>
))}
</div>
{/* Issues Section */}
<div>
<h3 style={styles.sectionTitle}>Trending Causes</h3>
{issues.map((issue) => (
<div
key={issue.id}
>
<img
src={issue.image}
alt={issue.title}
/>
<h4>{issue.title}</h4>
<p>
Raised: ${issue.raised} / ${issue.goal}
</p>
<div>
<div
/>
</div>
</div>
))}
</div>
</div>
);
};
// 스타일 정의
const styles = {
header: {
backgroundColor: "#4f46e5",
padding: "16px",
color: "#ffffff",
textAlign: "center",
},
// ...
};
export default Home;
이렇게 구현할 경우 UI 레이아웃과 스타일이 컴포넌트 자체에 하드코딩 되기 때문에 데이터를 바탕으로 UI가 렌더링 되고, 각 컴포넌트의 구조와 스타일을 변경하려면 별도의 작업이 필요하다.
만약 위의 이미지에서 메인 페이지의 컴포넌트 배치 순서를 바꾼다거나 UI의 스타일을 수정하려 한다면 클라이언트 코드를 수정하고 해당 어플리케이션을 다시 배포하는 과정을 거쳐야 한다.
🤔 SDUI 없는 개발 프로세스는 어떻게 진행됐나요
앞에서도 언급했듯, 빠른 테스트와 검증이 필요한 환경에서는 이러한 과정이 비효율적일 수 있다.
Server Driven UI에서는 위에서처럼 전체 UI를 하드코딩하는 대신 클라이언트가 서버에 UI를 요청하고, 서버는 UI 구조를 포함하는 payload(JSON, XML 등)로 응답한다. 아래는 서버가 전송하는 데이터를 예상하여 작성해본 JSON 데이터이다.
{
"components": [
{
"type": "searchBar",
"placeholder": "Search for causes or categories...",
"style": {
"backgroundColor": "#f9fafb",
"textColor": "#333333",
"borderRadius": "8px",
"padding": "12px",
"fontSize": "14px"
}
},
{
"type": "header",
"text": "Welcome, Jacob",
"subtext": "Find causes you care about and make an impact.",
"profileImage": "https://example.com/profile.jpg",
"style": {
"backgroundColor": "#4f46e5",
"textColor": "#ffffff",
"profileImageBorderRadius": "50%",
"padding": "16px",
"textFontSize": "20px",
"subtextFontSize": "14px"
}
},
{
"type": "categorySection",
"title": "Categories",
"categories": [
{ "name": "Health", "highlight": true },
{ "name": "Education", "highlight": false },
{ "name": "Environment", "highlight": false },
{ "name": "Animals", "highlight": true },
{ "name": "Community", "highlight": false }
],
"style": {
"titleColor": "#1f2937",
"categoryButton": {
"highlightBackgroundColor": "#f87171",
"defaultBackgroundColor": "#e5e7eb",
"textColor": "#ffffff",
"borderRadius": "12px",
"padding": "10px"
}
}
},
{
"type": "issueSection",
"title": "Trending Causes",
"issues": [
{
"id": 1,
"title": "Save the Rainforest",
"image": "https://example.com/rainforest.jpg",
"raised": 5000,
"goal": 10000,
"progress": 50,
"highlight": true
},
{
"id": 2,
"title": "Support Local Schools",
"image": "https://example.com/schools.jpg",
"raised": 800,
"goal": 5000,
"progress": 16,
"highlight": false
},
{
"id": 3,
"title": "Help Stray Animals",
"image": "https://example.com/animals.jpg",
"raised": 2000,
"goal": 3000,
"progress": 66,
"highlight": true
}
],
"style": {
"titleColor": "#111827",
"issueCard": {
"highlightBackgroundColor": "#fbbf24",
"defaultBackgroundColor": "#ffffff",
"borderColor": "#d1d5db",
"padding": "16px",
"borderRadius": "8px",
"titleColor": "#374151",
"progressBarColor": "#4ade80"
}
}
}
]
}
클라이언트 측에서는 위와 같은 JSON/XML 데이터를 서버로부터 전송받은 뒤 아래처럼 렌더링할 수 있다.
ComponentMapper
는 서버에서 전달받은 데이터에 따라 UI를 동적으로 렌더링하기 위해 미리 클라이언트 측에서 특정 컴포넌트 타입과 React 함수형 컴포넌트를 매핑해둔 객체이다.
const ComponentMapper: Record<ComponentType, React.FC<any>> = {
searchBar: ({ placeholder }) => <input placeholder={placeholder} className="search-bar" />,
header: ({ text, subtext, profileImage }) => (
<div className="header">
<h2>{text}</h2>
<p>{subtext}</p>
<img src={profileImage} alt="Profile" />
</div>
),
categorySection: ({ title, categories }) => (
<div className="category-section">
<h3>{title}</h3>
{categories.map((cat: any, index: number) => (
<button key={index} className={cat.highlight ? 'highlight' : ''}>
{cat.name}
</button>
))}
</div>
),
issueSection: ({ title, issues }) => (
<div className="issue-section">
<h3>{title}</h3>
{issues.map((issue: any) => (
<div key={issue.id} className={issue.highlight ? 'issue-card highlight' : 'issue-card'}>
<img src={issue.image} alt={issue.title} />
<h4>{issue.title}</h4>
<p>{`Raised: $${issue.raised} / $${issue.goal}`}</p>
<div className="progress-bar">
<div style={{ width: `${issue.progress}%` }} className="progress" />
</div>
</div>
))}
</div>
),
};
위와 같이 정의해두고 실제 UI가 렌더링 되어야 하는 Slot에서는 배열을 순회하면서 각 요소의 type
속성에 따라 적절한 컴포넌트를 보여줄 수 있다
const App: React.FC<{ components: any[] }> = ({ components }) => {
return (
<div>
{components.map((component, index) => {
const Component = ComponentMapper[component.type];
return Component ? <Component key={index} {...component.props} /> : null;
})}
</div>
);
};
물론 SDUI가 모든 상황에서 최적의 방식이라는 것은 아니다.
그러므로 SDUI의 도입을 위해서는 팀 내에서 현재 상황을 파악하고 적합하다고 판단할 때에 도입하는 것이 중요해 보인다.
결론적으로, 디자인 변경이 빈번하지 않고, 복잡한 사용자 경험(UX)보다는 데이터 중심의 화면 구성이 중요한 경우, 그리고 프론트엔드의 개발 효율성과 유지보수를 최적화하고자 하는 상황에서는 SDUI 도입을 고려해볼 만 하다. 특히, SDUI에 대해 정리하면서 백엔드와 프론트엔드 간의 명확한 역할 분리를 통해 협업이 원활해지고, 새로운 페이지나 뷰를 빠르게 확장할 수 있는 환경이라면 SDUI는 강력한 선택지가 될 수 있겠다고 느꼈다.