
36기 AT SOPT에서 안드로이드 OB를 맡고 있다. XML 기반 안드로이드 개발을 했던 YB들을 compositionLocal을 활용하여 컴포즈 환경에서 컬러 및 폰트를 세팅하는 방법을 돕기 위해 해당 아티클을 준비했다.
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로 등록해 주면 된다.
폰트의 경우 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을 통해 폰트도 등록해주자.
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와 호환성을 유지하도록 돕는다.
@Composable
fun Greeting() {
Text(
text = "안녕하세요. 36기 안드로이드 파트 OB 박세호입니다.",
style = WithSuhyeonTheme.typography.heading01_B,
color = WithSuhyeonTheme.colors.Purple600
)
}
이제 위에처럼 스타일을 적용하여 전역적으로 컬러와 폰트를 사용하면 된다.