Compose의 Material Theme를 적용하여 앱 구성하기

SSY·2024년 6월 8일
0

Compose

목록 보기
6/10
post-thumbnail

시작하며

앱이 커짐에 따라 디자인 시스템의 필요성을 느끼게 되었다. 흔히 Toss Design System이란 말을 많이 들었는데, 이 부분이 생각의 출발점이었다. Design에 왜 System이란 말을 붙였을까? 그것은 아무래도 Design을 체계적으로 구축 및 System화하고 개발자는 기존 구축된 시스템을 단순히 사용함으로써 비즈니스 로직에 더욱 집중할 수 있기 때문이 아닐까 한다.

이에 맞춰 Compose를 사용하다보면 MatherialTheme()라는 메서드를 볼 수 있다. 해당 메서드는 하위에 ColorScheme, Shapes, Typography를 주입받을 수 있게 설계되어 있는걸 볼 수 있다.

package androidx.compose.material3

@Suppress("DEPRECATION_ERROR")
@Composable
fun MaterialTheme(
    colorScheme: ColorScheme = MaterialTheme.colorScheme,
    shapes: Shapes = MaterialTheme.shapes,
    typography: Typography = MaterialTheme.typography,
    content: @Composable () -> Unit
)

처음에는 이게 필요한 이유를 잘 몰랐지만, Design System이란 무엇인걸까 란 생각을 무의식적으로 계속 하다보니 이들을 통해 앱의 디자인을 일관적으로 꾸밀 수 있는 유용한 툴이란 것을 알게되었다. 이런 툴들은 아래와 같은 참조 방식으로 사용되곤 한다.

val materialShape = MaterialTheme.shapes
val materialColorScheme = MaterialTheme.colorScheme
val materialTypo = MaterialTheme.typography

또한 이들을 각각 참조하려할 때, MaterialTheme에 주입한 각각의 객체 상태를 조회할 수 있다.

[Shapes 참조]

[ColorShapes 참조]

[Typography 참조]

또한 Google사가 제공하는 권장 아키텍처 앱인 Now In Android에서도 MaterialTheme객체에 아래와 같은 값을 주입시킴으로써 Design System을 적극 활용하고 있단것도 알 수 있다.

@Composable
fun NiaTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    androidTheme: Boolean = false,
    disableDynamicTheming: Boolean = true,
    content: @Composable () -> Unit,
) {
    // Color scheme
    val colorScheme = when {
        androidTheme -> if (darkTheme) DarkAndroidColorScheme else LightAndroidColorScheme
        !disableDynamicTheming && supportsDynamicTheming() -> {
            val context = LocalContext.current
            if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
        }

        else -> if (darkTheme) DarkDefaultColorScheme else LightDefaultColorScheme
    }
    // Gradient colors
    val emptyGradientColors = GradientColors(container = colorScheme.surfaceColorAtElevation(2.dp))
    val defaultGradientColors = GradientColors(
        top = colorScheme.inverseOnSurface,
        bottom = colorScheme.primaryContainer,
        container = colorScheme.surface,
    )
    val gradientColors = when {
        androidTheme -> if (darkTheme) DarkAndroidGradientColors else LightAndroidGradientColors
        !disableDynamicTheming && supportsDynamicTheming() -> emptyGradientColors
        else -> defaultGradientColors
    }
    // Background theme
    val defaultBackgroundTheme = BackgroundTheme(
        color = colorScheme.surface,
        tonalElevation = 2.dp,
    )
    val backgroundTheme = when {
        androidTheme -> if (darkTheme) DarkAndroidBackgroundTheme else LightAndroidBackgroundTheme
        else -> defaultBackgroundTheme
    }
    val tintTheme = when {
        androidTheme -> TintTheme()
        !disableDynamicTheming && supportsDynamicTheming() -> TintTheme(colorScheme.primary)
        else -> TintTheme()
    }
    // Composition locals
    CompositionLocalProvider(
        LocalGradientColors provides gradientColors,
        LocalBackgroundTheme provides backgroundTheme,
        LocalTintTheme provides tintTheme,
    ) {
        MaterialTheme(
            colorScheme = colorScheme,
            typography = NiaTypography,
            content = content,
        )
    }
}

