인스타 릴스를 보면, 설명란의 텍스트 하단에 그라데이션 페이드아웃 효과가 들어간 것을 볼 수 있습니다.

스크롤을 하면 실시간으로 텍스트에서 그라이데이션 페이드아웃이 적용되는 위치가 달라집니다.
만약 뒤에 이미지가 보이는 형태가 아니었다면, 텍스트 위에 그라데이션이 적용된 컴포넌트를 얹어서 구현할 수 있습니다

하지만 배경이 이미지라면 아래와 같이 배경에도 그라데이션이 먹히는 이슈가 생깁니다.

어떻게 하면 글자에만 실시간 그라데이션을 적용할 수 있을까요? 지금부터 그 방법을 알아보도록 하겠습니다
graphicsLayer는 Composable을 별도의 렌더링 레이어(RenderNode) 위에 그리도록 만드는 Modifier입니다.
Box(
modifier = Modifier
.graphicsLayer {
scaleX = 0.9f
alpha = 0.8f
rotationZ = 45f
}
)
기본적으로 Compose는 UI 트리를 하나의 Canvas에 순서대로 그립니다. graphicsLayer를 붙이면 해당 Composable은 독립된 레이어에 먼저 그려지고, 이후 부모 Canvas에 합성(composite)됩니다.
트리 구조로 표현하면 다음과 같습니다.
기본 동작 (graphicsLayer 없음)
부모 Canvas
├── Composable A 그리기
├── Composable B 그리기 ← 모두 같은 Canvas에 순서대로
└── Composable C 그리기
graphicsLayer 적용 시
부모 Canvas
├── Composable A 그리기
├── [별도 RenderNode] ← 독립된 레이어
│ └── Composable B 그리기 ← 여기에 먼저 그려짐
│ ↓ composite
└── 부모 Canvas에 합성
1. 리컴포지션 없이 변환 가능
graphicsLayer는 그리기 명령을 그래픽 레이어에 격리시킵니다. alpha, scaleX, translationX 같은 그래픽 속성은 레이어 수준에서 처리되기 때문에, 애플리케이션 코드를 재실행하지 않아도 GPU가 직접 변환을 처리할 수 있습니다. 이 덕분에 리컴포지션 없이도 효율적인 재렌더링이 가능합니다. 애니메이션에서 성능이 중요한 이유가 여기 있습니다.
2. 독립적인 합성 컨텍스트
레이어가 분리되면 해당 Composable의 그리기 결과가 독립된 버퍼에 저장됩니다. BlendMode나 마스킹 같은 효과를 정확하게 적용할 수 있는 기반이 됩니다. 또한 컴포지션 기능으로 오프스크린 버퍼를 관리할 수도 있습니다.
3. 레이아웃에 영향을 주지 않습니다
graphicsLayer는 Composable의 측정된 크기나 배치를 변경하지 않습니다. 즉, 레이아웃 경계 외부에 그릴 수 있지만 레이아웃 자체는 그대로 유지됩니다.
4. 상태 객체와 람다 버전
graphicsLayer의 속성을 상태 객체로 업데이트하는 경우, 람다를 받는 버전(Modifier.graphicsLayer { ... })을 사용하는 것이 필수입니다. 람다 버전은 상태가 변경될 때 리컴포지션 없이 속성만 업데이트하기 때문입니다.
graphicsLayer에는 compositingStrategy 파라미터가 있습니다. 이 값이 레이어가 실제로 어떻게 생성되고 합성될지를 결정합니다.
Modifier.graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen)
세 가지 값이 있습니다.
나머지 graphicsLayer 매개변수에 의해 전략이 결정됩니다. alpha가 1.0f 미만이거나 RenderEffect가 설정된 경우에는 오프스크린 버퍼를 사용하고, 그 외에는 별도 레이어를 만들지 않고 바로 부모 Canvas에 그립니다. 불필요한 오버헤드를 줄이는 기본 전략입니다.
콘텐츠를 항상 별도의 오프스크린 텍스처/비트맵으로 래스터화한 뒤, 완성된 결과를 부모 Canvas에 합성합니다. BlendMode가 픽셀 단위로 올바르게 동작하려면 이 전략이 필요합니다.
예를 들어 BlendMode.Clear를 사용해 프로필 이미지 위에 알림 원을 뚫는 효과를 구현할 때도 Offscreen이 필요합니다. 오프스크린 버퍼 안에서만 블렌드가 동작하기 때문입니다.
한 가지 중요한 차이점이 있습니다. Auto에서는 변환이 레이아웃 경계 외부로 그려질 수 있지만, Offscreen에서는 콘텐츠가 레이어 경계 내로 클리핑됩니다. 이 점을 염두에 두고 사용해야 합니다.
출처: https://developer.android.com/develop/ui/compose/graphics/draw/modifiers
alpha 처리에 특화된 전략입니다. 오프스크린 버퍼를 생성하지 않고, 레이어 내의 각 그리기 명령에 alpha를 개별적으로 적용합니다.
Auto에서 alpha가 1.0f 미만인 경우와 비교하면 동작 방식이 다릅니다. Auto는 오프스크린 버퍼에 모든 콘텐츠를 먼저 그린 뒤 버퍼 전체에 alpha를 한 번에 적용합니다. 반면 ModulateAlpha는 버퍼 없이 각 그리기 명령 단위로 alpha를 곱합니다.
따라서 콘텐츠가 서로 겹치지 않는 경우에는 Auto보다 더 나은 성능을 기대할 수 있습니다. 단, 겹치는 콘텐츠가 있다면 Auto와 다른 시각적 결과가 나올 수 있으므로 주의가 필요합니다. 또한 RenderEffect가 설정된 경우에는 ModulateAlpha를 사용해도 오프스크린 버퍼가 생성됩니다.
페이드 아웃 효과를 구현하는 아이디어는 이렇습니다:
스크롤 영역 하단에 투명→불투명 그라디언트를 그린다.
그라디언트가 불투명한 영역에서는 아래에 있는 콘텐츠를 지운다.
이를 가능하게 하는 게 BlendMode.DstOut입니다.
Blend Mode는 두 픽셀(Source와 Destination)을 어떻게 합칠지 정의합니다.
DstOut의 공식은 다음과 같습니다:
결과 = Destination × (1 - Source.alpha)
즉, 그라디언트를 지우개처럼 사용하는 것입니다.
CompositingStrategy.Auto(기본값)에서는 alpha가 1.0인 경우 별도 레이어를 만들지 않습니다. 이 상태에서 BlendMode.DstOut으로 drawRect를 호출하면, 영향 범위가 이 Composable만이 아니라 전체 Canvas로 퍼집니다.
결과적으로 그라디언트가 Composable 안의 콘텐츠만 지우는 게 아니라, Canvas 전체를 뚫어버리는 의도치 않은 결과가 생깁니다. 즉, 이미지까지 지워집니다!
CompositingStrategy.Offscreen을 사용하면 이 Composable의 모든 그리기 작업이 독립된 버퍼 안에서 이루어집니다. DstOut도 그 버퍼 안에서만 동작하므로, 콘텐츠의 하단만 정확하게 지워낼 수 있습니다.
위에서 이론을 배웠으니 이제 실제로 적용해보도록 하겠습니다.
val scrollState = rememberScrollState()
val fadeHeight = 20.dp
val fadeHeightPx = with(LocalDensity.current) { fadeHeight.toPx() }
Box(
modifier = Modifier
.weight(1f)
.graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen)
.drawWithCache {
val gradientBrush = Brush.verticalGradient(
0f to Color.Transparent,
1f to Gray12,
startY = size.height - fadeHeightPx,
endY = size.height
)
onDrawWithContent {
drawContent()
drawRect(brush = gradientBrush, blendMode = BlendMode.DstOut)
}
}
) {
Column(
modifier = Modifier
.verticalScroll(scrollState)
.fillMaxSize()
) {
// 스크롤 콘텐츠
}
}
graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen) → 이 Box의 모든 그리기를 오프스크린 버퍼에서 처리합니다.drawWithCache → 사이즈가 바뀔 때만 Brush를 재계산하도록 캐싱합니다.onDrawWithContent { drawContent() } → 자식 Composable(콘텐츠)을 먼저 그립니다.drawRect(brush = gradientBrush, blendMode = BlendMode.DstOut) → 그라디언트를 지우개로 사용해 콘텐츠 하단을 점진적으로 지웁니다.구현 결과는 다음과 같습니다

