Engineering
여기어때 셀러카드 디자인 시스템
Richard여기어때
2022년 3월 16일
원문에서 보기 ↗
Photo by Ricardo Gomez Angel on Unsplash
안녕하세요. 여기어때에서 안드로이드 개발을 하고 있는 리처드입니다.
여기어때를 사용하는 유저들이 가장 많이 접하게 되는 셀러카드! 셀러카드가 뭐냐? 라고 생각하실 거 같은데요.
셀러카드는 숙소의 사진, 숙박 정보를 담고 있는 카드입니다. 이미지로 보시면 바로 이해가 되실거에요.

Photo by Richard on GC COMPANY Corp
카드 하나에 숙소의 많은 정보들이 들어가 있습니다. 카드의 메인을 장식하는 이미지부터 숙소의 등급, 이름, 가격 등등 많은 정보들을 담고 있는데요.
여기어때는 코드 수정 없이 셀러카드의 디자인을 변경할 수 있습니다. 바로 이렇게요!

Photo by Richard on GC COMPANY Corp
쿠폰, 타이틀, 가격, 평점, 취향, 이벤트, 배경 등 셀러카드의 모든 요소들의 색상을 코드 수정 없이 자유 자재로 변경할 수 있습니다.
어떻게 되는 건지 궁금하신 분들이 계실거 같아요. 코드로 구현하기 전에 이미지를 통해서 어떤 시스템으로 동작하는 건지 파악해 보겠습니다.

Photo by Richard on GC COMPANY Corp
Palette 는 앱에서 사용할 색상들을 모아놓은 도구입니다. 색상 이름 옆에 숫자는 투명도를 의미하는데요 숫자가 커질수록 색이 선명해집니다.
앱이 실행되면 서버에서 Palette 데이터를 갖고와서 보관하고 있다가 필요할 때마다 꺼내서 사용하고 있습니다.
red_50 과 같은 이름은 사람이 구분하기 위한 이름이에요. 안드로이드는 이게 어떤 색인지 알지 못합니다. 그래서 Palette 에는 각 색상에 매칭되는 Hex 코드가 같이 존재합니다.
정리해서 “Palette에는 색상 이름과 그에 매칭되는 Hex 코드가 들어있구나.” 라고 생각하시면 될 거 같아요. Palette에 대한 설명은 여기까지입니다. 이제 셀러카드 각각의 영역들이 어떻게 자동으로 색상이 변경되는지 설명드릴게요.