그리고 이에 잘 맞게도, Now In Android에서는 동적 테마 변경 기능까지 제공한다. 해당 기능을 통해 3가지의 테마를 바꿀 수 있다. 그렇다면 이제 MaterialTheme객체의 상태값을 설정하고자 할 때, 각각의 프로퍼티들이 어떤 의미가 있는지만 알면 된다. 그렇다면 앱 구축 시, Design System을 구축하고 그에 맞게 잘 활용할 수 있을 것이다.

추가로 Material Theme Builder를 사용해보면 알겠지만, Design System이 적용된 앱이 참 예쁘게 느껴지기도 한다.

ColorScheme

참고 : https://m3.material.io/styles/color/roles

ColorScheme내부에 정의된 프로퍼티들이 어떤 역할을 하는지 알기 위한 좋은 방법은
1. 안드로이드 공식 홈페이지
2. Material Design공식 홈페이지

가 있다. 해당 자료를 통해 Colorscheme 프로퍼티의 역할을 이해할 수 있다. 또한 색상을 보다보면 특정 키워드들(ex. Surface, Primary, Secondary, Tertiary, Container, On, Variant)이 있다. 우선적으로 이 키워드들의 의미를 이해하면 ColorScheme를 좀 더 쉽게 이해할 수 있을거라 본다. (참고 : Material Color General Concepts)

  • Surface : 배경의 크고 약한 강조를 위해 사용되는 색상이다.
  • Priamry : 강한 강조가 필요할 때 또는 앱 전체에서 가장 빈번하게 사용되는 컴포넌트에 사용되는 색상이다. (ex. FloatingActionButton 또는 Button 또는 활성 상태)
  • Secondary : Primary색깔보다 덜 강조하려할 때 사용되는 색상이다. (ex. filter chip)
  • Tertiary : PrimarySecondary색상의 대비 강조 효과를 주고자할 때 사용되는 색상이다. 또는 InputField에도 쓰일 수 있다
  • Container : 버튼 등과 같이 내부 UI요소들을 채우기 위한 색상으로 사용된다. 이들은 텍스트 또는 아이콘 색상으로 사용되면 안된다.
  • On : OnPrimary, OnSecondary등과 같이 특정 색상의 접두사로, 해당 접두사의 부모 색상들(Primary, Secondary..)들 위쪽에 사용되는 아이콘과 텍스트 색상이다.
  • Variant : OnSurfaceVariant, OutlineVariant등과 같이 특정 색상의 접미사로, 해당 접미사의 부모 색상들(OnSurface, OutlineVariant)과 대비하여 약한 강조를 할때 사용되는 색상이다.

Typography

앱 내에선 글자에 다양한 크기, 폰트, 강조 효과 등을 적용한다. 이를 가능하게 하는게 바로 Typography이다. MatherialTheme에선 아래와 같이 글자 크기를 설정할 수 있다.

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,
)

위와 같이 각각의 경우에 해당하는 TextStyle을 정의하고 꺼내쓰면 된다. 하지만 디자인 시스템이 체계화되어있지 않다면 이를 사용하는게 현실적으로 가능할까란 생각이 들기도 한다.

다만, Now In Android에선 이를 사용하고 있다. 아무래도 구글팀이고, 안드로이드 권장 아키텍처 샘플앱인만큼 작업이 되어있는게 아닐까 한다.

[Google사, Now In Android]

