
서론
디자이너와 협업 시 가장 중요한 것은 디자이너가 의도한 디자인을 정확하게 구현하는 것이다. 특정 텍스트가 어떤 폰트를 사용해야 하는지, 글자 크기는 얼마인지, 자간과 행간은 어떻게 적용되어야 하는지까지 세세하게 정해져 있다.
같은 스타일의 텍스트를 구현하면서도 매번 폰트나 크기, 행간이나 자간을 다시 설정해야 하고 화면이 많아질수록 이러한 작업은 반복되어 개발 생산성이 점점 저해된다. 이러한 불편함을 줄이기 위해 디자이너가 설계한 디자인을 기준으로 재사용할 수 있는 시스템을 만드는 것이 바로 디자인 시스템이다.
디자인 시스템을 도입하면 디자이너의 요구사항을 일관된 방식으로 반영할 수 있고 이미 정의된 시스템을 활용해 보다 안정적으로 화면을 구현할 수 있다. 오늘은 실제 프로젝트에서 사용한 디자인 시스템을 기반으로 디자인 시스템을 설계하는 법에 대해 소개하겠다.
색상 시스템 정의
디자이너가 요구한 색상을 Compose의 theme/Colors 패키지에 정의해준다. 다음은 우리 팀에서 사용하는 색상의 일부다.

package com.twix.designsystem.theme
import androidx.compose.ui.graphics.Color
object GrayColor {
val C050 = Color(0xFFF4F4F4)
val C100 = Color(0xFFECECEC)
val C200 = Color(0xFFC6C6C6)
val C300 = Color(0xFF858585)
val C400 = Color(0xFF464646)
val C500 = Color(0xFF171717)
}
색상 값을 화면마다 직접 사용하는 대신 디자이너가 정의한 이름으로 정의된 색상을 사용함으로써 디자인 변경에 유연하게 대응할 수 있다. 만약 특정 회색 계열의 색상이 수정되더라도 해당 색상 객체만 변경하면 모든 화면에 동일하게 반영된다. 또한 디자이너와의 커뮤니케이션에서도 “이 화면에는 Gray 500을 사용했다”처럼 같은 용어를 사용할 수 있어 협업이 훨씬 수월하다.
Typography 디자인 시스템 설계
텍스트는 제목, 본문, 설명 등 다양한 역할을 가지며 화면마다 반복적으로 사용된다. 그만큼 일관된 규칙 없이 관리하기 어려운 요소다. Typography 디자인 시스템의 핵심은 텍스트를 값이 아닌 역할 기준으로 정의하는 것이다. 다음은 우리 앱의 Typography 가이드 중 일부다.

이를 일관된 형태로 사용할 수 있도록 enum 형태로 정의한다. 이 enum은 텍스트의 크기나 굵기와 같은 구체적인 값이 아니라 해당 텍스트가 어떤 역할을 가지는지를 나타내며 디자이너가 정의한 변수명을 그대로 사용함으로써 수월한 의사소통을 가능하게 한다.
enum class AppTextStyle {
H1, H2, H3, H4,
T1, T2, T3,
B1, B2, B3, B4,
C1, C2
}
AppTypography 설계
텍스트 스타일을 역할 기준으로 정의했다면 이제 각 역할에 실제로 어떤 스타일을 적용할지 정리해야 한다. 이를 위해 모든 텍스트 스타일을 하나로 묶은 AppTypography를 만든다.
@Immutable
data class AppTypography(
val h1: TextStyle,
val h2: TextStyle,
val h3: TextStyle,
val h4: TextStyle,
val t1: TextStyle,
val t2: TextStyle,
val t3: TextStyle,
val b1: TextStyle,
val b2: TextStyle,
val b3: TextStyle,
val b4: TextStyle,
val c1: TextStyle,
val c2: TextStyle,
)
AppTypography는 앱에서 사용하는 모든 텍스트 스타일을 한 곳에서 관리하기 위한 데이터 클래스다. 제목, 본문, 캡션 등 각 역할에 대응하는 TextStyle을 속성으로 가지고 있으며 이를 통해 타이포그래피 규칙을 일관되게 유지할 수 있다.
또한 텍스트 스타일과 관련된 모든 설정이 한 곳에 모여 있기 때문에 디자인 변경이 발생했을 때 특정 화면을 직접 수정할 필요 없이 AppTypography만 수정하면 된다.
Font Family 정의
다음으로 앱 전반에서 사용할 폰트를 정의해주어야 한다. theme/Type 패키지에 Font Family를 정의함으로써 타이포그래피에서 사용할 폰트를 한 곳에서 관리할 수 있도록 했다.
package com.twix.designsystem.theme
val LaundryGothicFamily =
FontFamily(
Font(R.font.laundry_gothic_regular, FontWeight.Normal),
Font(R.font.laundry_gothic_bold, FontWeight.Bold),
)
val NanumSquareNeoFamily =
FontFamily(
Font(R.font.nanum_square_neo_light, FontWeight.Light),
Font(R.font.nanum_square_neo_regular, FontWeight.Normal),
Font(R.font.nanum_square_neo_bold, FontWeight.Bold),
Font(R.font.nanum_square_neo_extra_bold, FontWeight.ExtraBold),
)
이처럼 Font Family를 미리 정의해두면 이후 타이포그래피 스타일에서 어떤 폰트를 사용할지 일관되게 관리할 수 있다. 또한 폰트 변경이 필요해질 경우에도 한 곳만 수정하면 전체 화면에 적용할 수 있다는 장점이 있다.
디자인 스펙을 그대로 코드로 구현

