← Back to list

Jetpack Compose Modifier

Jetpack Compose는 Android UI 개발을 명령형(Imperative) 방식에서 선언형(Declarative) 모델로 전환한 UI 프레임워크다. 전통적인 View 시절에는 개발자가 UI 변경을 직접 지시해야 했다.

Eden in Tecoble · 2025-11-24 07:10 · 0 claps · 14.9 min read
#modifier #jetpack-compose #android #woowacourse
Open on Medium ↗
Wiki topics: 📱 · Mobile Development

Jetpack Compose Modifier

Jetpack Compose는 Android UI 개발을 명령형(Imperative) 방식에서 선언형(Declarative) 모델로 전환한 UI 프레임워크다. 전통적인 View 시절에는 개발자가 UI 변경을 직접 지시해야 했다.

textView.setText("Hello")
button.setPadding(16)

하지만 Compose에서는 UI가 “어떻게 변화해야 하는지”를 코드로 명시하는 것이 아니라, “UI가 어떤 상태(State)를 기반으로 그려져야 하는지”만 선언하면 된다.

UI = f(State)

상태가 변하면 Compose는 해당 UI 요소만 자동으로 재구성(Recomposition)한다.

개발자는 더 이상 UI 갱신 절차를 관리하지 않아도 된다.

val isSelected by remember { mutableStateOf(false) }
Button(
    onClick = { isSelected = !isSelected },
    colors = ButtonDefaults.buttonColors(
        containerColor = if (isSelected) Color.Red else Color.Gray
    )
) {
    Text("Click Me")
}

이처럼 UI는 상태의 함수라는 Compose의 철학이 자리를 잡게 되면서, UI의 “구조(Structure)”와 “스타일/동작(Behavior)”을 분리하는 메커니즘이 필요해졌다.

바로 이 지점에서 등장한 것이 Modifier이다.

Modifier는 단순한 꾸밈 옵션이 아니라, “Composable의 레이아웃·그리기·입력·상호작용”을 선언적으로 조합하는 언어적 도구다.

이 글은 바로 이 Modifier가 어떻게 구성되고 활용되며, 왜 Compose 개발자에게 필수적인 기반이 되는지를 중점적으로 살펴본다.

1. Compose의 철학과 Modifier의 등장 배경

1.1 선언적 UI의 원리: UI = f(State)

Compose의 가장 중요한 원리는 “UI는 상태(State)에 의해 완전히 정의된다”는 것이다.

UI = f(State)

상태가 변하면 Compose는 해당 UI 조각만 자동으로 재구성(Recomposition)한다.

이는 기존 Android View 시스템의 명령형 UI 갱신 방식과 근본적으로 다르다.

기존 View 시스템의 문제

  • setText(), setPadding() 같은 명령형 API 호출로 직접 UI를 갱신해야 함
  • UI와 상태의 싱크가 어긋나기 쉬움
  • 변경 과정이 많아 코드가 복잡해짐
  • 예측 가능한 UI 동작을 보장하기 어려움

Compose의 선언적 접근

  • UI는 항상 현재 상태의 결과물
  • 상태가 변하면 Compose가 UI를 다시 계산
  • 불변성 기반이라 일관성예측 가능성이 높음

아래 코드는 그 차이를 간단히 보여준다.

val isSelected by remember { mutableStateOf(false) }

Button(
    onClick = { isSelected = !isSelected },
    colors = ButtonDefaults.buttonColors(
        containerColor = if (isSelected) Color.Red else Color.Gray
    )
) {
    Text("Click Me")
}

isSelected상태가 변경되면 Compose가 자동으로 버튼 색상을 재구성한다. 이런 구조에서는 UI의 구조(Structure) 스타일,동작(Style/Behavior)을 명확하게 분리할 필요가 생긴다.

이 역할을 담당하는 것이 바로 Modifier다.

1.2 Modifier의 철학적 위치

기존 Android View에서는 View 객체가 모든 속성(레이아웃, 색상, 입력 처리)을 직접 품고 있었다.

view.setPadding(16)
view.setBackgroundColor(Color.Gray)

이 방식은 다음과 같은 한계가 있었다.

  • UI 속성이 객체에 직접 붙어 있어 재사용성이 떨어짐
  • UI 업데이트 과정이 명령형이라 예측 불가능한 상태 발생 가능
  • UI와 로직이 강하게 결합됨

