1. Modifier = composed {}

- 현재는 공식적으로 사용하지 않는 기능

- Modifier에서 remember, LaunchedEffect, derivedStateOf 등의 기능을 사용 가능

- 단, 리컴포지션할때마다 {} 람다가 계속 재실행되고, 이로 인해 매번 Modifier 객체를 새로 생성하는 오버헤드 발생

- 최근에는 위 오버헤드를 해결하기 위해 Modifier.Node 및 ModifierNodeElement 방식을 사용


2. Modifier.Node

- 상태를 가질 수 있음

- 리컴포지션 시 유지되어 재사용 가능

- 실제 기능을 구현하는 부분

  - 기능 구현 시 Node 클래스에 Modifier.Node와 추가로 필요한 기능들을 붙일 수 있음

  - 예를들어 그리기 기능이 필요한 경우 DrawModifierNode를, 레이아웃을 만드는 경우 LayoutModiferNode를 상속받아 구현


3. ModifierNodeElement

- 상태를 갖지 않음

- 리컴포지션 할때마다 객체가 새로 생성됨

- create(), update() 함수를 구현하는 불변 클래스

 

1) create()

- Modifier가 처음 적용되는 시점에 노드를 생성하기 위해 호출됨

 

2) update(node)

- 기존 위치에 동일한 Modifier가 사용되었으나, 속성이 변경되는 경우 호출됨

- 클래스의 equals() 메서드로 속성이 변경되었는지 비교하므로 ModifierNodeElement는 일반적으로 data class로 구현

  - data class는 equals() 메서드를 기본적으로 구현하므로

  - equals() 판단 조건을 세부적으로 구현해야 하는 경우 일반 class로 만들고 equals() 메서드 직접 구현

  - equals() 판단 조건을 제대로 구현하지 않으면 불필요한 update가 계속 발생함

- update 내에서 node를 새로 만들면 안되고, 기존의 노드를 갱신하는 방향으로 구현해야 함

  - 이를 위해 기존 노드객체가 매개변수로 전달됨

- 만약 Node에 매개변수가 하나도 없다면 update할 필요 없으므로 data class 대신 단일 인스턴스인 data object로 구현


4. inspectableProperties

- Android Studio의 Layout Inspector 디버깅 도구를 열어 특정 컴포저블을 클릭했을 때, 우측의 속성창에 이 Modifier의 이름과 매개변수 값을 시각적으로 보여주는 역할


5. Modifier Factory

- Modifier.Node와 ModifierNodeElement를 사용해 구현한 Modifier를 부여하기 위한 API


6. 코드 구현 예시

// 2. Modifier.Node를 상속받아 구현한 클래스
// - 상태를 가질 수 있음
// - 실제 기능을 구현하는 부분
class CustomTagNode(
    var tag: String // tag가 State 역할
) : Modifier.Node()

// 3. 노드의 생성과 업데이트를 관리하는 ModifierNodeElement
private data class CustomTagElement(
    val tag: String
) : ModifierNodeElement<CustomTagNode>() {

    // 최초 생성 시 호출됨
    override fun create(): CustomTagNode = CustomTagNode(tag)

    // 매개변수(상태) 변경 시 기존 노드 업데이트
    override fun update(node: CustomTagNode) {
        node.tag = tag
    }
    
    // 4. Layout Inspector 디버깅용 속성 정보 설정
    override fun InspectorInfo.inspectableProperties() {
        name = "customTag"           // Layout Inspector에 표시될 Modifier 이름
        properties["tag"] = tag      // Layout Inspector에 표시될 매개변수 및 값
    }
}

// 5. Modifier Factory
fun Modifier.customTag(tag: String): Modifier = this.then(CustomTagElement(tag))

- 특정 컴포저블의 Modifier에 .customTag를 붙이게 되면 해당 컴포저블이 처음 Attach될 때 Compose 프레임워크가 CustomTagElement의 create() 함수를 호출해서 노드 트리에 배치할 CustomTagNode 객체를 생성함

- 해당 컴포저블이 살아있는 동안 CustomTagNode도 함께 살아서 유지되며 기능을 수행함

- CustomTagNode의 매개변수는 State 역할을 하며, State 값이 바뀔때마다 CustomTagElement의 update 함수가 호출되어 Node 재생성 없이 기존의 Node를 갱신함

- 여러 컴포저블에 .customTag를 추가하면 추가한 갯수만큼 CustomTagNode 객체가 생성됨


7. 상속 가능한 Node의 종류

- Modifier.Node()와 함께 아래의 Node 클래스를 상속받아 추가적인 기능 구현 가능

- 대응되는 기능들은 간단하게 사용 가능한 고수준 람다 API이며, 더 세부적인 커스텀이나 최적화가 필요한 경우 아래 Node들을 상속받아 직접 구현

 

가. Layout 관련

1) LayoutModifierNode

- 레이아웃 측정(Measure) 및 배치(Layout) 제어

- Modifier.layout에 대응

 

2) ParentDataModifierNode

- 부모 레이아웃에 데이터 전달

- Modifier.weight()처럼 자식 컴포저블이 부모 레이아웃 측정에 데이터를 전달해줘야 할 때 사용

 

3) LayoutAwareModifierNode

