안드로이드 프레임워크·시스템 학습 노트 목차

Compose Material 3·테마 — 색·타이포·다크모드·디자인 시스템

왜 디자인을 한 곳에 모으나

화면마다 색을 직접 Color(0xFF1565C0)로, 글꼴 크기를 16.sp 박으면 — 처음엔 빨라도 곧 지옥이 된다. 브랜드 색을 살짝 바꾸려면 수백 곳을 찾아 고쳐야 하고, 다크모드를 켜면 하드코딩한 색이 어두운 배경에서 안 보여 깨지고, 화면마다 글꼴·여백이 미묘하게 달라져 일관성이 무너진다. 그래서 디자인 토큰(색·글꼴·모양)을 한 곳에 정의하고, 모든 화면이 그것을 참조하게 만들어야 한다. Compose는 **Material 3(Material You)**를 기본 디자인 시스템으로 주고, **MaterialTheme**으로 이 토큰을 앱 전역에 주입한다 — *다크모드·동적 색상(Android 12+)*까지 여기서 해결된다. (출처: Android — Material Design 3 in Compose / Theming.)

MaterialTheme — 색·타이포·셰이프의 단일 출처

MaterialTheme을 앱 루트에 한 번 감싸면 — 하위의 모든 컴포저블이 MaterialTheme.colorScheme·typography·shapes를 어디서나 참조한다. 이게 가능한 건 MaterialThemeCompositionLocal([18])로 값을 트리 전체에 암묵 전파하기 때문이다(매 컴포저블에 색을 인자로 넘길 필요가 없다).

@Composable
fun AppTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) {
    val colors = if (darkTheme) darkColorScheme() else lightColorScheme()  // 한 곳에서 light/dark 선택
    MaterialTheme(colorScheme = colors, typography = AppTypography, shapes = AppShapes) {
        content()                                                          // 하위 전체가 이 테마를 따름
    }
}

// 하위 어디서나 — 색·글꼴을 '테마에서' 가져온다 (직접 박지 않는다)
Text("제목", color = MaterialTheme.colorScheme.primary,
     style = MaterialTheme.typography.titleLarge)
다이어그램 로딩 중…

색을 "역할"로 쓴다 — 다크모드가 공짜가 되는 이유

Material 3 색의 핵심 발상은 — 색을 "파랑·빨강" 같은 값이 아니라 "역할(role)"로 쓰는 것이다. primary(주요 동작), surface(표면/배경), onPrimary(primary 위에 올라갈 콘텐츠 색), error(오류) 등 의미로 지정한다. 왜 이게 중요한가? — 역할로 쓰면 다크모드 전환이 자동이 된다. 라이트에서 surface가 흰색, 다크에서 surface가 짙은 회색으로 ColorScheme만 바뀌면, 컴포넌트 코드는 한 줄도 안 건드리고 모든 화면이 다크모드로 따라온다. 반대로 Color.White를 하드코딩하면 다크모드 배경에서 흰 글자가 흰 카드 위에 떠 안 보이게 깨진다. 그래서 색은 항상 MaterialTheme.colorScheme.*로, 절대 리터럴로 박지 않는다.

on* 짝(primaryonPrimary)도 같은 원리다 — 어떤 표면 위에 올라가든 그 위의 콘텐츠가 항상 읽히게 명암 대비를 보장하는 색 쌍이다.

다크모드·동적 색상

  • 다크 테마isSystemInDarkTheme()시스템 설정을 읽어 light/dark ColorScheme를 고른다. 색을 역할로 써 뒀으니 스킴만 바꾸면 끝이다.
  • 동적 색상(dynamic color, Android 12+)사용자의 배경화면에서 추출한 색으로 테마를 생성한다(dynamicLightColorScheme(context)). 사용자의 폰마다 앱 색이 개인화되는 Material You의 핵심이다(기기가 지원할 때만 켠다).

컴포넌트 — 만들어진 블록

Material 3는 테마를 자동으로 따르는 완성 컴포넌트를 준다 — Button·Card·TextField·TopAppBar·NavigationBar·Scaffold·ModalBottomSheet 등. 직접 색·모양을 지정하지 않아도 현재 테마(라이트/다크/동적)에 맞춰 그려져 일관성이 공짜다.

Card { Column(Modifier.padding(16.dp)) { Text("카드") } }   // 테마의 surface 색·모서리 자동 적용
Button(onClick = {}) { Text("확인") }                        // primary 색 자동
FilledTonalButton(onClick = {}) { Text("보조") }             // 보조 강조용 변형

자체 디자인 시스템·Edge-to-edge

브랜드 고유의 토큰(고유 간격·보조 색)이 필요하면 — 커스텀 객체 + CompositionLocalMaterialTheme을 감싸 확장한다([18]). 그러면 MyTheme.spacing.large처럼 내 디자인 토큰도 트리 전체에서 쓸 수 있다. 그리고 현대 앱은 edge-to-edge시스템 바(상태바·내비바) 뒤까지 콘텐츠를 그리고(enableEdgeToEdge()), WindowInsets그 바가 가리는 영역만큼 패딩해 화면을 꽉 채운다.

정리 — 한 곳에서 정의하고 역할로 쓴다

Compose 테마의 핵심 — MaterialTheme에 색·타이포·셰이프를 정의해 전역(CompositionLocal) 주입, 색은 의미 역할(primary·surface·on)로 써서 다크모드·테마 교체에 자동 대응*(하드코딩 색은 다크에서 깨진다), isSystemInDarkTheme/동적 색상으로 전환, Material 3 컴포넌트가 테마를 자동으로 따름, 고유 토큰은 CompositionLocal로 확장. 디자인을 한 곳에 모으고 역할로 참조하면 일관성과 다크모드가 따라온다.

한 줄 요약MaterialTheme(colorScheme/typography/shapes)를 루트에→하위가 MaterialTheme.colorScheme.primary 등 참조(CompositionLocal 전파). 색은 역할(primary/surface/on*)로(리터럴 금지→다크모드 자동·on*은 명암 보장). isSystemInDarkTheme()·동적 색상(Android 12+). Material3 컴포넌트(Button/Card/Scaffold)가 테마 자동 적용·edge-to-edge+WindowInsets. 고유 토큰은 CompositionLocal로 확장.

(출처: Android — Material 3 in Compose / Theming / Dynamic color / Material Design 3.)

Compose 레이아웃·Modifier — Column·Row·Box·체이닝Compose 리스트 — LazyColumn·key·성능