Compose Material3 Theme

Arakene·2025년 5월 12일

Material Design이란?

Material Design은 Google이 제공하는 공식 디자인 시스템으로 최신 버전이 Material3이다.

The latest version, Material 3, enables personal, adaptive, and expressive experiences – from dynamic color and enhanced accessibility, to foundations for large screen layouts and design tokens.

안드로이드 12부터는 Material You를 통해서 개인화된 디자인 시스템을 구성할 수 있다고 한다. 특히

  • 동적 색상(Dynamic Color)
    • 사용자의 배경화면에서 주요 색상을 추출해서 색상 테마를 자동으로 적용, Compose에서는 dynamicLightColorScheme()dynamicDarkColorScheme()을 통해 구현 가능

를 통해서 개인화된 테마도 설정이 가능하다고한다.

val colorScheme = when {
    dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
        val context = LocalContext.current
        if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
    }

    darkTheme -> DarkColorScheme
    else -> LightColorScheme
}

MaterialTheme(
    colorScheme = colorScheme,
    typography = Typography,
    content = content
)

이런식으로 구현되어있는데 회사에서 사용하는 디자인 시스템이 있다면 아마 dynamicColor는 사용될 일이 별로 없을 것이라고 생각된다.
만약 디자인 시스템이 없다면 기능의 하나로 추가해도 괜찮은 차별점으로 할 수 있지 않을까? 라는 생각이 든다.

Design Tokens??

Material Design은 디자인 토큰으로 구성되어있다고 한다. 이 토큰을 어떤걸 의미하는거고 어느 이점이 있어 사용하는 걸까?

Design Token이 뭐야?

디자인 시스템에서 작고 재사용 가능한 디자인 요소를 뜻한다. 디자인 토큰의 이름은 자기자신을 잘 설명하도록 설정된다.
2가지로 구성되는데

  • code-like 이름 - md.ref.palette.secondary90
  • 관련된 실제 값 - #E8DEF8

토큰의 값은 색상, textface, 사이즈값, 다른 토큰값 같은걸 가진다.
예시로는 특정 상태나 브랜드 색상등을 들 수 있다.
브랜드 색상은
primary(토큰) - #E8DEF8(토큰이 나타내는 값)
텍스트 스타일
titleLarge(토큰) - TextStyle(fontSize = 16.sp, .. , fontWeight = FontWeight.Bold)(토큰이 나타내는 값)

그럼 왜 사용하니?

  • 디자인과 코드간의 일관성 보장
    primary container color#E8DEF8로 설정한 같은 토큰으로 다음 디자이너가 피그마에 디자인을 하고 개발자가 해당 디자인을 구현하면 해당 디자인은 같은 부분에 같은 색상을 넣었다는 일관성을 유지할 수 있다.
    이후 디자인과 관련된 소통을 할때도 매우매우 편하게 소통이 가능하다.
    나중에 primary container color의 헥사코드가 변경된다 하더라도 일관적으로 한번에 변경이 가능하다.

개인적으로는 디자인 시스템을 적용한 프로젝트가 개발 시 UI 정확도나 이후의 유지보수가 간편했다.

Theme

Theme은 디자인 시스템을 간편하게 도입하고 관리할 수 있도록 도와주는 컴포넌트다. Material3는 안드로이드에서 제공해주는
구성요소로는

  • ColorScheme
  • Shapes
  • Typography

로 이루어져있다.

구조

위 세가지 요소들을 CompositionLocal로 전달해서 사용하게 된다. 내부 코드를 보면 staticCompositionLocalOf를 접근할 수 있는 object로 구성되어있는 것을 확인할 수 있다.

ColorScheme

ColorScheme도 그렇고 다른 구조들도 미리 정의된 역할들이 있다. 해당 역할에 맞는 색상을 지정해두고 사용하는 방식이다. 각 키워드를 이해하면 색상정의하는데 있어 도움이 될거라고 생각한다.