비슷해 보이는 draw Modifier들의 차이를 정리하면 다음과 같습니다.
| Modifier | 특징 |
|---|---|
drawBehind | 자식 콘텐츠 뒤에 그립니다. drawWithContent의 편리한 래퍼로, 자식 콘텐츠 호출을 별도로 할 필요가 없습니다 |
drawWithContent | drawContent()를 직접 호출하여 컴포저블의 실제 콘텐츠를 렌더링합니다. 콘텐츠 앞/뒤 모두 자유롭게 그릴 수 있습니다 |
drawWithCache | drawWithContent와 동일하게 동작하되, 그리기 영역 크기가 같거나 상태 객체가 변경되지 않으면 Brush, Shader, Path 등 생성된 객체를 재할당하지 않고 캐시를 유지합니다 |
그라디언트처럼 사이즈에 의존하는 객체를 매 프레임 생성하면 GC 압력이 생기므로, drawWithCache를 사용하는 것이 좋습니다.
오프스크린 버퍼는 비용이 있습니다
CompositingStrategy.Offscreen은 별도의 텍스처/비트맵 버퍼를 할당하고 두 번 그리는 과정(오프스크린 → 화면)을 거칩니다. 화면에 많은 수의 아이템이 있다면 메모리와 GPU 부하를 고려해야 합니다.
단순 alpha 처리만 필요하다면 CompositingStrategy.Auto나 ModulateAlpha를 먼저 검토하세요.
alpha < 1.0f 시 자동으로 오프스크린 버퍼가 생성됩니다
CompositingStrategy.Auto(기본값) 상태에서 graphicsLayer의 alpha를 1.0f 미만으로 설정하면, 시스템이 자동으로 오프스크린 버퍼를 생성합니다. 의도하지 않은 오버헤드가 발생할 수 있으므로, 겹치지 않는 콘텐츠에 alpha를 적용할 때는 ModulateAlpha를 사용하면 버퍼 생성 없이 더 효율적으로 처리할 수 있습니다.