internal val NiaTypography = Typography(
    displayLarge = TextStyle(
        fontWeight = FontWeight.Normal,
        fontSize = 57.sp,
        lineHeight = 64.sp,
        letterSpacing = (-0.25).sp,
    ),
    displayMedium = TextStyle(
        fontWeight = FontWeight.Normal,
        fontSize = 45.sp,
        lineHeight = 52.sp,
        letterSpacing = 0.sp,
    ),
    displaySmall = TextStyle(
        fontWeight = FontWeight.Normal,
        fontSize = 36.sp,
        lineHeight = 44.sp,
        letterSpacing = 0.sp,
    ),
    headlineLarge = TextStyle(
        fontWeight = FontWeight.Normal,
        fontSize = 32.sp,
        lineHeight = 40.sp,
        letterSpacing = 0.sp,
    ),
    headlineMedium = TextStyle(
        fontWeight = FontWeight.Normal,
        fontSize = 28.sp,
        lineHeight = 36.sp,
        letterSpacing = 0.sp,
    ),
    headlineSmall = TextStyle(
        fontWeight = FontWeight.Normal,
        fontSize = 24.sp,
        lineHeight = 32.sp,
        letterSpacing = 0.sp,
        lineHeightStyle = LineHeightStyle(
            alignment = Alignment.Bottom,
            trim = Trim.None,
        ),
    ),
    titleLarge = TextStyle(
        fontWeight = FontWeight.Bold,
        fontSize = 22.sp,
        lineHeight = 28.sp,
        letterSpacing = 0.sp,
        lineHeightStyle = LineHeightStyle(
            alignment = Alignment.Bottom,
            trim = Trim.LastLineBottom,
        ),
    ),
    titleMedium = TextStyle(
        fontWeight = FontWeight.Bold,
        fontSize = 18.sp,
        lineHeight = 24.sp,
        letterSpacing = 0.1.sp,
    ),
    titleSmall = TextStyle(
        fontWeight = FontWeight.Medium,
        fontSize = 14.sp,
        lineHeight = 20.sp,
        letterSpacing = 0.1.sp,
    ),
    // Default text style
    bodyLarge = TextStyle(
        fontWeight = FontWeight.Normal,
        fontSize = 16.sp,
        lineHeight = 24.sp,
        letterSpacing = 0.5.sp,
        lineHeightStyle = LineHeightStyle(
            alignment = Alignment.Center,
            trim = Trim.None,
        ),
    ),
    bodyMedium = TextStyle(
        fontWeight = FontWeight.Normal,
        fontSize = 14.sp,
        lineHeight = 20.sp,
        letterSpacing = 0.25.sp,
    ),
    bodySmall = TextStyle(
        fontWeight = FontWeight.Normal,
        fontSize = 12.sp,
        lineHeight = 16.sp,
        letterSpacing = 0.4.sp,
    ),
    // Used for Button
    labelLarge = TextStyle(
        fontWeight = FontWeight.Medium,
        fontSize = 14.sp,
        lineHeight = 20.sp,
        letterSpacing = 0.1.sp,
        lineHeightStyle = LineHeightStyle(
            alignment = Alignment.Center,
            trim = Trim.LastLineBottom,
        ),
    ),
    // Used for Navigation items
    labelMedium = TextStyle(
        fontWeight = FontWeight.Medium,
        fontSize = 12.sp,
        lineHeight = 16.sp,
        letterSpacing = 0.5.sp,
        lineHeightStyle = LineHeightStyle(
            alignment = Alignment.Center,
            trim = Trim.LastLineBottom,
        ),
    ),
    // Used for Tag
    labelSmall = TextStyle(
        fontWeight = FontWeight.Medium,
        fontSize = 10.sp,
        lineHeight = 14.sp,
        letterSpacing = 0.sp,
        lineHeightStyle = LineHeightStyle(
            alignment = Alignment.Center,
            trim = Trim.LastLineBottom,
        ),
    ),
)

Shape

앱 내에서 각종 UI Component에 여러 모양새를 넣을 수 있다. 예를 들면, 꼭지점 부분을 둥글게 한다던지, 아니면 아예 동그랗게 만들다든지, 테두리를 적용한다든지 등이 있다. 이러한 부분을 상위 MatherialTheme에 적용하고 공통적으로 꺼내쓰고자할 때 사용한다. 위와 비슷한 부분이 많기에 자세한 설명은 생략한다.

마치며

Material Design System이란게 생각보다 복잡하단 것을 알게되었다. 그러기 위해선 함께 협업하는 디자이너분의 실력이나 책임감이 필요하며, 그에 맞게 Material Design System에 맞게 디자인을 설계해주실 필요가 있어보인다.

개발자가 스스로 이를 적용하기엔 단순 'Material Builder'같은 곳에서 다운받아와 적용하는게 최선이지 않을까 생각한다.

현재 진행하는 사이드 프로젝트가 있는데, Design System이 적용되어 있지 않을까하고 해당 주제로 약간의 공부를 해보았다. 근데 크게 적용되어 있는지는 모르겠고, 단순 내가 적용할 수 있는 부분이라도 조금 찾아서 진행하는게 나아보인다.

Toss에는 TDS라는 디자인 시스템이 자체적으로 존재한다. 그러다보니 이들의 디자인 시스템은 얼마나 고도화되어있고 체계적일지 궁금하기도 하다. 아무튼 오늘 공부 끝.

profile
불가능보다 가능함에 몰입할 수 있는 개발자가 되기 위해 노력합니다.

0개의 댓글