각 텍스트 스타일은 디자이너가 정의한 디자인 스펙을 기준으로 작성했다. 폰트 종류, 굵기, 글자 크기뿐만 아니라 행간(line height)과 자간(letter spacing)까지 모두 정의했다.
fun provideAppTypography(): AppTypography =
AppTypography(
h1 =
TextStyle(
fontFamily = NanumSquareNeoFamily,
fontWeight = FontWeight.Bold,
fontSize = 28.sp,
lineHeight = lineHeightPercent(28f, 140f),
letterSpacing = letterSpacingPercent(28f, -1f),
),
h2 =
TextStyle(
fontFamily = NanumSquareNeoFamily,
fontWeight = FontWeight.Normal,
fontSize = 24.sp,
lineHeight = lineHeightPercent(24f, 140f),
letterSpacing = letterSpacingPercent(24f, -1f),
),
//...
)
행간과 자간은 디자이너가 퍼센트 단위로 전달해주는 경우가 많아 이를 그대로 반영하기 위해 퍼센트 기반으로 계산하는 함수로 분리했다. 이 방식의 장점은 디자인 스펙을 직관적으로 코드로 확인할 수 있다는 점이다.
/**
* 행간 계산하는 메서드
* */
private fun lineHeightPercent(
fontSizeSp: Float,
percent: Float,
): TextUnit = (fontSizeSp * (percent / 100f)).sp
/**
* 자간 계산하는 메서드
* */
private fun letterSpacingPercent(
fontSizeSp: Float,
percent: Float,
): TextUnit = (fontSizeSp * (percent / 100f)).sp
AppTextStyle과 AppTypography 연결
앞서 정의한 AppTextStyle enum은 텍스트의 역할만 표현한다. 실제 TextStyle과 연결하기 위해 AppTextStyle을 TextStyle로 변환하는 매핑 함수를 추가했다.
fun AppTextStyle.toTextStyle(typography: AppTypography): TextStyle =
when (this) {
AppTextStyle.H1 -> typography.h1
AppTextStyle.H2 -> typography.h2
AppTextStyle.H3 -> typography.h3
AppTextStyle.H4 -> typography.h4
AppTextStyle.T1 -> typography.t1
AppTextStyle.T2 -> typography.t2
AppTextStyle.T3 -> typography.t3
AppTextStyle.B1 -> typography.b1
AppTextStyle.B2 -> typography.b2
AppTextStyle.B3 -> typography.b3
AppTextStyle.B4 -> typography.b4
AppTextStyle.C1 -> typography.c1
AppTextStyle.C2 -> typography.c2
}
이 매핑을 통해 UI에서는 AppTextStyle만 사용하면 되고 실제 스타일 값은 디자인 시스템 내부에서 자동으로 결정되어 결과적으로 화면 구현 코드에서 디자인과 관련된 세부 설정을 분리할 수 있다.
AppTypography를 전역으로 제공
AppTypography를 앱 전반에서 사용할 수 있도록 CompositionLocal을 사용해 전역으로 제공했다. 그리고 Theme에서 AppTypography를 한 번만 생성해 모든 Composable에 전달했다.
val LocalAppTypography =
staticCompositionLocalOf<AppTypography> {
error("AppTypography가 제공되지 않음")
}
@Composable
fun TwixTheme(content: @Composable () -> Unit) {
val typography = remember { provideAppTypography() }
CompositionLocalProvider(
LocalAppTypography provides typography,
) {
content()
}
}
이 구조를 사용하면 어떤 Composable에서도 동일한 타이포그래피 규칙을 사용할 수 있으며 디자인 시스템이 앱 전반에 자연스럽게 적용된다.
공용 Text Composable 구현
마지막으로 프로젝트 전역에서 앞서 설정한 디자인을 적용하는 Text Composable을 적용한다. 이를 통해 텍스트가 필요한 화면에서 원하는 style과 색상만 전달하면 앞서 구축한 디자인 시스템을 통해 일관된 디자인을 적용할 수 있다.
@Composable
fun AppText(
text: String,
style: AppTextStyle,
color: Color,
modifier: Modifier = Modifier,
textAlign: TextAlign? = null,
maxLines: Int = Int.MAX_VALUE,
overflow: TextOverflow = TextOverflow.Clip,
) {
val typo = LocalAppTypography.current
val baseStyle = style.toTextStyle(typo)
CompositionLocalProvider(LocalDensity provides Density(LocalDensity.current.density, fontScale = 1f)) {
Text(
modifier = modifier,
text = text,
style = baseStyle,
color = color,
textAlign = textAlign,
maxLines = maxLines,
overflow = overflow,
)
}
}
// 사용하는 곳 에서...
AppText(
text = "페토",
style = AppTextStyle.T3,
color = GrayColor.C400,
)
참조
'Android' 카테고리의 다른 글
| HiltViewModel 의존성 주입 원리 (0) | 2025.11.06 |
|---|---|
| 아주 쉽게 알아보는 뷰가 그려지기까지의 여정 (0) | 2025.10.29 |
| 운영체제 메모리 (0) | 2025.10.14 |
| 안드로이드에서 네트워크 상태에 따라 API를 재호출해보자 (0) | 2025.09.27 |
| Retrofit Internals - Retrofit In Coroutine (0) | 2025.06.20 |
