Android 개발/Jetpack Compose

MeshGradient Modifier, 한 줄로 끝난다는 말의 진짜 의미

stackD 2026. 5. 30. 18:00

 

어제 1.12.0-alpha02 받자마자 온보딩 배경 Modifier 한 줄을 meshGradient 로 바꿔봤는데요. 픽셀 6 에서는 부드럽게 흐르는데, 에뮬레이터에서는 시커먼 사각형만 떠있더군요.

 

알파 버전 릴리즈 노트에서 Modifier.meshGradient() 가 정식 API로 들어왔다는 줄을 보고, 사이드 프로젝트 환영 화면 배경을 바꿔보고 싶어서 손이 먼저 갔습니다. 결과부터 말씀드리면, 분명 한 줄로 적용은 되는데 그 한 줄 뒤에 깔아야 할 게 의외로 많은 API였어요.

 

Compose 1.12.0-alpha02 meshGradient 시그니처와 한 줄 적용

먼저 시그니처부터 짚고 가겠습니다. points 가 2D 그리드의 (Offset, Color) 쌍 리스트이고, 해상도 인자 두 개가 따라붙는 구조입니다.

// Modifier 확장 함수 시그니처
Modifier.meshGradient(
    points: List<List<Pair<Offset, Color>>>,
    resolutionX: Int = 32,
    resolutionY: Int = 32,
)

 

여기서 핵심은 Offset 이 0f~1f 정규화 좌표라는 점이에요. 화면 크기에 종속되지 않으니까 같은 points 상수를 폰·태블릿·폴더블 어디든 그대로 꽂을 수 있습니다.

private val OnboardingMesh = listOf(
    listOf(Offset(0f, 0f) to Color(0xFF0F2027), Offset(1f, 0f) to Color(0xFF203A43)),
    listOf(Offset(0f, 1f) to Color(0xFF2C5364), Offset(1f, 1f) to Color(0xFF000000)),
)

Box(Modifier.fillMaxSize().meshGradient(OnboardingMesh))

 

points 를 파일 상단 상수로 빼두니까 호출부는 진짜 한 줄로 줄더라고요. "한 줄 적용"이라는 마케팅 카피가 거짓말은 아니었습니다. 다만 그 한 줄 위에 4쌍짜리 코너 정의가 깔려 있다는 사실은 같이 봐야겠지요.

 

 

Brush.linearGradient 와 meshGradient, 언제 갈아탈까

기존 Brush.linearGradient 로 4색 그라데이션을 만들어 본 적이 있으신 분이라면 각도 계산과 색상 띠(banding) 현상에서 고생한 기억이 있을 것 같아요. 저도 디자이너 시안 받아서 각도 67도 비슷하게 맞춰가며 RGB 보간 띠 잡으려고 한참 매달렸습니다.

meshGradient 는 코너 색상 4개(혹은 그 이상)만 정의하면 사이를 셰이더가 알아서 메워줍니다. 4색 이상 교차하는 시안에서 띠 현상이 안 보이는 게 가장 큰 차이로 보여요. 반대로 단색 강조나 2색 수직 그라데이션 같은 단순 배경에는 GPU 부하만 늘어나는 오버스펙입니다.

 

제 기준으로는 3색 이상 교차하는 정적 화면 — 환영 화면, 결제 성공 화면, 랜딩 페이지 정도에만 선별 적용하는 게 합리적이라고 봅니다. 스크롤 컨테이너나 다이얼로그 배경에 깔면 셰이더 컴파일 비용이 매 프레임 발목을 잡을 수 있거든요.

 

안드로이드 13(API 33) 미만 폴백과 셰이더 콜드 스타트

여기서부터가 함정 구간입니다. meshGradient 는 내부적으로 AGSL(Android Graphics Shading Language) 셰이더를 쓰기 때문에, 공식 문서상 AGSL/RuntimeShader 는 안드로이드 13(API 33) 이상에서 지원됩니다. API 33 미만에서는 폴백 동작이 필요한데, 제 환경(API 30 에뮬레이터)에서는 좌상단 색상의 단색 사각형 비슷한 형태로만 그려졌어요 — 알파 릴리즈 노트에 명시된 폴백 스펙은 아니고 어디까지나 제 관찰입니다.

 