Genaral concepts

  • Surface - 강조 영역에 대한 배경색
  • Primary, Secondary, Tertiary - 강조 수준을 조절하기 위해 사용되는 색상
    • Primary - 앱의 브랜드 색상, 가장 강조되는 항목에 사용
    • Secondary - Primary보다는 우선순위가 낮지만 여전히 중요한 항목에 사용
    • Tertiary - 보조적인 요소에 사용
  • Container - 버튼 색상같이 특정 UI 컴포넌트의 배경색, text or icons에 사용되면 안됨
  • On - onPrimary처럼 on 키워드가 붙으면 뒤에 붙은 역할의 text나 icons의 색상
    예를 들면 onPrimary는 버튼의 배경색을 primary 색상으로 적용된 버튼의 텍스트나 아이콘의 색상이됨
  • Variant - 해당 키워드로 끝나면 앞에 붙은 키워드의 색상과 덜 강조된 색상의 조합으로 다양한 강조가 가능

더 자세한 내용은 material color문서에서 예시를 보며 이해하는 것이 더 도움이 된다.

Shapes

UI의 배경의 Radius를 결정

@Immutable
class Shapes(
    // Shapes None and Full are omitted as None is a RectangleShape and Full is a CircleShape.
    val extraSmall: CornerBasedShape = ShapeDefaults.ExtraSmall,
    val small: CornerBasedShape = ShapeDefaults.Small,
    val medium: CornerBasedShape = ShapeDefaults.Medium,
    val large: CornerBasedShape = ShapeDefaults.Large,
    val extraLarge: CornerBasedShape = ShapeDefaults.ExtraLarge,
)

내부를 들여다보면 각 기준에 맞는 RoundedCornerShape를 제공하고 있다.

Typography

ColorScheme, Shapes와 비슷하게 TextStyle을 사전에 지정해 각 토큰에 맞는 스타일을 설정해서 사용한다.
어떤 폰트를 사용할건지, 폰트의 weight, size와 자간 및 행간의 조정이 가능하다.

DefaultTextStyle.copy(
    fontFamily = TypeScaleTokens.TitleSmallFont, // 폰트 종류
    fontWeight = TypeScaleTokens.TitleSmallWeight, // 폰트 weight
    fontSize = TypeScaleTokens.TitleSmallSize, // 글자 크기
    lineHeight = TypeScaleTokens.TitleSmallLineHeight, // 행간
    letterSpacing = TypeScaleTokens.TitleSmallTracking, // 자간
)

내부 코드를 들여다보면 사전 정의된 기본값이 있는걸로 파악할 수 있다.
사내 디자인 시스템의 설정에 맞춰 커스텀을 사용하면 된다.

@Immutable
class Typography(
    val displayLarge: TextStyle = TypographyTokens.DisplayLarge,
    val displayMedium: TextStyle = TypographyTokens.DisplayMedium,
    val displaySmall: TextStyle = TypographyTokens.DisplaySmall,
    val headlineLarge: TextStyle = TypographyTokens.HeadlineLarge,
    val headlineMedium: TextStyle = TypographyTokens.HeadlineMedium,
    val headlineSmall: TextStyle = TypographyTokens.HeadlineSmall,
    val titleLarge: TextStyle = TypographyTokens.TitleLarge,
    val titleMedium: TextStyle = TypographyTokens.TitleMedium,
    val titleSmall: TextStyle = TypographyTokens.TitleSmall,
    val bodyLarge: TextStyle = TypographyTokens.BodyLarge,
    val bodyMedium: TextStyle = TypographyTokens.BodyMedium,
    val bodySmall: TextStyle = TypographyTokens.BodySmall,
    val labelLarge: TextStyle = TypographyTokens.LabelLarge,
    val labelMedium: TextStyle = TypographyTokens.LabelMedium,
    val labelSmall: TextStyle = TypographyTokens.LabelSmall,
)

느낀점

회사에서 했던 프로젝트에는 디자인시스템의 개발부터 적용까지 담당했는데 Theme을 좀 더 잘 사용했으면 더 쉽고 간편하게 개발 및 유지보수가 가능했을 것이라고 생각한다.
Theme도 결국엔 CompositionLocal을 이용한 기능이니 material이 제공해주는 인터페이스에서 사용하지 않는 값이 많아 복잡성만 늘린다면 직접 관련 클래스와 CompositionLocal을 통해 제공하면 보다 더 관리하기 좋은 디자인 시스템이 될 것이라고 생각한다. 다음 프로젝트에서는 반드시 커스텀을 해보고싶다.

profile
안녕하세요 삽질하는걸 좋아하는 4년차 안드로이드 개발자입니다.

1개의 댓글

comment-user-thumbnail
2025년 5월 14일

오 좋은 글 감사합니다

답글 달기