[Android] Jetpack Compose 환경에서 Color, Font 세팅하기 (with CompositionLocal)

박세호·2025년 4월 20일
0

Android

목록 보기
1/3
post-thumbnail

36기 AT SOPT에서 안드로이드 OB를 맡고 있다. XML 기반 안드로이드 개발을 했던 YB들을 compositionLocal을 활용하여 컴포즈 환경에서 컬러 및 폰트를 세팅하는 방법을 돕기 위해 해당 아티클을 준비했다.

1. Color 설정하기 (Color.kt)

xml을 사용해 보신 분들은 컬러를 설정할 때는 아래처럼 작성 했을 것이다.

<resources>
    <color name="purple_200">#FFBB86FC</color>
    <color name="purple_500">#FF6200EE</color>
    <color name="purple_700">#FF3700B3</color>
    <color name="black">#FF000000</color>
    <color name="white">#FFFFFFFF</color>
</resources>

컴포즈에서는 파일 생성할 때 생긴 ui.theme > Color.kt 파일에서 넣어주면 된다.

그러면 아래처럼 바로 사용할 수 있다.

실제 사용을 위한 자세한 설명은 아래와 같다.

우선 디자인 시스템에 따라 Color.kt에 컬러를 세팅한다.

val Purple25 = Color(0xFFFAFAFF)
val Purple50 = Color(0xFFF2F2FF)
val Purple100 = Color(0xFFDCDDFF)
val Purple200 = Color(0xFFC5C6FD)
val Purple300 = Color(0xFF9899F9)
...

이후, 데이터 클래스를 선언한다.

@Immutable
data class WithSuhyeonColors(
    val Purple25: Color,
    val Purple50: Color,
    val Purple100: Color,
    val Purple200: Color,
    val Purple300: Color,
    val Purple400: Color,
    val Purple500: Color,
    val Purple600: Color,
    val Purple700: Color,
		...
) 

여기서 @Immutable을 사용하는 이유는 Jetpack Compose에서는 값이 바뀌면 Recomposition되기 때문이다. 즉, 어떤 값이 바뀌었는지 추적하고, 변경된 값이 있는 경우에 UI를 다시 그린다. 때 Compose는 내부적으로는 equals()를 통해 객체 비교한다. composee는 data class, class가 불변인지, 변경될 수 있는지 알 수 없기에 @Immutable을 명시해줘야 한다.

참고로 Color, Typography처럼 앱 전역에 사용되는 테마 값들은 한 번 설정되면 바뀌지 않는 구조이다.

    @Immutable
    data class WithSuhyeonColors(
        val Purple50: Color,
        val Purple100: Color,
        ...
    )

이렇게 선언하면 Compose는 이 객체는 절대 내부 값이 바뀌지 않는다고 신뢰하게 되어 불필요한 Recomposition 방지하고 성능이 최적화 되고, 디자인 시스템에서 안정성을 확보하며 결국 의도를 명확화할 수 있다는 이점이 있다. 따라서, @Immutable을 명시적으로 붙여주는 것이 가장 안전하고, 효율적인 방법이다.

다시 코드로 돌아오자.

val defaultWithSuhyeonColors = WithSuhyeonColors(
    Purple25 = Purple25,
    Purple50 = Purple50,
    Purple100 = Purple100,
    Purple200 = Purple200,
    ...
)

데이터 클래스를 기반으로 실제 색상 값을 할당한 기본 테마 인스턴스를 만들어주자.

val LocalWithSuhyeonColorsProvider = staticCompositionLocalOf { defaultWithSuhyeonColors }

정의한 Color들을 staticCompositionLocalOf로 등록해 주면 된다.

2. Font 적용하기 (Type.kt)

폰트의 경우 res > New > Android Resource Directory 파일을 열어준다.

이후 font로 이름과 타입을 선택하자.

그리고 아래처럼 폰트까지 추가하면 된다.

보통 디자이너들이 스타일 가이드를 아래와 같이 준다.

이 스타일 가이드를 기반으로 ui.theme > Type.kt 파일에서 폰트를 정의하고, TextStyle에 매핑하면 된다.

val withSuhyeonFontBold = FontFamily(Font(R.font.suit_bold))
val withSuhyeonFontSemiBold = FontFamily(Font(R.font.suit_semibold))
val withSuhyeonFontRegular = FontFamily(Font(R.font.suit_regular))

Jetpack Compose에서는 FontFamily 객체를 활용하여 폰트를 등록한다.

@Immutable
data class WithSuhyeonTypography(
    val heading01_B: TextStyle,
    val heading01_SB: TextStyle,
    val heading01_R: TextStyle,
    ...
)