제 사이드 프로젝트 minSdk 가 26이라, 픽셀 6(API 33 이상)에서는 부드러운 메쉬가 흐르는데 에뮬레이터(API 30)에서는 시커먼 사각형만 떠있던 이유가 이거였어요. 최소 SDK 가 33 미만인 프로젝트는 결국 분기 처리 코드를 한 벌 더 들고 다녀야 합니다.

 

콜드 스타트 첫 프레임 셰이더 컴파일도 챙기셔야 되는데요, 첫 진입 시 검은 화면이 잠깐 깜빡이는 경우가 생기더라고요. 저는 단색 배경 Box 를 먼저 그리고 그 위에 메쉬를 올리는 식으로 회피했는데, 체감 깜빡임이 확실히 줄어들었습니다.

 

 

meshGradient 알파 도입 전 체크리스트 4가지

프로덕션 도입 전에 짚어야 할 지점을 정리하면 이렇습니다.

  1. 최소 SDK 가 33 미만이면 isAtLeast(33) 분기로 Brush.linearGradient 폴백 코드를 한 벌 더 유지해야 합니다.
  2. 다크 모드는 MaterialTheme.colorScheme 과 자동 연동되지 않아서, isSystemInDarkTheme() 으로 색상표를 직접 교체해야 해요.
  3. 그라데이션 위 텍스트는 WCAG 명도 대비 확보가 어려우니, 반투명 스크림(scrim) Box 를 한 겹 깔아주는 게 안전합니다.
  4. Paparazzi 같은 스크린샷 회귀 테스트는 셰이더 렌더링을 100% 재현하지 못해 실패할 가능성이 있어, 해당 화면은 테스트 제외 또는 우회 처리가 필요합니다.

특히 1번과 2번은 "한 줄로 적용" 이라는 첫인상과 가장 크게 어긋나는 지점이에요. 결국 컴포저블 안에서 분기·스크림·테마 색상 교체까지 같이 챙겨야 하니까 실제 코드량은 5~10줄로 늘어나게 됩니다.

 

 

안드로이드 스튜디오 Preview 색상이 실기기와 다를 때

개인적으로 가장 당황스러웠던 대목은 안드로이드 스튜디오(Android Studio) Preview 결과물이 픽셀 6 실기기와 채도가 확연히 다르게 보였다는 점입니다. Preview 화면에서는 채도가 다르게 보였어요 — Preview 가 AGSL 셰이더를 그대로 처리하지 못하는 내부 처리 차이 때문으로 추정합니다.

 

그래서 디자이너 시안 검수는 무조건 API 33 이상 실기기에서 진행했는데요, 개인적으로 에뮬레이터에 의존하는 것보다 실기기 한 대 옆에 놓고 핫리로드 돌리는 쪽이 색상 검수 속도가 훨씬 빨라서 추천드리고 싶네요. Preview 색만 믿고 PR 올렸다가 QA 단계에서 시안 미일치로 되돌아오면 그 손실이 더 큽니다.

 

정식 stable 전, 미리 손에 익혀둘 가치

알파 API 라서 시그니처가 stable 가기 전에 한 번 더 바뀔 가능성도 있고, 최소 SDK 가 33 미만인 프로젝트라면 폴백·스크림·다크 모드 분기 코드까지 같이 들고 다녀야 하니까 프로덕션 본격 도입은 stable 출시 이후로 미뤄두시는 게 안전합니다.

 

그래도 환영 화면이나 결제 성공 화면처럼 정적이고 화려해도 되는 한두 화면에 한정해서 미리 손에 익혀두는 건 의미가 있다고 봅니다. stable 떨어졌을 때 그때 가서 시안 받아 허둥대는 것보다, 알파 단계에서 코너 색상 정의 감각과 폴백 패턴을 미리 잡아두면 정식 도입 시 며칠을 아낄 수 있거든요.

 

결국 어제 제 픽셀 6 화면에서 부드럽게 흐르던 그 그라데이션도, 코드 위에서는 4쌍짜리 좌표 정의와 폴백 한 벌이 깔린 결과였던 셈이지요.