Compose에서는 이러한 갱신 로직을 Modifier 체이닝으로 대체한다.

Modifier
    .padding(16.dp)
    .background(Color.Gray)

이 구조는 Compose의 세 가지 핵심 철학을 반영한다.

  • 불변성 (Immutability) 모든 Modifier 함수는 새로운 객체를 생성한다. → Compose 런타임이 안전하게 캐싱 가능
  • 조합성 (Composability) Modifier를 체인으로 연결해 복잡한 UI 동작을 선언적으로 구성 → “필요한 조각을 쌓아 올리는 느낌”
  • 단일 책임 원칙 (Single Responsibility) 각 Modifier는 명확히 한 가지 역할만 수행
  • padding: 레이아웃
  • background: 그리기
  • clickable: 입력 처리 → 순서가 중요한 이유

2. Modifier 구조 및 체이닝 동작 원리

2.1 불변성과 결합 구조

Modifier는 불변(immutable) 객체로 설계되어 있다.

각 Modifier 연산(padding, background, clickable 등)은 기존 Modifier를 변경하지 않고 새로운 Modifier 인스턴스를 반환한다.

val paddingModifier = Modifier.padding(8.dp)
val backgroundModifier = paddingModifier.background(Color.Gray)

paddingModifier는 변경되지 않으며, backgroundModifier는 별도의 Modifier 체인이다.

왜 이렇게 설계되었을까?

Compose에서 불변성이 중요한 이유는 다음과 같다.

  • Modifier를 안전하게 비교하고 캐싱 가능
  • “변경된 부분만 재구성(Recomposition)”할 수 있도록 런타임에서 최적화 가능
  • UI 동작을 예측 가능하게 유지

즉, Modifier의 불변성은 단순한 코드 스타일이 아니라 Compose 성능·안정성·예측 가능성을 지탱하는 핵심 기반이다.

2.2 체인 결합(Chaining) 및 then()

Modifier는 내부적으로 연결 리스트(Linked List) 구조로 결합된다. 각 Modifier는 이전 Modifier를 참조하며, Compose 런타임은 이를 앞에서 뒤로 순서대로 해석한다.

Compose는 UI를 그리는 과정에서 Modifier 체인을 다음 렌더링 단계에서 적용한다.

  • Measure (측정)
  • Layout (배치)
  • Draw (그리기)
  • Input (제스처·클릭 처리)
  • Semantics (접근성 정보)

즉, Modifier는 체인에 추가된 순서대로 적용되며, 순서가 달라지면 UI 결과도 달라진다.

Modifier
    .padding(16.dp) // Layout 단계에서 적용
    .background(Color.Black) //Draw 단계에서 적용
    .clickable { /* … */ } //Input 단계에서 적용

내부적으로는 다음과 같은 연결 구조가 만들어진다.

Modifier → padding → background → clickable

then()의 역할

Modifier.then(other)는 기존 Modifier에 다른 Modifier를 뒤에 이어 붙이는 함수다. 기존 Modifier를 변경하지 않고 새로운 Modifier 체인을 반환한다.

val base = Modifier.padding(8.dp)
val extended = base.then(Modifier.background(Color.Gray))

이 방식은 Modifier의 불변성(immutability)조합성(composability)을 유지하는 핵심 메커니즘이며, 체인의 순서를 명확히 유지하는 데에도 중요하다.

2.3 Node API (Compose 1.5 이후)

Compose 1.5+ 버전부터 Modifier는 Node API 기반으로 재설계되었다.

기존에는 하나의 Modifier가 여러 기능(측정, 그리기, 입력 처리 등)을 동시에 담당할 수 있었지만, Node API는 이러한 기능을 단계별로 Node 단위로 분리하는 방식으로 동작한다.

Modifier가 Node로 변환되는 구조는 다음과 같다.

  • paddingLayoutNode
  • backgroundDrawNode
  • clickablePointerInputNode
  • semanticsSemanticsNode

각 Node는 자신이 담당하는 렌더링 단계에만 참여하기 때문에 Modifier 간 결합도가 낮아지고 구조적 안정성이 높아진다.

Node API의 주요 장점

1) Modifier 간 간섭 최소화

레이아웃, 입력, 그리기 로직이 역할별 Node로 분리되므로 복잡한 Modifier 체인에서도 사이드 이펙트가 줄어든다.

2) 성능 및 메모리 효율 개선

