(Android/Compose) Compose Theme — MaterialTheme, colors, typography


1. Compose Theme이란?

MaterialTheme은 앱 전체에 일관된 디자인(색상, 타이포그래피, 도형)을 적용하는 컨테이너입니다. Composable들이 하드코딩된 색상 대신 테마 값을 참조하게 만들어, 테마 하나만 바꾸면 앱 전체 디자인이 바뀝니다.

MaterialTheme
  ├── colorScheme  (primary, secondary, background, error 등)
  ├── typography   (headlineLarge, bodyMedium, labelSmall 등)
  └── shapes       (small, medium, large — 모서리 둥글기)

2. ColorScheme 정의

Material 3부터는 ColorScheme으로 색상 체계를 관리합니다. 라이트/다크 모드를 각각 정의합니다.

import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.ui.graphics.Color

// 라이트 모드 색상 체계
private val LightColors = lightColorScheme(
    primary = Color(0xFF6750A4),           // 주요 강조색 (버튼, 활성 상태)
    onPrimary = Color(0xFFFFFFFF),         // primary 위에 놓이는 콘텐츠 색 (텍스트/아이콘)
    primaryContainer = Color(0xFFEADDFF),  // primary의 은은한 배경 버전
    secondary = Color(0xFF625B71),         // 보조 강조색
    background = Color(0xFFFFFBFE),        // 화면 배경색
    onBackground = Color(0xFF1C1B1F),      // 배경 위 텍스트 색
    surface = Color(0xFFFFFBFE),           // 카드, 시트 등의 표면색
    onSurface = Color(0xFF1C1B1F),
    error = Color(0xFFB3261E)              // 에러 상태 색상
)

// 다크 모드 색상 체계 — 같은 역할, 다른 값
private val DarkColors = darkColorScheme(
    primary = Color(0xFFD0BCFF),
    onPrimary = Color(0xFF381E72),
    primaryContainer = Color(0xFF4F378B),
    secondary = Color(0xFFCCC2DC),
    background = Color(0xFF1C1B1F),
    onBackground = Color(0xFFE6E1E5),
    surface = Color(0xFF1C1B1F),
    onSurface = Color(0xFFE6E1E5),
    error = Color(0xFFF2B8B5)
)

on{Color} 네이밍 규칙: onPrimary는 “primary 위에 얹는 색”을 의미합니다. 버튼 배경이 primary라면 그 위의 텍스트는 onPrimary를 사용해 항상 대비가 보장됩니다.


3. Typography 정의

텍스트 스타일을 계층별로 정의합니다. Material 3는 크기에 따라 display / headline / title / body / label 5단계로 구성됩니다.

import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp

val AppTypography = Typography(
    // 화면 제목처럼 가장 큰 텍스트
    headlineLarge = TextStyle(
        fontFamily = FontFamily.Default,
        fontWeight = FontWeight.Bold,
        fontSize = 32.sp,
        lineHeight = 40.sp
    ),
    // 섹션 제목
    titleMedium = TextStyle(
        fontFamily = FontFamily.Default,
        fontWeight = FontWeight.SemiBold,
        fontSize = 18.sp,
        lineHeight = 24.sp
    ),
    // 본문 텍스트 (가장 많이 사용)
    bodyMedium = TextStyle(
        fontFamily = FontFamily.Default,
        fontWeight = FontWeight.Normal,
        fontSize = 14.sp,
        lineHeight = 20.sp
    ),
    // 캡션, 버튼 텍스트처럼 작은 텍스트
    labelSmall = TextStyle(
        fontFamily = FontFamily.Default,
        fontWeight = FontWeight.Medium,
        fontSize = 11.sp,
        lineHeight = 16.sp
    )
)

4. 앱 테마 조합하기

ColorScheme, Typography, Shapes를 하나의 테마 Composable로 묶습니다.

import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.Shapes
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable

private val AppShapes = Shapes(
    small = RoundedCornerShape(4.dp),   // 칩, 작은 버튼
    medium = RoundedCornerShape(8.dp),  // 카드
    large = RoundedCornerShape(16.dp)   // 바텀시트, 다이얼로그
)

