스타일 사용 시 권장사항과 금지사항

이 페이지에서는 코드베이스 전반에서 일관성을 달성하는 스타일 작업의 권장사항과 API를 설계하는 동안 Google에서 따른 원칙을 설명합니다.

권장사항

다음의 권장사항을 따르세요.

권장사항: 시각적 요소에 스타일 사용, 동작에 수정자 사용

시각적 구성 (배경, 패딩, 테두리)에는 Styles API를 사용하고 클릭 로직, 동작 감지 또는 접근성과 같은 동작에는 수정자를 예약합니다.

권장사항: 디자인 시스템에서 스타일 매개변수 노출

자체 맞춤 디자인 시스템 구성요소의 경우 수정자 매개변수 뒤에 Style 객체를 노출해야 합니다.

@Composable
fun GradientButton(
    modifier: Modifier = Modifier,
    // ✅ DO: for design system components, expose a style modifier to consumers to be able to customize the components
    style: Style = Style
) {
    // Consume the style
}

권장사항: 시각적 기반 매개변수를 스타일로 바꾸기

컴포저블의 매개변수를 단일 Style 매개변수로 바꾸는 것이 좋습니다. 예를 들면 다음과 같습니다.

// Before
@Composable
fun OldButton(background: Color, fontColor: Color) {
}

// After
// ✅ DO: Replace visual-based parameters with a style that includes same properties
@Composable
fun NewButton(style: Style = Style) {
}

권장사항: 애니메이션에 스타일 우선순위 지정

수정자보다 성능이 향상된 애니메이션으로 상태 기반 스타일 지정을 위해 기본 제공 animate 블록을 사용합니다.

권장사항: '마지막 쓰기 우선' 활용

style 속성은 스택이 아닌 덮어쓰기한다는 점을 활용합니다. 이를 사용하여 여러 매개변수가 필요 없이 기본 구성요소 테두리 또는 배경을 재정의합니다.

금지사항

다음 패턴은 권장되지 않습니다.

금지사항: 상호작용 로직에 스타일 사용

스타일 내에서 onClick 또는 동작 감지를 처리하려고 시도하지 마세요. 스타일은 상태를 기반으로 하는 시각적 구성으로 제한되므로 비즈니스 로직을 처리해서는 안 됩니다. 대신 상태에 따라 시각적 요소가 달라야 합니다.

금지사항: 기본 스타일을 기본 매개변수로 제공

스타일 매개변수는 항상 style: Style = Style을 사용하여 선언해야 합니다.

@Composable
fun BadButton(
    modifier: Modifier = Modifier,
    // ❌ DON'T set a default style here as a parameter
    style: Style = Style { background(Color.Red) }
) {
}

'기본' 매개변수를 포함하려면 수신되는 매개변수 스타일을 정의된 기본값과 병합합니다.

@Composable
fun GoodButton(
    modifier: Modifier = Modifier,
    // ✅ Do: always pass it as a Style, do not pass other defaults
    style: Style = Style
) {
    // ...
    val defaultStyle = Style { background(Color.Red) }
    // ✅ Do Combine defaults inside with incoming parameter
    Box(modifier = modifier.styleable(styleState, defaultStyle, style)) {
      // your logic
    }
}

금지사항: 레이아웃 기반 컴포저블에 스타일 매개변수 제공

컴포저블에 스타일을 제공할 수 있지만 레이아웃 기반 컴포저블 또는 화면 수준 컴포저블이 스타일을 허용할 것으로 예상되지는 않습니다. 소비자 관점에서 이 수준에서 스타일이 무엇을 하는지 명확하지 않습니다. 스타일은 레이아웃이 아닌 구성요소를 위해 설계되었습니다.

금지사항: 컴포지션에서 스타일 만들기

CompositionLocals 는 사용되는 위치가 아니라 스타일이 정의된 지점에서 읽힙니다. 스타일이 실제로 사용되면 CompositionLocal의 상태가 변경되어 스타일이 부정확해질 수 있습니다.

// DON'T - Create styles in Composition that access composition locals in this way - this will likely lead to issues when style is used / accessed, as it would not get updated when the value changes.
@Composable
fun containerStyle(): Style {
    val background = MaterialTheme.colorScheme.background
    val onBackground = MaterialTheme.colorScheme.onBackground
    return Style {
        background(background)
        contentColor(onBackground)
    }
}

// Do: Instead, Create StyleScope extension functions for your subsystems to access themed composition Locals
val StyleScope.colors: JetsnackColors
    get() = JetsnackTheme.LocalJetsnackTheme.currentValue.colors

val StyleScope.typography: androidx.compose.material3.Typography
    get() = JetsnackTheme.LocalJetsnackTheme.currentValue.typography
val StyleScope.shapes: Shapes
    get() = JetsnackTheme.LocalJetsnackTheme.currentValue.shapes
// Access CompositionLocals
val button = Style {
    background(colors.brandSecondary)
    shape(shapes.small)
}

권장사항: 하위 시스템 값 변경을 위한 스타일 하나 만들기

예를 들어 어두운 모드와 밝은 모드 간에 전환하는 경우 기존 테마 값 (CompositionLocal을 통해)을 쿼리하여 Style을 동적으로 변경합니다.

// Do: Use CompositionLocals or themed values to create a single style
val buttonStyle = Style {
    background(colors.brandSecondary)
    shape(shapes.small)
}

권장사항: 구성요소가 테마 정의 전반에서 근본적으로 다른 경우 전체 스타일 전환

근본적으로 다른 테마인 경우 테마 수준에서 전체 스타일 객체를 전환할 수 있습니다.

예를 들어 제품/페이지 또는 제공별로 테마가 다르고 스타일의 여러 속성이 다른 앱을 만드는 경우 테마 수준에서 전체 스타일 집합을 전환하는 것이 좋습니다.

// DO Switch out whole styles when many properties differ - if Product A and Product B are two white labelled apps that provide different Themes.
val productBThemedButton = Style {
    shape(shapes.small)
    background(colors.brandSecondary)
    // other properties are fundamentally different
}

val productAThemedButton = Style {
    shape(shapes.large)
    background(colors.brand)
    // other properties are fundamentally different
}