Node는 필요한 기능만 포함하기 때문에 불필요한 연산을 줄이고 Compose 내부 최적화가 더 쉬워진다.

3) ModifierLocal과 자연스러운 통합

ModifierLocal을 통해 Modifier 간 데이터를 공유하는 기능이 Node 구조와 결합되며 훨씬 더 안정적으로 동작한다.

4) Compose Multiplatform 기반 통일화

Node 기반 Modifier는 Android, Desktop, Web Compose에서 공통적으로 사용할 수 있는 구조로 설계되었다. → 플랫폼 간 차이를 줄이고 멀티플랫폼 개발에 일관성을 제공한다.

Node 기반 Modifier 예시

Modifier
    .then(CustomLayoutModifier())

이와 같이 커스텀 Modifier를 만들면 내부적으로 LayoutNode를 생성하고, 해당 Node는 Compose 렌더링 파이프라인에서 Layout 단계에만 참여하게 된다.

참고: Create custom modifiers — developer.android.com

3. Modifier 순서의 중요성

Modifier의 적용 순서에 따라 UI 결과가 완전히 달라진다. Compose는 Modifier를 적용 순서대로 해석한다.

// 예시 1
Modifier
    .padding(16.dp)
    .background(Color.Gray)

// 예시 2
Modifier
    .background(Color.Gray)
    .padding(16.dp)

1️⃣ padding → background 배경 밖으로 16dp 여백 생김

2️⃣ background → padding 배경 안쪽에 16dp 여백 생김

레이아웃 관련 Modifier는 앞쪽에, 시각적 Modifier는 뒤쪽에 배치하는 것이 권장된다. 이 규칙은 Compose 공식 가이드라인에 명시되어 있다.

4. Modifier와 성능 (Recomposition)

4.1 Modifier 객체 생성과 재구성

Compose는 상태가 변경될 때 관련 Composable을 재구성한다.

이 때 Modifier가 매번 새 객체로 생성되면, 불필요한 Recomposition이 발생할 수 있다.

// ❌ 매 호출마다 다른 Modifier 생성
Text(
    text = "Hello",
    modifier = Modifier.padding(Random.nextInt(4).dp)
)

4.2 성능 최적화 전략

  • Modifier 재사용 : remember { Modifier.padding(8.dp) } 로 객체 캐싱
  • Stable 객체 유지 : 불필요한 .composed {} 호출 지양
  • Layout Inspector 활용 : Recomposition 횟수 시각화 및 병목 지점 분석

5. Modifier를 활용한 UI 일관성 확보

5.1 디자인 시스템 구현

디자인 시스템은 색상, 간격, 모서리 반경 등의 토큰 일관성을 요구한다. Modifier는 이러한 토큰을 코드 레벨에서 표현하는 효율적인 수단이다.

object AppModifiers {
    val Card = Modifier
        .clip(RoundedCornerShape(12.dp))
        .background(Color.White)
        .padding(16.dp)

val Button = Modifier
        .height(56.dp)
        .fillMaxWidth()
        .clip(RoundedCornerShape(8.dp))
}

Design Token — Modifier 표현 Spacing 16dp — Modifier.padding(16.dp) Radius 12dp — Modifier.clip(RoundedCornerShape(12.dp)) Surface — Modifier.background(AppColors.surface)

6. Modifier 확장 및 커스텀 Modifier 제작

6.1 Modifier.composed 사용 및 한계

과거에는 Modifier.composed 가 커스텀 Modifier 구현에 널리 쓰였으나, 현재는 Node API 도입으로 권장되지 않는다.

fun Modifier.shakeOnClick() = composed {
    val offset = remember { Animatable(0f) }
    this
        .clickable {
            offset.animateTo(10f)
            offset.animateTo(0f)
        }
        .graphicsLayer { translationX = offset.value }
}

⚠️ Modifier.composed 는 Recomposition 스코프에 직접 접근하므로, 성능 문제를 일으킬 수 있다. 가능하면 Modifier.Node 또는 draw/measure 확장 함수를 사용하는 것이 좋다.

6.2 상태를 가진 Modifier 설계 시 주의점

  • Modifier.composed 내의 상태는 해당 Composable 수명에 종속된다.
  • 장기 상태(long-lived state)는 ViewModel 또는 State Hoisting으로 분리한다.
  • Stateless Modifier를 기본값으로 두고, 상태 관리는 UI 외부로 위임한다.

7. 협업 및 코드 컨벤션

