[Android] Draw Modifiers

MariGold·2025년 10월 13일

[Android]

목록 보기
5/12
post-thumbnail

Jetpack Compose에서는 UI가 상태에 따라 자동으로 다시 그려지는 리컴포지션(Recomposition) 덕분에 선언적 UI 작성이 쉬워졌습니다.
하지만 모든 것을 @Composable로만 그리면, 불필요한 리컴포지션이 자주 발생해 성능이 떨어질 수 있습니다.

이번 글에서는 Compose의 그래픽 API와 Draw Modifiers를 활용해 효율적으로 커스텀 드로잉을 처리하고 리컴포지션을 줄이는 방법을 정리해보겠습니다.


🚀 Draw Modifiers란?

Compose의 Draw Modifiers는 Composable의 시각적 출력을 커스터마이징할 수 있는 도구입니다. 대표적으로 다음과 같은 Modifier 함수들이 있습니다.

  • Modifier.drawBehind { ... }
    → Composable 뒤에 직접 그리기

  • Modifier.drawWithContent { ... }
    → Composable 기존 콘텐츠와 함께 그리기

  • Modifier.drawWithCache { ... }
    → 캐싱 기반 드로잉 — 리컴포지션을 줄이는 핵심!


⚡ drawBehind vs drawWithCache

두 가지 모두 Canvas에 직접 그릴 수 있는 API지만, 리컴포지션 빈도에 큰 차이가 있습니다.

항목drawBehinddrawWithCache
호출 시점매번 그릴 때마다캐시된 DrawScope를 활용
리컴포지션자주 발생최소화됨
렌더링 성능간단하지만 비효율적일 수 있음비싼 연산을 캐시하여 효율적
사용 용도간단한 배경, 작은 효과복잡한 드로잉, 반복 계산 최소화
상태 의존성상태 변경 시 즉시 다시 그려짐상태가 바뀌지 않으면 재계산하지 않음
대표 예시배경색, 단색 도형Gradient, Path, Brush 등 복잡한 효과
추천 시나리오빠르게 간단히 그릴 때성능이 중요한 커스텀 그래픽 구현 시
예시: drawBehind
Box(
    modifier = Modifier
        .size(200.dp)
        .drawBehind {
            drawRect(Color.Gray)
            drawCircle(Color.Blue, radius = 80f)
        }
)

이 코드는 단순하지만, 상태가 바뀌어 Box가 리컴포즈될 때마다 매번 drawBehind가 다시 호출됩니다. 이런 경우는 간단한 드로잉엔 괜찮지만, 복잡한 그래픽이면 퍼포먼스 저하가 생깁니다.


🧠 drawWithCache로 리컴포지션 줄이기

drawWithCache는 이름 그대로, 드로잉 계산을 캐시하여 불필요한 다시 그리기를 막아줍니다.

Box(
    modifier = Modifier
        .size(200.dp)
        .drawWithCache {
            // 캐시에 저장되는 영역
            val gradient = Brush.linearGradient(
                colors = listOf(Color.Cyan, Color.Blue)
            )

            onDrawBehind {
                // drawScope 내부에서 gradient 재사용
                drawRect(brush = gradient)
            }
        }
)

onDrawBehind { ... }는 실제 그리기 단계에서만 호출되고,
그 외의 비용이 큰 계산(예: Brush 생성)은 drawWithCache의 초기 캐시 영역에서 한 번만 수행됩니다.

상태 변경이 일어나도 gradient가 바뀌지 않는다면, drawWithCache는 다시 계산하지 않습니다. 결과적으로 리컴포지션과 재그리기 모두 줄어듭니다.


🧩 상태 의존성 관리

drawWithCache 내부에서 상태를 사용할 때 주의할 점이 있습니다.

@Composable
fun CachedCircle(color: Color) {
    Box(
        modifier = Modifier
            .size(150.dp)
            .drawWithCache {
                onDrawBehind {
                    drawCircle(color)
                }
            }
    )
}

이 예시는 단순히 color만 사용하는데, 만약 color 값이 자주 바뀐다면?
➡️ drawWithCache는 내부 캐시를 새로 계산합니다. 따라서 진짜로 자주 바뀌지 않는 값만 캐싱하는 것이 핵심입니다.


🎨 실전 예시: 그래픽 성능 개선하기

예를 들어, 매 프레임마다 회전하는 애니메이션을 그려야 한다면?

@Composable
fun RotatingShape(angle: Float) {
    Box(
        modifier = Modifier
            .size(200.dp)
            .drawWithCache {
                val brush = Brush.radialGradient(
                    listOf(Color.Magenta, Color.Red)
                )
                onDrawBehind {
                    rotate(angle) {
                        drawRect(brush = brush)
                    }
                }
            }
    )
}

이때 angle 값이 바뀔 때마다 전체 리컴포지션이 발생하지는 않습니다.
drawWithCache 내부에서는 draw 단계만 갱신되므로, Canvas의 invalidate()만 호출되어 매우 효율적입니다.


🧩 유용한 Draw Modifier

Modifier설명
drawBehind기존 콘텐츠 뒤에 그리기
drawWithContent기존 콘텐츠 위에 덧그리기
drawWithCache드로잉 계산 캐시
graphicsLayer하드웨어 가속 및 변환 (회전, 스케일 등)
alpha, clip, shadow자주 쓰이는 그래픽 효과

예를 들어, drawWithContent는 기존 컴포넌트를 유지한 채 추가적인 효과를 넣을 때 유용합니다.

Modifier.drawWithContent {
    drawContent()
    drawRect(Color.Black.copy(alpha = 0.2f)) // 반투명 오버레이
}

🧾 정리: 리컴포지션 최소화 전략

전략설명
drawWithCache 사용비싼 계산(Brush, Path 등)은 캐시
remember와 함께 사용Compose 상태 기반 캐시 병행
graphicsLayer 활용GPU 변환으로 효율적인 렌더링
⚠️ 상태를 과도하게 의존하지 말기상태 변경이 많으면 캐시 무용지물
⚡ 가능한 한 “그리기 전용 함수”로 분리UI와 렌더 로직 분리

🧩 결론

리컴포지션을 줄이는 핵심은 “무엇을 다시 계산할지 최소화하는 것”입니다.

Compose의 그래픽 시스템은 선언적이지만, drawWithCache와 같은 도구를 잘 활용하면 낮은 오버헤드로 고성능 UI를 구현할 수 있습니다.

  • 단순한 효과 → drawBehind

  • 복잡한 계산 → drawWithCache

  • 실시간 애니메이션 → graphicsLayer

이렇게 나눠서 사용하는 것이 좋습니다.


📚 참고 자료


💡 TIP: 복잡한 UI 효과를 그릴 때는 Composable을 여러 개 쪼개기보다,
하나의 Composable 내부에서 drawWithCache를 사용하는 편이 오히려 리컴포지션을 덜 발생시킵니다.

profile
많은 것을 알아가고 싶은 Android 주니어 개발자

0개의 댓글