- 레이아웃의 크기가 측정된 직후나 부모 레이아웃에 배치된 직후의 이벤트 감지

- 노드의 크기가 새롭게 측정되었을때, 부모 레이아웃 안에서 위치가 결정 되었을때 감지하여 호출됨

 

4) GlobalPositionAwareModifierNode

- 레이아웃 배치가 완료된 후 윈도우 기준 Node의 최종 위치/크기 감지

- Modifier.onGloballyPositioned에 대응

 

※ LayoutAwareModifierNode는 부모 레이아웃 내 상대적 위치 결정 시점을 감지하는 것이고, GlobalPositionAwareModifierNode는 윈도우 전체 기준의 절대 위치 변화를 감지하는 것

 

나. 그리기 관련

1) DrawModifierNode

- 레이아웃 공간에 그리기 작업을 수행

- Modifier.drawBehind나 drawWithContent에 대응

 

다. 입력 관련

1) PointerInputModifierNode

- 터치, 드래그, 마우스 등의 포인터 입력 처리

- pointerInput {}과 유사한 역할

 

2) KeyInputModifierNode / RotaryInputModifierNode

- 하드웨어 키보드 키 입력 및 Rotary 입력 처리

 

라. Focus 관련

1) FocusEventModifierNode / FocusPropertiesModifierNode / FocusRequesterModifierNode

- Focus 상태 감지 및 제어

- 포커스 이동/획득/상실 상태 감지 및 포커스 순서 변경 등 관리

 

마. Compose 관련

1) CompositionLocalConsumerModifierNode

- @Composable이 아닌 Modifier.Node 내부에서 CompositionLocal 데이터(LocalContext, LocalDensity 등) 접근

 

2) ObserverModifierNode

- Compose의 State 변화를 관찰하고 반응

- @Composable이 아닌 Modifier.Node 내부에서 특정 State 변경을 감지하고, State 변경 시 리컴포지션 없이 노드의 특정 로직을 수행할 수 있음

 

바. Modifier.Node 관련

1) TraversableNode

- Modifier Node 트리 간 통신/순회

- Node 트리 상하위에 위치한 다른 TraversableNode를 찾아 데이터를 공유하거나 이벤트 전달

 

2) DelegatingNode

- Modifier.Node를 만들 때 기존에 작성된 다른 Modifier.Node들의 기능을 조합해서 노드 위임을 구현할 수 있게 해주는 클래스

 

사. 접근성 관련

1) SemanticsModifierNode

- 접근성, semantics 정보 제공


8. 추가로 궁금했던 것들

1) Modifier = composed{}의 {} 내에서 remember를 써서 구현한 값들은 모두 Node의 매개변수로 대체할 수 있는건지?

- 답변 : 매개변수 또는 멤버변수로 대체 가능

- composed {}에서는 리컴포지션 시 상태를 유지하기 위해 remember가 필수였음

- 반면 Modifier.Node는 일반 클래스의 객체이므로 리컴포지션 시 그대로 유지됨

- 그러므로 Modifier.Node에서는 remember 없이 그냥 멤버변수로 선언하면 됨

// 1. composed {} 방식
fun Modifier.myModifier() = composed {
    val state = remember { MyState() } // remember 필요
}

// 2. Modifier.Node 방식
class MyNode(
    ... // 매개변수들
) : Modifier.Node() {
    val state = MyState() // remember 없이 그냥 멤버 변수로 선언하면 유지됨!
}

  

2) Modifier = composed{}의 LaunchedEffect는 다 update에서 구현하는 로직으로 대체 가능한건지?

- 답변 : 맞음

- Modifier.Node에는 기본적으로 coroutineScope 멤버변수가 있음

- 위 변수로 LaunchedEffect 내부 Scope 역할을 해서 기존 작업 cancel 후 launch 수행하면 됨

// 1. composed {} 방식
fun Modifier.customTagModifier(tag: String) = composed {
    LaunchedEffect(tag) {
        somthingFunc(tag)
    }
}


// 2. Modifier.Node 방식
class CustomTagNode(var tag: String) : Modifier.Node() {
    var job: Job? = null

    fun updateKey(newTag: String) { // LaunchedEffect 내부 구현부 역할
        job?.cancel()
        job = coroutineScope.launch {
            somthingFunc(newTag)
        }
    }
}

private data class CustomTagElement(
    val tag: String
) : ModifierNodeElement<CustomTagNode>() {

    ...
    
    override fun update(node: MyNode) {
        node.updateKey(key) // update 호출 시 Node 객체에 구현해둔 함수 호출
    }

}

 

 

3) Modifier = composed{}의 DisposableEffect, onDispose는 어떻게 대체할 수 있는지? 소멸시점에 수행할 동작을 정의 가능한지?

- 답변 : onAttach(), onDetach()로 대체 가능

- Modifier.Node에는 onAttach(), onDetach() 함수가 있음

- 위 두 함수를 override해서 해당 시점의 동작 정의

class CustomTagNode : Modifier.Node() {

    // UI 트리에 추가될 때 (DisposableEffect 시작 시점)
    override fun onAttach() {
        super.onAttach()
        ...
    }

    // UI 트리에서 제거될 때 (onDispose 시점)
    override fun onDetach() {
        super.onDetach()
        ...
    }
}