@Composable
fun MyAppTheme(
    // isSystemInDarkTheme(): 기기의 다크 모드 설정을 자동으로 감지
    darkTheme: Boolean = isSystemInDarkTheme(),
    content: @Composable () -> Unit
) {
    // 조건에 따라 라이트/다크 색상 체계 선택
    val colorScheme = if (darkTheme) DarkColors else LightColors

    MaterialTheme(
        colorScheme = colorScheme,
        typography = AppTypography,
        shapes = AppShapes,
        content = content  // 이 테마가 적용될 하위 Composable 트리
    )
}

// 앱 최상단에서 감싸기
@Composable
fun App() {
    MyAppTheme {
        // 이 블록 안의 모든 Composable이 테마 적용받음
        MainScreen()
    }
}

5. 테마 값 사용하기

하드코딩 대신 MaterialTheme 객체를 통해 값을 참조합니다.

@Composable
fun ThemedCard() {
    Card(
        // ❌ 하드코딩: Color(0xFF6750A4) → 다크모드 대응 안 됨
        // ✅ 테마 참조: 다크모드에서 자동으로 올바른 색 적용
        colors = CardDefaults.cardColors(
            containerColor = MaterialTheme.colorScheme.primaryContainer
        ),
        shape = MaterialTheme.shapes.medium  // 테마의 medium 모양(8dp 둥근 모서리)
    ) {
        Column(modifier = Modifier.padding(16.dp)) {
            Text(
                text = "제목",
                style = MaterialTheme.typography.titleMedium,  // 테마 타이포그래피
                color = MaterialTheme.colorScheme.onPrimaryContainer
            )
            Text(
                text = "본문 내용입니다.",
                style = MaterialTheme.typography.bodyMedium,
                color = MaterialTheme.colorScheme.onSurfaceVariant
            )
        }
    }
}

핵심 원칙: Composable 내부에서 Color(0xFF...)를 직접 쓰지 않고 항상 MaterialTheme.colorScheme.xxx를 참조하면, 다크 모드나 브랜드 리뉴얼 시 테마 정의만 바꾸면 앱 전체가 갱신됩니다.


6. 다이나믹 컬러 (Android 12+)

기기 배경화면에서 추출한 색상을 앱 테마에 자동 적용하는 기능입니다.

import android.os.Build
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.ui.platform.LocalContext

@Composable
fun MyAppTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    dynamicColor: Boolean = true,  // 다이나믹 컬러 사용 여부
    content: @Composable () -> Unit
) {
    val colorScheme = when {
        // Android 12(API 31) 이상 + 다이나믹 컬러 옵션 켜짐
        dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
            val context = LocalContext.current
            if (darkTheme) dynamicDarkColorScheme(context)
            else dynamicLightColorScheme(context)
        }
        // 그 외에는 직접 정의한 색상 체계 사용
        darkTheme -> DarkColors
        else -> LightColors
    }

    MaterialTheme(colorScheme = colorScheme, typography = AppTypography, content = content)
}

7. CompositionLocal — 커스텀 테마 값 전파

Material에 없는 커스텀 값(간격, 커스텀 색상 등)을 트리 전체에 전파하고 싶을 때 사용합니다.

import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.CompositionLocalProvider

// 앱 전용 간격(spacing) 값
data class AppSpacing(val small: Dp = 4.dp, val medium: Dp = 16.dp, val large: Dp = 32.dp)

// compositionLocalOf: 값이 없을 때 기본값 제공, 하위 트리 어디서든 조회 가능
val LocalAppSpacing = compositionLocalOf { AppSpacing() }

@Composable
fun MyAppTheme(content: @Composable () -> Unit) {
    // CompositionLocalProvider: 하위 트리에 커스텀 값 주입
    CompositionLocalProvider(LocalAppSpacing provides AppSpacing()) {
        MaterialTheme(colorScheme = LightColors, content = content)
    }
}

// 사용: props로 전달받지 않고도 어디서든 조회 가능
@Composable
fun SomeDeepChildComposable() {
    val spacing = LocalAppSpacing.current
    Box(modifier = Modifier.padding(spacing.medium)) {
        Text("커스텀 간격 적용")
    }
}

8. 정리

구성 요소 역할 접근 방법
ColorScheme 색상 체계 (라이트/다크) MaterialTheme.colorScheme
Typography 텍스트 스타일 계층 MaterialTheme.typography
Shapes 모서리 둥글기 MaterialTheme.shapes
CompositionLocal 커스텀 테마 값 전파 LocalXxx.current


Related Posts