[Android Compose] Figma 그림자를 쉽게 만들어 보자

2025. 5. 14. 14:18·Android/Compose

안녕하세요! 오늘은 Jetpack Compose에서 디자이너가 피그마로 디자인한 그림자를 제대로 구현하기 위해 만든 확장함수를 소개합니다! 피그마의 양식을 어떻게 하면 제대로 적용할 수 있을지 고민했어요.

1. 선 요약

  • Jetpack Compose에서 제공하는 기본 Modifier.shadow는 커스터마이징에 한계가 있어요
  • 디자이너가 피그마에서 디자인한 그림자 효과를 구현하기 위해서 커스텀 확장 함수를 구현했어요
  • 그림자 효과를 구현하기 위해 Canvas API와 drawBehind 를 사용했어요
  • 성능 최적화를 위해 remember와 composed를 사용해 객체 재생성을 방지할 수 있어요

2. 기본 Modifier.shadow의 한계

Jetpack Compose에서 제공하는 기본 Modifier.shadow는 간단한 그림자 효과를 적용할 수 있지만, 디자이너가 피그마에서 정교하게 디자인한 그림자 효과(spread, blur 정도, 정확한 offset 등)를 구현하기에는 한계가 있어요.

// 기본 shadow 사용 예시
Box(
    modifier = Modifier
        .shadow(
            elevation = 4.dp,
            shape = RoundedCornerShape(8.dp)
        )
        .size(100.dp)
        .background(Color.White)
)

하지만 이 방식으로는 그림자의 색상, 번짐(blur) 정도, 확산(spread) 정도 등을 세밀하게 조절하기 어려워요.

그리고 좀 못생겼어요

3. 커스텀 dropShadow 확장 함수를 만들자

아래는 커스텀 그림자 효과를 구현하기 위한 확장 함수예요

@Composable
fun Modifier.dropShadow(
    shape: Shape,
    color: Color = Color.Black.copy(0.25f),
    blur: Dp = 1.dp,
    offsetY: Dp = 1.dp,
    offsetX: Dp = 1.dp,
    spread: Dp = 1.dp
) = composed {
    val density = LocalDensity.current
    
    val paint = remember(color, blur) {
        Paint().apply {
            this.color = color
            val blurPx = with(density) { blur.toPx() }
            if (blurPx > 0f) {
                this.asFrameworkPaint().maskFilter = 
                    BlurMaskFilter(blurPx, BlurMaskFilter.Blur.NORMAL)
            }
        }
    }
    
    drawBehind {
        val spreadPx = spread.toPx()
        val offsetXPx = offsetX.toPx()
        val offsetYPx = offsetY.toPx()
        
        val shadowWidth = size.width + spreadPx
        val shadowHeight = size.height + spreadPx
        
        if (shadowWidth <= 0f || shadowHeight <= 0f) return@drawBehind
        
        val shadowSize = Size(shadowWidth, shadowHeight)
        val shadowOutline = shape.createOutline(shadowSize, layoutDirection, this)
        
        drawIntoCanvas { canvas ->
            canvas.save()
            canvas.translate(offsetXPx, offsetYPx)
            canvas.drawOutline(shadowOutline, paint)
            canvas.restore()
        }
    }
}

이 코드는 다음과 같은 파라미터를 통해 그림자를 제어할 수 있어요

  • shape: 그림자의 모양을 결정해요
  • color: 그림자의 색상을 지정해요
  • blur: 그림자의 번짐 정도를 설정해요
  • offsetX/offsetY: 그림자의 위치를 조정해요
  • spread: 그림자가 얼마나 넓게 퍼질지 결정해요

성능 최적화도 고려했어요

  1. composed 사용: composed 블록을 사용해 Compose에 특화된 로직을 작성할 수 있게 했어요.
  2. remember 활용: 주요 객체들을 remember를 사용해 캐싱하여 불필요한 재생성을 방지했어요.
  3. 조건 검사 추가: shadowWidth나 shadowHeight가 0 이하일 경우 그리지 않도록 얼리 리턴을 적용해서 불필요한 연산을 방지했어요.

바로 적용해 보자!

피그마의 그림자 예시

Box(
    modifier = Modifier
        .dropShadow(
            shape = RoundedCornerShape(12.dp),
            color = Color(0xFF82848D).copy(alpha = 0.3f),
            offsetX = (-8).dp,
            offsetY = 0.dp,
            blur = 10.dp,
            spread = 0.dp
				)
        .size(100.dp)
        .background(Color.White, RoundedCornerShape(8.dp))
)

