Table of Contents
์น๊ณผ ๋ฌ๋ฆฌ ๋ค์ดํฐ๋ธ ๋ชจ๋ฐ์ผ ์ฑ์ ๋น๋, ๋ฐฐํฌ ํ์๋ ์์ ์ด ๋ถ๊ฐ๋ฅํ๋ค. ๋ง์ฝ ์๋ชป๋ ์์น์ ๋ฒํผ์ ๋ฐฐ์นํ ์ฑ๋ก ์คํ ์ด์ ์ฑ์ ๋ฐฐํฌํ๋ค๋ฉด, ๊ทธ๋ฆฌ๊ณ ์ฌ์ฉ์๊ฐ ์๋ชป๋ ๋ฒ์ ์ ์ฑ์ ์ค์นํ๋ค๋ฉด ๋ฒํผ์ ์์น๋ฅผ ์์ ํ ๋ฐฉ๋ฒ์ด ์๋ค. ์ ์ผํ ๋ฐฉ๋ฒ์ ์ฌ์ฉ์๊ฐ ์ค์ค๋ก ์คํ ์ด์ ๋ค์ด๊ฐ ์์ ๋ ๋ฒ์ ์ ์ฑ์ผ๋ก ์ ๋ฐ์ดํธํ๋ ๊ฒ ๋ฟ์ด๋ค.
๋ฐฐํฌ ํ ์์ ์ด ๋ถ๊ฐ๋ฅํ๋ค๋ ํน์ฑ์ด ๋ถ๋ชํ๋ ๋ ๋ค๋ฅธ ์ํฉ์ A/B ํ ์คํ ์ด๋ค. ์ํํธ์จ์ด๋ฅผ ์ฌ์ฉํ๋ ๋์ ์ผ์ด๋๋ ์ฌ์ฉ์์ ํ๋๊ณผ ๊ฒฝํ์ ํ๋ฉด ๊ตฌ์ฑ์ด๋ ๋ฌธ๊ตฌ์ ๋ฐ๋ผ ํฌ๊ฒ ๋ฌ๋ผ์ง๊ธฐ ๋๋ฌธ์ ์ต์ ์ ํ๋ฉด์ ๋์์ธํ๋ ๊ฒ์ด ์ค์ํ๋ค. ๊ทธ๋ฐ๋ฐ ์ฌ์ฉ์์ ํ๋๊ณผ ๊ฒฝํ์ ์์ธกํ๋ ๊ฒ์ ๋งค์ฐ ์ด๋ ค์ด ์ผ์ด๊ธฐ ๋๋ฌธ์ ํ์ค์ ์ฌ์ฉ์๋ค์๊ฒ ๋ค์ํ ์ ํ์ UI๋ฅผ ์ ๊ณตํ๊ณ , ์ด๋ค UI๊ฐ ์ ํฉํ์ง ์ค์ธกํ ํ์๊ฐ ์๋ค. ์ค์ ๋ก ๋ง์ ์ํํธ์จ์ด ๊ธฐ์ ๋ค์ด ์ฌ์ฉ์๋ฅผ A, B ๊ทธ๋ฃน์ผ๋ก ๋๋๊ณ (๋ ๋ง์ ๊ทธ๋ฃน์ผ๋ก ๋๋ ์๋ ์๋ค.) ๊ฐ ๊ทธ๋ฃน์๊ฒ ์๋ก ๋ค๋ฅธ UI๋ฅผ ์ ๊ณตํด ๊ฐ์ฅ ์ ํฉํ UI๋ฅผ ์ ์ ํ๋ A/B ํ ์คํ ์ ํ๊ณ ์๋ค.
์ ์ฐํ UI๋ฅผ ์ ๊ณตํ๋ ค๋ฉด UI๊ฐ ํด๋ผ์ด์ธํธ์ ๋น๋์ ๋ฐฐํฌ๋ก๋ถํฐ ์์ ๋ก์์ผ ํ๋ค. ์ด๋ฌํ ๋ชฉํ๋ฅผ ์ด๋ฃจ๊ธฐ ์ํด ์น๋ทฐ์ ๊ฐ์ด ๋ค์ดํฐ๋ธ ํ๊ฒฝ์ ๋ฒ์ด๋ ๋ค์ํ ๋ฐฉ๋ฒ์ ์ ํํ ์๋ ์๊ฒ ์ง๋ง, ํ์ค์์๋ ๋ค์ํ ์ด์ ๋ก ์น๋ทฐ๋ฅผ ์ฌ์ฉํ ์ ์๋ ์ํฉ์ด ์๋ค. ์ด ๊ธ์์๋ ์น๋ทฐ๋ฅผ ์ฌ์ฉํ์ง ์๋๋ค๋ ์ ์ ํ์ ์ ์ฐํ๊ฒ UI๋ฅผ ๋ค๋ฃจ๊ธฐ ์ํ Server Driven UI ์ค๊ณ์ ๋ํด ์๊ฐํ๊ณ ์ ํ๋ค.
์๋ฒ์์ UI ๋ค๋ฃจ๊ธฐ
ํด๋ผ์ด์ธํธ๊ณผ ๋ฌ๋ฆฌ ์๋ฒ๋ ์ธ์ ๋ ๋ณ๊ฒฝ, ๋ฐฐํฌํ ์ ์๋ค. ๊ทธ๋ ๋ค๋ฉด ์๋ฒ์์ ์ ๊ณตํ๋ API๋ฅผ ์ด์ฉํด ๋์ ์ผ๋ก ํด๋ผ์ด์ธํธ์ UI๋ฅผ ๊ตฌ์ฑํ๋ฉด ์ด๋จ๊น? ์๋ฒ๊ฐ API ์๋ต์ UI ์ ๋ณด๋ฅผ ๋ด์ ํด๋ผ์ด์ธํธ์ ์ ๊ณตํ๊ณ , ํด๋ผ์ด์ธํธ๊ฐ API ์๋ต์ ๋ฐ๋ผ ํ๋ฉด์ ๋ ๋๋งํ๋ค๋ฉด ์๋ฒ์์ API ์๋ต์ ๋ณ๊ฒฝํ๋ ๊ฒ๋ง์ผ๋ก ํด๋ผ์ด์ธํธ์ ํ๋ฉด ๊ตฌ์ฑ์ ๋์ ์ผ๋ก ๋ณ๊ฒฝํ ์ ์์ ๊ฒ์ด๋ค.
์๋ฅผ ๋ค์ด ์ฌ์ฉ์์๊ฒ ํ ํ๋ฉด์ ์ ๊ณตํ๋ ๊ฒฝ์ฐ, ์๋ฒ๊ฐ ์ ๊ณตํ๋ REST API screen
์ ํตํด home
ํ๋ฉด์ ๋ํ UI ์ ๋ณด๋ฅผ ์ป์ ์ ์์ ๊ฒ์ด๋ค.
GET /screen/home
์ด API๋ ํ ํ๋ฉด์ ๊ตฌ์ฑํ๋ UI ์์ ๋ฆฌ์คํธ๋ฅผ JSON ํฌ๋งท์ผ๋ก ์๋ตํ๋ค. ๋ฐ๋ผ์ ํด๋ผ์ด์ธํธ๋ ์๋ต์ data
๋ฆฌ์คํธ๋ฅผ ์ํํ๋ฉฐ ๊ฐ๊ฐ์ type
์ ํด๋นํ๋ UI ์์๋ฅผ ํ๋ฉด์ ๊ทธ๋ ค์ฃผ๋ฉด ๋๋ค.
์ด๋ ๊ฒ ํ๋ฉด ํด๋ผ์ด์ธํธ๋ฅผ ์๋ก ๋ฐฐํฌํ์ง ์์๋ ์๋ฒ์์ data
๋ฆฌ์คํธ์ ์์๋ฅผ ๋ณ๊ฒฝํจ์ผ๋ก์จ ํด๋ผ์ด์ธํธ๊ฐ ์ ์ฐํ UI๋ฅผ ์ ๊ณตํ ์ ์์ ๊ฒ์ด๋ค. ์ด์ฒ๋ผ UI์ ๋ํ ์ ๋ณด๋ฅผ ์๋ฒ์์ ๊ด๋ฆฌ, ์ ๊ณตํ๋ ๊ฒ์ด Server Driven UI ์ค๊ณ์ ๊ธฐ๋ณธ ๊ฐ๋
์ด๋ค.
GraphQL: ์ฌ์ฌ์ฉ ๊ฐ๋ฅํ UI ์ปดํฌ๋ํธ ์ ๊ณตํ๊ธฐ
์๋ฒ์์ UI๋ฅผ ๊ด๋ฆฌํ๋ฉด ์ ์ฐ์ฑ์ ํ๋ณดํ ์ ์์ง๋ง, ์ฌ์ฉํ๋ UI ์์์ ์ฌ์ฌ์ฉ์ฑ์ ๊ณ ๋ คํ์ง ์์ผ๋ฉด ๋ค์ํ ํ๋ฉด์์ UI ์์๋ฅผ ๊ต์ฒดํ๊ธฐ ์ด๋ ค์์ง๋ค. ์ด๋ฌํ ๋ฌธ์ ๋ฅผ ํผํ๋ ค๋ฉด ๋ชจ๋ UI ์์๋ฅผ ์ฌ์ฌ์ฉ ๊ฐ๋ฅํ ์ปดํฌ๋ํธ๋ก ๊ตฌ์ฑํ๊ณ , UI ์ปดํฌ๋ํธ๋ฅผ ๋ค์ํ ํ๋ฉด์์ ์กฐ๋ฆฝํด์ ์ฌ์ฉํ ์ ์๋๋ก ๋ง๋ค์ด์ผ ํ๋ค.
๋ํ ์์๋ก ํ๋ฉด์ ์๋ก์ด ์ปดํฌ๋ํธ๊ฐ ์ถ๊ฐ๋๊ณ ์ ๊ฑฐ๋๋ฉด ์๋ฒ์ ํด๋ผ์ด์ธํธ ์ฌ์ด์ ํ์ ์ ์์ ๋ถ์ผ์น๊ฐ ๋ฐ์ํ๊ธฐ ์ฝ๋ค. ์ด๋ GraphQL์ ์ฌ์ฉํ๋ฉด ์๋ฒ์ ํด๋ผ์ด์ธํธ๊ฐ ๊ณต์ ํ๋ ์คํค๋ง๋ฅผ ํตํด API์ ํ์ ์์ ์ฑ์ ๋ณด์ฅํ ์ ์๋ค.
์ฟผ๋ฆฌ ์ค๊ณ
์๋ฒ๋ UI ์ปดํฌ๋ํธ ๋ฆฌ์คํธ๋ฅผ ๋ฐํํ๋ screen
์ฟผ๋ฆฌ๋ฅผ ํตํด ํน์ ํ๋ฉด์ ๋ํ UI ์ ๋ณด๋ฅผ ์ ๊ณตํ๋ค.
type Query {
screen(screenType: ScreenType!): Screen!
}
enum ScreenType {
HOME
SIGN_IN
}
type Screen {
components: [Component!]!
}
ํด๋ผ์ด์ธํธ๋ screen
์ฟผ๋ฆฌ๋ฅผ ํธ์ถํ๋ฉฐ ํ ํ๋ฉด์์๋ screenType: HOME
์ธ์๋ฅผ, ๋ก๊ทธ์ธ ํ๋ฉด์์๋ screenType: SIGN_IN
์ธ์๋ฅผ ์ ๋ฌํ ๊ฒ์ด๋ค. ์๋ฒ๋ ์ฟผ๋ฆฌ๋ฅผ ๋ฐ์ผ๋ฉด ํด๋น screenType
์ ๋ง๋ ์ปดํฌ๋ํธ๋ฅผ ์กฐํฉํ์ฌ Screen
ํ์
์ components
ํ๋์ Component
๋ฆฌ์คํธ๋ฅผ ๋ด์ ์๋ตํ๋ค.
Component
๋ ์ ๋์จ ํ์
์ด๋ค. ์ ๋์จ ํ์
์ ๋ค์ํ ํ์
์ ์ปดํฌ๋ํธ๋ฅผ Component
๋ผ๋ ํ๋์ ํ์
์ผ๋ก ๋ค๋ฃฐ ์ ์๊ฒ ํด์ค๋ค. Screen
ํ์
์ components
ํ๋๊ฐ Component
๋ฆฌ์คํธ๋ฅผ ๋ฐํํ๋ค๋ ๊ฒ์ ๋ฆฌ์คํธ ์์ AppBar
, TextButton
, Image
ํ์
์ด ์์ผ ์ ์๋ค๋ ์๋ฏธ๋ค.
union Component = AppBar | TextButton | Image
type AppBar {
title: String!
}
type TextButton {
text: String!
route: String
}
type Image {
url: String!
}
๋ง์ฝ ์ปดํฌ๋ํธ๋ค์ด ๊ณตํต ํ๋๋ฅผ ๊ฐ์ง๋ค๋ฉด Component
๋ฅผ ์ ๋์จ ํ์
๋์ ์ธํฐํ์ด์ค๋ก ๋ง๋ค์ด๋ ๋๋ค.
interface Component {
position: Int!
}
type AppBar implements Component {
position: Int!
title: String!
}
type TextButton implements Component {
position: Int!
text: String!
route: String
}
type Image implements Component {
position: Int!
url: String!
}
์ ๋์จ ํ์
์ ๋จ์ํ ๋
๋ฆฝ์ ์ธ ์ปดํฌ๋ํธ ํ์
๋ค์ ํ๋์ ํ์
์ผ๋ก ์ฌ์ฉํ๊ธฐ ์ํ ๋ฐฉ์์ด์๋ค๋ฉด, ์ธํฐํ์ด์ค๋ ๊ฐ๊ฐ์ ์ปดํฌ๋ํธ ํ์
๋ค์ด ์ถ์ ํ์
์ธ Component
๋ฅผ ๊ตฌํํ๋ ๋ฐฉ์์ด๊ธฐ ๋๋ฌธ์ ์ด๋ค ํ์
์ด UI ์ปดํฌ๋ํธ์ธ์ง ๋ช
ํํด์ง๋ค๋ ์ฅ์ ์ด ์๋ค.
์์ฒญ๊ณผ ์๋ต
GraphQL์ ์ฌ์ฌ์ฉ ๊ฐ๋ฅํ ํ๋ ๋ฌถ์์ธ ํ๋๊ทธ๋จผํธ(Fragment)๋ UI ์ปดํฌ๋ํธ๋ฅผ ์ฃผ๊ณ ๋ฐ๊ธฐ์ ๋งค์ฐ ์ ํฉํ๋ค. ํด๋ผ์ด์ธํธ์์ ์ปดํฌ๋ํธ๋ฅผ ์์ฒญํ ๋๋ ์ฌ์ฉ ๊ฐ๋ฅํ ๋ชจ๋ ์ปดํฌ๋ํธ ํ๋๊ทธ๋จผํธ๋ฅผ ์์ฒญํ ๊ฒ์ด๋ค.
query fetchScreen {
screen(screenType: HOME) {
components {
... on AppBar {
__typename
title
}
... on TextButton {
__typename
text
route
}
... on Image {
__typename
url
}
}
}
}
์ฃผ์ํ ์ ์ '์ฌ์ฉ ๊ฐ๋ฅํ ๋ชจ๋ ์ปดํฌ๋ํธโ๋ฅผ ์์ฒญํ๋ค๋ ์ ์ด๋ค. ๋ง์ฝ ๊ตฌํ ๋น์์ ์ฌ์ฉํ ์ปดํฌ๋ํธ๋ง ์์ฒญํ๋ฉด ์ฐจํ ์๋ฒ์์ ๋ค๋ฅธ ์ปดํฌ๋ํธ๋ฅผ ํ๋ฉด์ ์ถ๊ฐํด๋ ๋ณด์ฌ์ค ์ ์๊ธฐ ๋๋ฌธ์ด๋ค.
์์ฒญ์ ๋ฐ์ ์๋ฒ๋ ํ ํ๋ฉด์์ ์ฌ์ฉํ ์ปดํฌ๋ํธ๋ฅผ ๊ณจ๋ผ์ ๋ฐํํ๋ค. ์ด ์์์์๋ ํ ํ๋ฉด์ AppBar
์ปดํฌ๋ํธ ํ๋์ TextButton
์ปดํฌ๋ํธ ๋ ๊ฐ๋ฅผ ์๋ตํ๋ค.
impl QueryRoot {
fn screen(screen_type: ScreenType) -> FieldResult<Screen> {
Ok(
Screen {
components: match screen_type {
ScreenType::Home => home_components(),
ScreenType::SignIn => sign_in_components(),
}
}
)
}
}
fn home_components() -> Vec<Component> {
vec![
Component::AppBar(AppBar {
title: "Home".to_string(),
}),
Component::TextButton(TextButton {
text: "Sign in".to_string(),
route: Some("/sign_in".to_string()),
}),
Component::TextButton(TextButton {
text: "Sign up".to_string(),
route: None,
}),
]
}
๋ฌ์คํธ๋ก ์๋ฒ ์ฝ๋๋ฅผ ์์ฑํ ์ด์ ๋ ์์ ํ ๊ฐ์ธ ์ทจํฅ์ด๋ฉฐ, Server Driven UI๋ GraphQL๊ณผ๋ ์ ํ ๊ด๋ จ์ด ์๋ค. ๋ค์ํ ์ธ์ด๋ก ๋ GraphQL API ์๋ฒ ๊ตฌํ์ฒด๊ฐ ์๊ธฐ ๋๋ฌธ์ ์ธ์ด์ ์ ํ์ ๋ฌธ์ ๊ฐ ๋์ง ์๋๋ค.[1]
์์ฒญ์ด ์ฑ๊ณตํ๋ฉด ์๋ฒ์์ ์๋ํ GraphQL ์๋ต์ ๋ฐ์ ์ ์๋ค.
{
"data": {
"screen": {
"components": [
{
"__typename": "AppBar",
"title": "Home"
},
{
"__typename": "TextButton",
"text": "Sign in",
"route": "/sign_in"
},
{
"__typename": "TextButton",
"text": "Sign up",
"route": null
}
]
}
}
}
์์ ํด๋ผ์ด์ธํธ๊ฐ components
ํ๋ ์๋์ Image
ํ๋๊ทธ๋จผํธ๋ ์์ฒญํ์ง๋ง, ์๋ฒ๊ฐ Image
์ปดํฌ๋ํธ๋ฅผ ์๋ตํ์ง ์์๊ธฐ ๋๋ฌธ์ ๋ฆฌ์คํธ์๋ ํฌํจ๋์ง ์์๋ค. ๋ฐ๋๋ก ์๋ฒ๊ฐ Image
์ปดํฌ๋ํธ๋ฅผ ์๋ตํ์ง๋ง ํด๋ผ์ด์ธํธ๊ฐ ์์ฒญํ์ง ๊ฒฝ์ฐ์๋ ๋ฆฌ์คํธ์ ํฌํจ๋์ง ์๋๋ค. ๋ฐ๋ผ์ ์๋ฒ๊ฐ ์ ๊ท ์ปดํฌ๋ํธ๋ฅผ ์ ์ํ๊ฑฐ๋ ๊ธฐ์กด ์ปดํฌ๋ํธ์ ์ ๊ท ํ๋๋ฅผ ์ถ๊ฐํด๋ ๊ตฌ๋ฒ์ ํด๋ผ์ด์ธํธ์์๋ ์ ๊ท ์ปดํฌ๋ํธ์ ํ๋๋ฅผ ์์ฒญํ์ง ์๊ธฐ ๋๋ฌธ์ ํด๋ผ์ด์ธํธ์ ํ์ํธํ์ฑ์ ํ๋ณดํ ์ ์๋ค. ๋จ, ๊ธฐ์กด ์ปดํฌ๋ํธ์ ๋ํด ๊ตฌ๋ฒ์ ํด๋ผ์ด์ธํธ์์ ์ฌ์ฉ ์ค์ธ ํ๋๋ฅผ ์ ๊ฑฐํ๊ฑฐ๋ non-nullable ํ๋๋ฅผ nullable ํ๋๋ก ๋ฐ๊พธ๋ ๊ฒฝ์ฐ ํ์ํธํ์ฑ์ด ๊นจ์ง๋ฏ๋ก ์ฃผ์ํด์ผ ํ๋ค.
Flutter: ๊ฒฌ๊ณ ํ ๋์์ธ ์์คํ ๊ณผ ์์ ฏ์ผ๋ก ํ๋ฉด ๊ทธ๋ฆฌ๊ธฐ
ํต์ผ๊ฐ์๋ ์ปดํฌ๋ํธ๋ฅผ ์ฌ์ฉํ๋ ค๋ฉด ๋์์ธ ์์คํ ์ด ์ ์กํ ์์ด์ผ ํ๋ค. ๋ง์ฝ UI ๋ ๋ฒจ์์ ๋์์ธ ์์คํ ์ด ์ ๋ฆฝ๋์ด ์์ง ์๋ค๋ฉด ์ ์ด์ ์ปดํฌ๋ํธ๋ฅผ ๊ฐ๋ ์ ๋์ ํ๋ ๊ฒ์ด ์ด๋ถ์ฑ์ค์ผ ๋ฟ๋๋ฌ, ์๋ฒ์ ํด๋ผ์ด์ธํธ, ๋์์ธ ์ฌ์ด์ ์ฌ์ฉํ๋ ์ฉ์ด๊ฐ ๋ฌ๋ผ์ ธ ์ปค๋ฎค๋์ผ์ด์ ๋น์ฉ๋ ์ฆ๊ฐํ๋ค.
ํ๋ฌํฐ(Flutter)์ ๋จธํฐ๋ฆฌ์ผ ๋ผ์ด๋ธ๋ฌ๋ฆฌ(Material Library)๋ ๊ตฌ๊ธ์ ๋จธํฐ๋ฆฌ์ผ ๋์์ธ ์์คํ ์ ๋์ ์์ค์ผ๋ก ๊ตฌํํ๊ณ ์์ด Server Driven UI๋ฅผ ๋ฐ๋ก ์ ์ฉํ ์ ์๋ค.
ํ๋๊ทธ๋จผํธ-์ปดํฌ๋ํธ-์์ ฏ ๋์
ํ๋ฌํฐ๊ฐ ๊ฐ์ง ์์ ฏ(Widget) ๊ฐ๋ ์ด ์ปดํฌ๋ํธ ๊ฐ๋ ๊ณผ ๋ถํฉํ๋ค๋ ์ ๋ Server Driven UI ์ค๊ณ์ ์ ๋ง๋๋ค. ํ๋ฌํฐ์ ์์ ฏ์ ์น ํ๋ก ํธ์๋ ํ๋ ์์ํฌ์ธ ๋ฆฌ์กํธ(React)์ ์ปดํฌ๋ํธ ์์คํ ์ผ๋ก๋ถํฐ ์๊ฐ์ ๋ฐ์ ๋ง๋ค์ด์ก์ผ๋ฉฐ, ๊ฐ ์์ ฏ์ ์์ ์ ํ์ฌ ์ํ์ ๋ฐ๋ฅธ UI๋ฅผ ํํํ๋ค.[2]
ํด๋ผ์ด์ธํธ์ ์ถ์ ํด๋์ค Component
๋ ์๋ฒ์์ ์๋ตํ๋ GraphQL ์ ๋์จ ํ์
Component
์ ๋์๋๋ค. Component
๋ฅผ ๊ตฌํํ๋ ํด๋์ค๋ ์์ ฏ์ ๋ฐํํ๋ compose
๋ฉ์๋๋ฅผ ํจ๊ป ๊ตฌํํด์ผ ํ๋ค.
abstract class Component {
Widget compose(Map<String, dynamic> args, BuildContext context);
}
๊ฐ๋ น ์ฑ ์๋จ์ ๋ค์ด๊ฐ๋ ์ฑ๋ฐ UI๋ฅผ ์๋ฏธํ๋ AppBarComponent
๋ Component
ํด๋์ค๋ฅผ ๊ตฌํํ๋ฉฐ, AppBar
์์ ฏ์ ๋ฐํํ๋ compose
๋ฉ์๋๋ฅผ ๊ฐ๋๋ค.
class AppBarComponent implements Component {
Widget compose(Map<String, dynamic> args, BuildContext context) {
return AppBar(
title: Text(args['title']),
);
}
}
compose
๋ฉ์๋๋ args
์ธ์์ title
ํ๋กํผํฐ์ ์ ๊ทผํด ์ฑ๋ฐ์ ํ์ดํ์ ์ฑ์ด๋ค. ์ด๋ args
์ธ์๋ ์๋ฒ์ ์๋ต์ ํฌํจ๋๋ AppBar
ํ๋๊ทธ๋จผํธ์ ํ๋๋ค์ด Map<String, dynamic>
ํ์
์ผ๋ก ์ ๋ฌ๋ ๊ฒ์ด๋ค.
ํด๋ผ์ด์ธํธ๊ฐ ์๋ฒ๋ก๋ถํฐ ๋ฐ์ ์๋ต์ ํ์ฑํ ๋ค์, components
ํ๋์ ํฌํจ๋ ๊ฐ๊ฐ์ ํ๋๊ทธ๋จผํธ๋ค์ ์์ ์ ์ปดํฌ๋ํธ์ ๋์์ํค๊ณ , ๊ฐ ์ปดํฌ๋ํธ์ ์์ ฏ์ ๋์์ํค๋ ค๋ฉด GraphQL ์คํค๋ง๋ฅผ ๋ฐํ์ผ๋ก ํ ์ปดํฌ๋ํธ ๋ ์ง์คํธ๋ฆฌ๊ฐ ํ์ํ๋ค.
class Registry {
static final Map<String, Component> _dictionary = {
'AppBar': AppBarComponent(),
'TextField': TextFieldComponent(),
'Image': ImageComponent(),
};
static Widget getComponent(dynamic component, BuildContext context) {
var matchedComponent = _dictionary[component['__typename']];
if (matchedComponent != null) {
return matchedComponent.compose(component, context);
} else {
return null;
}
}
static List<Widget> getComponents(dynamic components, BuildContext context) {
var matchedComponent = components as List<dynamic>;
return matchedComponent.map((component) => getComponent(component, context))
.where((element) => element != null)
.toList();
}
}
ํด๋ผ์ด์ธํธ๋ ์๋ต ๋ด์ฉ์ ๋ฐํ์ผ๋ก ์์ ฏ ๋ฆฌ์คํธ๋ฅผ ์ป๊ธฐ ์ํด ๋ ์ง์คํธ๋ฆฌ์ getComponents
๋ฉ์๋๋ฅผ ํธ์ถํ๊ณ , components
ํ๋๋ฅผ ์ํํ๋ฉฐ getComponent
๋ฉ์๋๋ฅผ ํตํด ํ๋๊ทธ๋จผํธ๋ฅผ ์์ ฏ์ผ๋ก ๋ณํํ๋ค.
getCompnent
๋ฉ์๋๋ ํ๋๊ทธ๋จผํธ์ ํฌํจ๋ ๋ฉํ ํ๋ __typename
๊ฐ์ ์ด์ฉํด ๊ฐ ํ๋๊ทธ๋จผํธ๋ฅผ ์ปดํฌ๋ํธ์ ๋์์ํค๊ณ , ํด๋น ์ปดํฌ๋ํธ์ compose
๋ฉ์๋๋ฅผ ํธ์ถํด ์ปดํฌ๋ํธ ๊ฐ๊ฐ์ ์์ ฏ์ ๋ฐํํ๋ค. ๋ง์ฝ ํด๋ผ์ด์ธํธ๊ฐ ๋ชจ๋ฅด๋(_dictionary
์ ๋ฑ๋ก๋์ง ์์) ์ปดํฌ๋ํธ๊ฐ ์๋ต์ ํฌํจ๋์ด ์๋ค๋ฉด ํํฐ๋ง๋ ๊ฒ์ด๋ค.
์ปดํฌ๋ํธ ์กฐ๋ฆฝ
์์ ์ดํด๋ณธ ๋ ์ง์คํธ๋ฆฌ๋ฅผ ์ด์ฉํด ์๋ฒ์์ ์๋ตํ๋ ๋ชจ๋ ์ปดํฌ๋ํธ๋ฅผ ์์ ฏ์ผ๋ก ๋ณํํ๊ณ , ๊ฐ ํ๋ฉด์ ๋ง๋ ์์ ฏ์ ๊ตฌ์ฑํ ์ ์๊ฒ ๋์๋ค. ์ง๊ธ๊น์ง์ ํ๋ฆ์ ์๋ฒ, API ์๋ต, ํด๋ผ์ด์ธํธ๋ก ์ ๋ฆฌํ๋ฉด ์๋์ ๊ฐ๋ค.
ํ๋ฌํฐ ์์ ฏ ์ค Container
์ด๋ Column
๊ณผ ๊ฐ์ด ๋ค๋ฅธ ์์ ฏ์ child
๋๋ children
์ผ๋ก ๋ด๋ ์์ ฏ๋ ๊ฐ์ ๋ฐฉ์์ผ๋ก ๋ง๋ค ์ ์๋ค. ์ผ์ข
์ '์ปดํฌ๋ํธ์ ์ปดํฌ๋ํธโ์ธ ์
์ด๋ค. ์๋ฅผ ๋ค์ด GridView
์์ ฏ์ ์๊ฐํด๋ณด์. ๊ทธ๋ฆฌ๋ ๋ทฐ๋ ๊ฒฉ์ ์
์ด ๋ฐ๋ณต๋๋ ๋ ์ด์์์ผ๋ก, ๊ฐ ์
์๋ ๋ค๋ฅธ ์์ ฏ์ ๋ฐฐ์น์ํฌ ์ ์๋ค. ๊ทธ๋ฆฌ๋์ ์ด ๊ฐ์์ ๊ฐ ์
์ ๋ฃ์ ์ปดํฌ๋ํธ๋ฅผ ์๋ฒ์์ ๊ด๋ฆฌํ๊ณ ์ ํ๋ค๋ฉด ์๋์ ๊ฐ์ด ์คํค๋ง๋ฅผ ๊ตฌ์ฑํ ์ ์์ ๊ฒ์ด๋ค.
type GridView {
column_count: Int!
chidren: [Component!]!
}
ํด๋ผ์ด์ธํธ์์ ์์ฒญํ ๋๋ GridView
ํ๋๊ทธ๋จผํธ์ children
ํ๋ ์๋์ ์ฌ์ฉ ๊ฐ๋ฅํ ๋ชจ๋ ์ปดํฌ๋ํธ๋ฅผ ์์ฒญํด์ผ ํ๋ค. ๊ฐ์ ํ๋๊ทธ๋จผํธ๋ฅผ ์ฌ์ฌ์ฉํ๋ฏ๋ก ์ธ๋ผ์ธ ํ๋๊ทธ๋จผํธ ๋์ ๋ณ๋์ ๊ธฐ๋ช
ํ๋๊ทธ๋จผํธ๋ฅผ ๋ง๋ค์๋ค.
query fetchScreen {
screen(screenType: HOME) {
components {
... AppBar
... TextButton
... Image
... on GridView {
__typename
column_count
children {
... AppBar
... TextButton
... Image
}
}
}
}
}
fragment AppBar on AppBar {
__typename
title
}
fragment TextButton on TextButton {
__typename
text
route
}
fragment Image on Image {
__typename
url
}
๋ง์ง๋ง์ผ๋ก GridVew
์์ ฏ์ ๋ฐํํ๋ GridViewComponent
๋ ์์ ์ children
ํ๋ ๊ฐ์ getComponents
๋ก ๋๊ฒจ ์์ ฏ ๋ฆฌ์คํธ๋ฅผ ๊ตฌ์ฑํ๋ค.
class GridViewComponent implements Component {
Widget compose(Map<String, dynamic> args, BuildContext context) {
return Expanded(
child: GridView.count(
padding: const EdgeInsets.all(20),
crossAxisSpacing: 20,
mainAxisSpacing: 20,
crossAxisCount: args["columnCount"],
children: Registry.getComponents(args["children"], context),
),
);
}
}
์ฌ๊ธฐ์๋ padding
, crossAxisSpacing
, mainAxisSpacing
ํ๋กํผํฐ๋ฅผ ์์ ๊ฐ์ผ๋ก ์ค์ ํ์ง๋ง, ๋ง์ฝ ์๋ฒ์์ ๊ด๋ฆฌํ๊ณ ์ถ๋ค๋ฉด ๊ฐ๋จํ ํ๋๋ฅผ ์ถ๊ฐํ๊ณ ๊ฐ์ ๋ฃ์ด์ฃผ๊ธฐ๋ง ํ๋ฉด ๋๋ค.
์ด์ ์๋ฒ์์ GridView
์ children
ํ๋์ TextButton
๋ ๊ฐ๋ฅผ ์๋ตํ๋ฉด ํด๋ผ์ด์ธํธ์์๋ ๊ทธ๋๋ก ๋ ๊ฐ์ ์
์ TextButton
์ด ๋ด๊ธด ํ๋ฉด์ ๊ตฌ์ฑํ๋ค. ๊ทธ๋ฆฌ๋์ ์ด ๊ฐ์๋ ๊ฐ ์
์ ๋ด์ฉ์ ์๋ฒ ์๋ต์ ์์ ํ๋ ๊ฒ๋ง์ผ๋ก ์ธ์ ๋ ๋ณ๊ฒฝํ ์ ์๋ค.
์ค์ ๋์ํ๋ ์ฝ๋๋ github.com/parksb/server-driven-ui์์ ํ์ธํ ์ ์๋ค. ๋์ ๋ฐฉ์์ด๋ ๊ฐ๋ ์ ์ด ๊ธ์์ ์ค๋ช ํ ๊ฒ๊ณผ ๋์ผํ์ง๋ง, ์ปดํฌ๋ํธ ์ข ๋ฅ๋ ๊ตฌ์ฒด์ ์ธ ํ๋๋ ๋ค์ ์ฐจ์ด๊ฐ ์๋ค. ๋ํ ์นด์นด์ค์คํ์ผ์์๋ Server Driven UI ์ค๊ณ๋ฅผ ์ ๊ทน์ ์ผ๋ก ์ฌ์ฉํ๊ณ ์๋๋ฐ, ์นด์นด์ค์คํ์ผ ๊ธฐ์ ๋ธ๋ก๊ทธ์์ ์์ธํ ๋ณผ ์ ์๋ค.
References
- ๊น๋ํ, โServer Driven UI (Feat.Flutter)โ, 2020.
- ๋ฐํธ์ค, โGraphQL์ด ๊ฐ์ ธ์จ ์์ด๋น์ค๋น ํ๋ก ํธ์ค๋ ๊ธฐ์ ์ ๋ณ์ฒ์ฌโ, DEVIEW 2020, 2020.
- Tom Lokhorst, โServer Driven UIโ, Tom Lokhorstโs blog, 2020.
- Ryan Brroks, โA Deep Dive into Airbnbโs Server-Driven UI Systemโ, 2021.