Photo by Richard on GC COMPANY Corp
셀러카드에는 각 영역마다 이름을 갖고 있습니다. title, sub_title, rate, distance, address 처럼요.
각 영역에 칠해줄 색상들을 서버에 요청합니다. 갖고 온 데이터를 SellerCard라고 하겠습니다. SellerCard에는 영역에 대한 이름과 색상들이 들어있습니다.
SellerCard 데이터 안에 있는 각 영역들의 이름을 UI 의 영역에 연결해 줍니다.(어떻게 연결하는지는 코드를 통해서 설명하겠습니다.)
연결이 완료되면 코드 수정 없이 서버에 세팅되어 있는 색상만 변경해 주면 자동으로 앱에 적용이 됩니다.
말로는 설명하는 데 한계가 있어서 토이 프로젝트를 통해서 간접적으로나마 여기어때 셀러카드 디자인 시스템을 구현해 보겠습니다.
토이 프로젝트
앱을 처음 실행하면 SplashActivity 가 실행이 됩니다. 해당 액티비티에서 Palette 데이터와 SellerCard 데이터를 서버에서 갖고 옵니다.
데이터들은 싱글턴으로 보관해놓습니다. 토이 프로젝트에선 최대한 심플하게 표현하고자 object를 이용하여 보관하겠습니다.
그다음 MainActivity를 호출하여 리사이클러뷰가 셀러카드를 보여주도록 구현했습니다.
처음부터 차근차근 구현해 볼게요. Palette 데이터와 SellerCard 데이터를 서버에서 가져오겠습니다. 형식은 json입니다.
{
"palette": [
{
"name": "red_5",
"code": "#0DFF0000"
},
{
"name": "red_10",
"code": "#1AFF0000"
},
{
"name": "red_15",
"code": "#26FF0000"
},
{
"name": "red_20",
"code": "#33FF0000"
},
{
"name": "red_25",
"code": "#40FF0000"
}
.
.
.
]
}
Palette는 색상의 name, code가 들어있습니다. code는 Hex 코드입니다.
{
"sellerCard": [
{
"type": "card",
"style": {
"bgColor": "red_5"
}
},
{
"type": "thumbnail",
"style": {
"bgColor": "red_10"
}
},
{
"type": "title",
"style": {
"bgColor": "red_15",
"textColor": "white_100"
}
},
{
"type": "sub_title",
"style": {
"bgColor": "red_20",
"textColor": "white_100"
}
},
{
"type": "rate_image",
"style": {
"imageColor": "red_25"
}
},
{
"type": "rate",
"style": {
"bgColor": "red_30",
"textColor": "white_100"
}
},
{
"type": "distance",
"style": {
"bgColor": "red_35",
"textColor": "white_100"
}
},
{
"type": "address",
"style": {
"bgColor": "red_40",
"textColor": "white_100"
}
}
]
}
SellerCard에는 각 영역들의 type, style 이 들어있습니다. type은 영역을 의미하고 style 은 textColor, imageColor, bgColor 값을 가지고 있습니다. 토이 프로젝트 이므로 3가지 타입만 다루도록 할게요.
이제 json 파일들을 파싱 하기 위해서 data class를 만들어 볼게요.
data class Palette(
@SerializedName("palette") val colors: List<Color>,
)
data class Color(
@SerializedName("name") val name: String, // ex. red_50
@SerializedName("code") val code: String. // ex. #80FF0000
)
Palette 데이터를 담을 data class 입니다. 이제 SellerCard를 만들어 볼게요.
data class SellerCard(
@SerializedName("sellerCard") val spaces: List<Space>,
)
data class Space(
val type: Type,
val style: Style
) {
data class Style(
val textColor: String? = null,
val bgColor: String? = null,
val imageColor: String? = null,
)
enum class Type(val id: String, val defaultStyle: Style) {
@SerializedName("card")
CARD("card", Style(bgColor = "black_5")),
@SerializedName("thumbnail")
THUMBNAIL("thumbnail", Style(bgColor = "black_10")),
@SerializedName("title")
TITLE("title", Style("white_100","black_15")),
@SerializedName("sub_title")
SUB_TITLE("sub_title", Style("white_100", "black_20")),
@SerializedName("rate_image")
RATE_IMAGE("rate_image", Style(imageColor = "black_25")),
@SerializedName("rate")
RATE("", Style("white_100", "black_30")),
@SerializedName("distance")
DISTANCE("distance", Style("white_100", "black_35")),
@SerializedName("address")
ADDRESS("address", Style("white_100", "black_40"));
}
}
SellerCard 데이터를 담을 data class 입니다.
Space는 셀러카드의 영역을 의미합니다. Style은 영역의 색상 값들을 갖고 있고 Type은 영역의 이름과 디폴트 Style을 갖고 있습니다.
디폴트 Style을 갖고 있는 이유는 혹시나 서버에서 데이터가 없는 경우를 대비하기 위한 방어 코드입니다. 이제 json 파싱을 위한 data class는 다 만들었습니다.
SplashActivity에서 해당 데이터들을 받아서 보관하도록 하겠습니다. 그전에 데이터들을 보관할 object를 먼저 볼게요.
object DesignManager {
private var palette: Palette? = null
private var sellerCard: SellerCard? = null
// Palette 데이터 보관
fun initPalette(palette: Palette) {
this.palette = palette
}
// SellerCard 데이터 보관
fun initSellerCard(sellerCard: SellerCard) {
this.sellerCard = sellerCard
}
// type의 style 반환
fun getStyle(type: Space.Type): Style {
return sellerCard?.spaces?.firstOrNull { it.type == type }?.style ?: type.defaultStyle
}
// 색상의 이름을 Hex 코드로 변환 (ex. red_50 -> #80FF0000)
fun getColor(name: String): String {
return palette?.colors?.find { it.name == name }?.code ?: "#000000"
}
}
object 에서는 Palette 데이터와 SellerCard 데이터를 보관하고 있습니다. 이제 SplashActivity를 볼게요.
@SuppressLint("CustomSplashScreen")
class SplashActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val palette: Palette = getPalette()
val sellerCard: SellerCard = getSellerCard()
DesignManager.initPalette(palette)
DesignManager.initSellerCard(sellerCard)
}
}
서버에서 데이터를 갖고 오는 비즈니스 로직은 생략하겠습니다. object에 보관을 하고 MainActivity를 호출합니다.
MainActivity의 리사이클러뷰 어댑터 설정, 네트워크 로직은 생략하겠습니다. 중요한 건 데이터바인딩 이거든요.
이제 object에서 보관하고 있는 데이터들을 연결해 주도록 할게요. 볼드체로 설정해놓은 부분만 보세요! (3개 있어요)
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<import type="com.example.blogproject.Space" />
<variable
name="item"
type="com.example.blogproject.Item" />
</data>
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="10dp"
app:cardElevation="3dp"
app:cardUseCompatPadding="true">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:clipChildren="false"
android:padding="20dp"
app:setStyle="@{Space.Type.CARD}">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/iv_thumbnail"
android:layout_width="match_parent"
android:layout_height="0dp"
android:elevation="3dp"
app:setStyle="@{Space.Type.THUMBNAIL}"
app:srcCompat="@drawable/ic_launcher_foreground"
app:layout_constraintDimensionRatio="5:4"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<com.google.android.material.textview.MaterialTextView
android:id="@+id/tv_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:padding="5dp"
android:elevation="5dp"
android:textSize="24dp"
android:textStyle="bold"
android:text="title"
android:gravity="center"
app:setStyle="@{Space.Type.TITLE}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/iv_thumbnail" />
.
.
.
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
</layout>
중간 코드들은 생략했습니다. 중요한 건 app:setStyle 이 부분인데요. 해당 부분 코드를 살펴볼게요.
@BindingAdapter("setStyle")
fun View.setStyle(type: Space.Type) {
val style = DesignManager.getStyle(type)
when (this) {
is TextView -> {
style.textColor?.let { setTextColor(it.parseColor()) }
style.bgColor?.let { setBackgroundColor(it.parseColor()) }
}
is ViewGroup -> {
style.bgColor?.let { setBackgroundColor(it.parseColor()) }
}
is ImageView -> {
style.imageColor?.let { imageTintList = ColorStateList.valueOf(it.parseColor()) }
style.bgColor?.let { setBackgroundColor(it.parseColor()) }
}
}
}
fun String.parseColor(): Int {
return Color.parseColor(DesignManager.getColor(this))
}
코드를 보면 type에 맞는 style을 object에서 가지고 온 뒤에 각 뷰들의 색상들을 설정해 주고 있습니다. 이렇게 바인딩어댑터를 이용하여 연결해 주면 끝이 납니다.
이제 결과 화면을 볼 건데요. 처음에 보여드렸던 json 파일의 color 값들을 red, blue, green, black, white 계열로 설정했을 때 화면들을 캡처해 봤습니다.

Photo by Richard on GC COMPANY Corp

Photo by Richard on GC COMPANY Corp
이렇게 코드 수정 없이 서버의 color 값들을 변경해 줌으로써 앱에 자동으로 적용되는 디자인 시스템을 갖게 됐습니다.
토이 프로젝트에서 사용한 색상은 textColor, bgColor, imageColor 3개인데요. 각자 상황에 맞게 확장해서 borderColor, underLineColor 등 여러 가지 디자인에 활용할 수 있습니다.
그리고 안드로이드에서 제공하는 기본 View뿐만 아니라 CustomView에 적용하거나 색상 외에 앱에서 자주 사용하는 widget 자체를 서버에서 컨트롤할 수 있게 설계하면 한 층 더 플렉서블한 앱이 될 거 같아요.
여기어때 셀러카드 디자인 시스템에 대한 설명은 여기까지입니다.
감사합니다.