7.1 함수 시그니처 컨벤션

Compose API 가이드라인은 다음 순서를 권장한다.

Required → Modifier → Optional parameter→ Trailing Lambda

@Composable
fun AppButton(
    onClick: () -> Unit, // Required
    modifier: Modifier = Modifier, // 첫번째 optional parameter - modifier
    enabled: Boolean = true, // optional parameter
    content: @Composable () -> Unit, // trailing Lambda
)

출처: Compose API Guidelines

7.2 협업 시 리뷰 포인트

  • Modifier 순서가 UI 결과에 영향을 주는지 확인하기 ex) padding → backgroundbackground → padding 은 렌더링 결과가 완전히 다르다.
  • 불필요한 중복 Modifier 제거하기 ex) padding().padding() 은 한 번으로 합칠 수 있다.
  • Modifier 안에 상태 로직이 섞이지 않았는지 검토하기 ex) 상태가 필요한 경우 Modifier.composed 대신 state hoisting, derivedStateOf 등 외부 구조를 우선 고려한다.

8. 잘못된 사용 패턴 및 안티패턴

(문제 → 원인 → 해결 순으로 정리)

  1. padding().padding() 중복 사용
  • 문제: 동일 연산이 두 번 발생해 불필요한 측정 비용 증가
  • 원인: Modifier 체이닝 시 중복된 연산을 인지하지 못함
  • 해결: 하나의 padding() 으로 통합
  1. fillMaxSize().width(100.dp) 같이 상충되는 지시
  • 문제: 서로 다른 크기 지시가 충돌
  • 원인: 하나의 컴포저블에 “모든 공간 사용” + “고정 너비”를 동시에 적용
  • 해결: 의도에 맞는 단일 Modifier만 적용
  1. Modifier.composed 남용
  • 문제: 불필요한 Recomposition 증가
  • 원인: 간단한 Modifier 로직을 composed로 감싸는 패턴
  • 해결: Stateless Modifier → 필요할 때만 composed 사용 원칙 유지
  1. remember를 Modifier 내부에 넣는 패턴
  • 문제: 상태가 Modifier 내부로 숨겨져 예측 불가능한 UI 동작
  • 원인: State hoisting 미사용
  • 해결: 상태는 외부로 분리하고 Modifier는 가능한 한 순수 함수로 유지

9. 한계 및 향후 방향

Modifier는 불변성 기반 구조 덕분에 매우 가볍고 안전하지만, 이 불변성 때문에 상태 기반 인터랙션에는 제약이 있었다. 이를 해결하기 위해 Compose 팀은 다음 두 가지 개념을 도입했다.

ModifierLocal

  • Modifier 간에 데이터를 전달할 수 있는 메커니즘
  • 기존 CompositionLocal은 트리 전체에 영향을 주지만, ModifierLocal은 Modifier 체인 내부에서만 scope가 제한됨

Node API

  • Modifier가 실제로 어떻게 노드로 변환되는지를 개발자가 통제할 수 있는 저수준 API
  • 컴파일러/런타임 최적화에 중요한 역할을 하며, Compose Desktop/Web에서도 동일한 아키텍처를 사용할 수 있도록 확장 중

향후 Compose 릴리스에서는 이 Node 기반 구조가 Android · Desktop · Web Compose 모두에서 통일된 아키텍처로 동작할 예정이다.

결론

Modifier는 Jetpack Compose가 추구하는 불변성, 선언성, 조합성을 가장 잘 드러내는 핵심 요소이다. Modifier를 단순한 “속성 집합”으로 보지 않고, Compose 문법의 한 단위로 이해하는 순간 UI 구성, 성능, 유지보수성, 협업 효율성까지 모두 한 단계 성장한다.

[참고 자료]

Modifiers in Jetpack Compose | Android Developers

Create custom modifiers | Android Developers

Compose API Guidelines | android.googlesource.com

Performance Tips for Compose (2023)

Android Developers Blog — Deep Dive into Modifiers and Nodes (Google I/O 2023)


메타데이터
post_id
cddfe5afa3bb
slug
jetpack-compose-modifier-cddfe5afa3bb
url
https://medium.com/tecoble/jetpack-compose-modifier-cddfe5afa3bb
canonical_url
https://medium.com/tecoble/jetpack-compose-modifier-cddfe5afa3bb
author_url
https://medium.com/@devfeijoa
status
ok
fetched_at
2026-09-01 05:41:57