이런식으로 작성만하면 피그마의 디자인대로 그림자를 그릴 수 있어요!

4. 코드 세부사항 설명

1. BlurMaskFilter 이해하기

if (blurPx > 0f) {
    this.asFrameworkPaint().maskFilter = 
    BlurMaskFilter(blurPx, BlurMaskFilter.Blur.NORMAL)
}

BlurMaskFilter는 Android의 그래픽 API에서 제공하는 필터로, 그림자의 흐림 효과를 구현해요. BlurMaskFilter.Blur.NORMAL은 일반적인 가우시안 블러를 적용해요.

2. Shape와 Outline의 관계

val shadowOutline = shape.createOutline(shadowSize, layoutDirection, this)

Compose에서 Shape은 UI 요소의 외형을 정의하는 인터페이스예요. createOutline 메서드는 주어진 크기와 레이아웃 방향에 따라 실제 윤곽선 정보를 생성해요. 이 윤곽선을 사용해 그림자를 그려요.

3. Canvas 조작

drawIntoCanvas { canvas ->
    canvas.save()
    canvas.translate(offsetXPx, offsetYPx)
    canvas.drawOutline(shadowOutline, paint)
    canvas.restore()
}

Canvas API를 사용해 그림자를 실제로 그려요:

  • save()/restore(): 캔버스의 상태를 저장하고 복원해요
  • translate(): 그림자의 위치를 조정해요
  • drawOutline(): 앞서 생성한 윤곽선을 캔버스에 그려요

5. 적용한 모습!

우측의 정렬 아이콘의 그림자가 피그마와 유사하게 수정되었어요.

Before

After

힘의 차이가 느껴지십니까?

결론.

M3의 디자인을 따르지 않고 독자적인 디자인을 사용하는 서비스일수록 커스텀 요소가 많아지는데, 성능과 함께 고려해 구현해 보았습니다!

혹시 비슷한 문제를 겪고 계신 분들께 도움이 되었으면 좋겠어요! 더 좋은 해결 방법이나 잘못된 정보 혹은 의견이 있으시다면 댓글로 공유해 주시면 감사하겠습니다 :)

'Android > Compose' 카테고리의 다른 글

[Android Compose] Effect Handlers 딥다이브  (0) 2025.06.20
[Android Compose] 상태 읽기 지연(Defer State Reads)으로 리컴포지션 최적화하기  (0) 2025.06.18
[Android Compose] Figma의 타원형 Radial Gradient를 구현해보자  (0) 2025.06.06
[Android Compose] TopBar 배경색 전환 애니메이션 구현 방법  (0) 2025.02.05
[Android Compose] ImePadding 이중 패딩 문제 해결 방법 (+키보드 영역 조정)  (7) 2025.02.05
'Android/Compose' 카테고리의 다른 글
  • [Android Compose] 상태 읽기 지연(Defer State Reads)으로 리컴포지션 최적화하기
  • [Android Compose] Figma의 타원형 Radial Gradient를 구현해보자
  • [Android Compose] TopBar 배경색 전환 애니메이션 구현 방법
  • [Android Compose] ImePadding 이중 패딩 문제 해결 방법 (+키보드 영역 조정)
한민돌
한민돌
Android 개발자가 되기까지.
  • 한민돌
    미래 가젯 연구소
    한민돌
  • 전체
    오늘
    어제
    • 분류 전체보기 (20) N
      • Android (4)
        • Compose (10)
        • Jetpack (2)
      • Kotlin (2)
        • Kotlin In Action (0)
      • 외부 활동 (2) N
        • 우아한테크코스 8기 (2) N
  • 블로그 메뉴

    • 홈
    • 태그
  • 링크

    • GitHub
  • 인기 글

  • 태그

    runcatching
    custom plugin
    Android
    LaunchedEffect
    build-logic
    compose
    rememberupdatedstate
    AboutLibraries
    우테코
    Multi-module
    Google Recommend Architecture
    jetpack
    API35
    producestate
    sideeffect
    backing property
    OssLicensePlugin
    coroutine
    Baseline Profile
    derivedstateof
  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
한민돌
[Android Compose] Figma 그림자를 쉽게 만들어 보자
상단으로

티스토리툴바