본문 바로가기
안드로이드

Compose에 Mesh Gradient가 생겼다, 단 함정이 있다

by 안드뽀개기 2026. 6. 24.
반응형

SwiftUI가 iOS 18에서 MeshGradient를 출시했을 때, Android 진영은 ShaderEffect 조합이나 서드파티 라이브러리로 비슷한 효과를 흉내 냈다. 그 격차가 Jetpack Compose에서 공식 API로 좁혀지고 있다. MeshGradientPainterandroidx.compose.ui에 등장했다.

아직 알파다. 그러나 개념을 이해하고 실제로 그려보기에는 충분히 성숙해 있다.


먼저, 버전 체크

# gradle/libs.versions.toml
composeBom = "2026.05.00"
composeUi  = "1.12.0-alpha03"

별도 의존성은 없다. androidx.compose.ui 알파 버전으로 올리면 바로 쓸 수 있다. 실험적 애노테이션(@OptIn(ExperimentalComposeUiApi::class) 같은 것)도 필요 없다. 단지 알파라는 사실만 기억하면 된다. API 시그니처는 stable 전까지 언제든 바뀔 수 있다.


Mesh Gradient가 일반 Gradient와 다른 이유

선형이나 방사형 그라디언트는 하나의 방향이나 중심점에서 색이 퍼진다. Mesh Gradient는 2D 격자 위에 색을 놓는다. 격자의 각 꼭짓점(vertex)에 위치와 색을 지정하면, 렌더러가 모든 방향으로 부드럽게 섞어낸다.

결과물이 "살아있는" 느낌을 주는 이유가 여기에 있다. 단순한 보간이 아니라 면 전체가 유기적으로 혼합된다.

Compose에서 이 구조는 세 가지로 정리된다.

  • Vertex: 격자 교점. 위치(정규화된 Offset)와 Color를 가짐
  • Patch: 꼭짓점 사이의 셀. 실제 블렌딩이 일어나는 곳
  • MeshGradientPainter: 표준 Painter를 구현. Modifier.paint(), Modifier.background(), Canvas 어디서든 쓸 수 있음

가장 단순한 예제

val painter = remember {
    MeshGradientPainter(rows = 1, columns = 1, hasBicubicColor = true) {
        setVertex(0, 0, Offset(0f, 0f), Color(0xFFAF52DE)) // 좌상
        setVertex(0, 1, Offset(1f, 0f), Color(0xFF5856D6)) // 우상
        setVertex(1, 0, Offset(0f, 1f), Color(0xFFFF9500)) // 좌하
        setVertex(1, 1, Offset(1f, 1f), Color(0xFFFFCC00)) // 우하
    }
}

Box(
    modifier = Modifier
        .fillMaxSize()
        .paint(painter)
)

위치 좌표는 픽셀이 아니라 정규화된 값이다. (0f, 0f)이 좌상단, (1f, 1f)이 우하단이다. 디바이스 해상도를 신경 쓸 필요가 없다.

hasBicubicColor = true는 바이큐빅 보간을 켠다. 기본값(false)보다 색이 훨씬 부드럽게 섞인다. 거의 항상 true로 두게 될 것이다.

격자를 촘촘하게 만들고 싶다면 rowscolumns를 올리면 된다. 4×4 격자라면 열여섯 개의 색이 동시에 흘러들어간다.


가장 흔히 걸리는 함정: rows/columns는 "패치" 수다

SwiftUI의 MeshGradientwidthheight로 꼭짓점 개수를 지정한다. width: 3, height: 3이면 3×3 = 9개의 포인트.

Compose는 다르다. rowscolumns는 꼭짓점 사이 셀(패치)의 수다.

// rows = 1, columns = 1 → 패치 1개 → 꼭짓점은 2×2 = 4개
MeshGradientPainter(rows = 1, columns = 1, ...) {
    setVertex(0, 0, ...) // row 인덱스 0~1, column 인덱스 0~1
    setVertex(0, 1, ...)
    setVertex(1, 0, ...)
    setVertex(1, 1, ...)
}

// rows = 2, columns = 2 → 패치 4개 → 꼭짓점은 3×3 = 9개
MeshGradientPainter(rows = 2, columns = 2, ...) {
    // setVertex 호출 9번 필요
}

꼭짓점 총 개수는 (rows + 1) × (columns + 1)이다. SwiftUI에서 넘어오거나 서드파티 라이브러리를 쓰던 사람이라면 거의 반드시 여기서 한 번 틀린다.

setVertex 인덱스가 범위를 벗어나도 컴파일 에러는 나지 않는다. 잘못된 꼭짓점 설정이 조용히 무시되거나 이상한 렌더링으로 이어질 수 있으므로, 격자 크기와 setVertex 호출 횟수를 항상 함께 검토해야 한다.


애니메이션이 자연스럽게 연결되는 구조

설정 블록은 MeshGradientScope이고, 내부적으로 DrawScope처럼 상태 읽기를 관찰한다. 즉, animateFloatAsState로 꼭짓점 위치를 움직이면 recomposition 없이 직접 재드로잉이 트리거된다.

val offset by animateFloatAsState(
    targetValue = if (animate) 0.3f else 0f,
    animationSpec = infiniteRepeatable(tween(2000), RepeatMode.Reverse)
)

val painter = remember(offset) {
    MeshGradientPainter(rows = 1, columns = 1, hasBicubicColor = true) {
        setVertex(0, 0, Offset(0f + offset, 0f), Color(0xFFAF52DE))
        setVertex(0, 1, Offset(1f, 0f - offset), Color(0xFF5856D6))
        setVertex(1, 0, Offset(0f, 1f + offset), Color(0xFFFF9500))
        setVertex(1, 1, Offset(1f - offset, 1f), Color(0xFFFFCC00))
    }
}

꼭짓점이 살짝씩 흘러다니는 배경 효과를 만들기에 딱 맞는 구조다.


언제 써야 하고, 언제 피해야 하는가

배경, 카드 그라디언트, 온보딩 화면처럼 정적이거나 완만하게 움직이는 장식 요소에 적합하다. 색을 레이아웃에 종속시키지 않고 독립적으로 제어할 수 있다는 점도 장점이다.

반면 아직 알파다. 현재 production 코드에 넣는 건 권장하지 않는다. API가 stable로 졸업하기 전에 파라미터 이름이나 동작이 바뀔 수 있고, 그 시점에 다시 뒤집어야 할 수 있다.

퍼포먼스 측면에서도 격자가 지나치게 촘촘하거나 애니메이션 프레임마다 remember를 갱신하는 구조라면 측정이 필요하다. 단순한 배경 용도에서는 문제가 없지만, 무거운 화면에서 동시에 여러 개를 돌리면 확인이 필요하다.


프로토타입이나 사이드 프로젝트에서 먼저 써보기엔 지금이 적당한 시점이다. 개념을 익혀두면 stable 출시 때 바로 실전에 투입할 수 있다.

※ 본 글은 정보 제공 목적이며 특정 제품·서비스의 추천이 아닙니다.

반응형