Color와 마찬가지로 @Immutable 어노테이션을 붙힌 데이터 클래스를 선언한다.


val defaultWithSuhyeonTypography = WithSuhyeonTypography(
    heading01_B = TextStyle(
        fontFamily = withSuhyeonFontBold,
        fontSize = 32.sp,
        fontWeight = FontWeight(700),
        lineHeight = 44.sp
    ),
    heading01_SB = TextStyle(
        fontFamily = withSuhyeonFontSemiBold,
        fontSize = 32.sp,
        fontWeight = FontWeight(600),
        lineHeight = 44.sp
    ),
    heading01_R = TextStyle(
        fontFamily = withSuhyeonFontRegular,
        fontSize = 32.sp,
        fontWeight = FontWeight(400),
        lineHeight = 44.sp
    ),
    ...
)

이제 스타일 가이드에 나와 있는 fontFamily, fontWeight, fontSize, lineHeight 등을 반영하여 TextStyle을 정의하면 된다.

val LocalWithSuhyeonTypographyProvider = **staticCompositionLocalOf** { defaultWithSuhyeonTypography }

마지막으로 staticCompositionLocalOf을 통해 폰트도 등록해주자.

3. Color와 Font를 CompositionLocal에 연결하기 (Theme.kt)

CompositionLocal이란?

앞서 Color와 Font를 세팅할 때, staticCompositionLocalOf를 사용했다.

val LocalWithSuhyeonColorsProvider = staticCompositionLocalOf { defaultWithSuhyeonColors }
val LocalWithSuhyeonTypographyProvider = staticCompositionLocalOf { defaultWithSuhyeonTypography }

이런 CompositionLocal은 Compose에서 전역 상태처럼 데이터를 공유할 수 있는 방법이다. Context, Theme, Resource 등의 데이터를 깊은 컴포저블 트리로 전달할 때 유용하다. Theme처럼 앱 전체에 영향을 주는 값을 쉽게 전달할 수 있고, 인자로 일일이 넘기지 않아도 하위 Composable에서 사용할 수 있어 구조가 간결해져 사용한다.

다시 코드로 돌아와서 우선 아래처럼 object를 선언한다.

object WithSuhyeonTheme {
    val colors: WithSuhyeonColors
        @Composable
        @ReadOnlyComposable
        get() = LocalWithSuhyeonColorsProvider.current

    val typography: WithSuhyeonTypography
        @Composable
        @ReadOnlyComposable
        get() = LocalWithSuhyeonTypographyProvider.current
}

이 객체는 앱 어디서든 색상과 타이포그래피에 접근할 수 있도록 공식적인 진입점 역할을 한다.
@Composable 그리고 @ReadOnlyComposable이 붙은 이유는 컴포지션 중에만 사용되며 상태를 구독하지 않고 읽기 전용으로 사용하기 위함이다.

@Composable
fun ProvideWithSuhyeonColorsAndTypography(
    colors: WithSuhyeonColors,
    typography: WithSuhyeonTypography,
    content: @Composable () -> Unit
) {
    CompositionLocalProvider(
        LocalWithSuhyeonColorsProvider provides colors,
        LocalWithSuhyeonTypographyProvider provides typography,
        content = content
    )
}

이 부분은 컬러와 폰트를 CompositionLocal로 공급하는 함수이다. 이 함수로 감싸진 컴포저블 트리 안에서는 언제든 지정한 color와 font를 사용할 수 있다.


@Composable
fun WithSuhyeonTheme(
    content: @Composable () -> Unit
) {
    ProvideWithSuhyeonColorsAndTypography (
        colors = defaultWithSuhyeonColors,
        typography = defaultWithSuhyeonTypography
    ) {
        val view = LocalView.current
        if (!view.isInEditMode) {
            SideEffect {
                (view.context as Activity).window.run {
                    WindowCompat.getInsetsController(this, view).isAppearanceLightStatusBars = false
                }
            }
        }
        MaterialTheme(
            content = content
        )
    }
}

이 부분은 ProvideWithSuhyeonColorsAndTypography로 지정한 컬러와 폰트를 주입하고, 그 아래에서 Compose의 MaterialTheme를 함께 사용하여 Material3와 호환성을 유지하도록 돕는다.

4. 적용

@Composable
fun Greeting() {
    Text(
        text = "안녕하세요. 36기 안드로이드 파트 OB 박세호입니다.",
        style = WithSuhyeonTheme.typography.heading01_B,
        color = WithSuhyeonTheme.colors.Purple600
    )
}

이제 위에처럼 스타일을 적용하여 전역적으로 컬러와 폰트를 사용하면 된다.

0개의 댓글