[Jetpack Compose] CompositionLocal

단디·2024년 7월 16일

Compose

목록 보기
3/12
post-thumbnail

상태 호이스팅(State Hoisting)을 하게 된다면 상위 계층에서 상태를 관리하게 되고 하위 계층에 파라미터로 전달해서 사용할 수 있습니다. 만약, 계층의 여러 단계를 통해 상태를 전달해야 하는 경우 번거로울 수 있습니다. 하지만 컴포즈에서 제공하는 CompositionLocal을 사용한다면 이러한 문제를 해결할 수 있습니다.

CompositionLocal 이란?

CompositionLocal은 상위 계층의 컴포저블에서 선언된 상태를 하위 계층의 컴포저블에서 사용할 수 있도록 방법을 제공합니다. 모든 하위 계층에 상태에 대한 파라미터를 전달하지 않고도 가장 높은 노드에 선언된 상태를 하위 노드에서 이용할 수 있습니다.

따라서, CompositionLocal을 이용하면 값이 할당된 지점 하위의 컴포저블에서만 데이터를 이용할 수 있습니다.

기존 방식

compose-tree

Composable 3: 상태 관리

Composable 5: LazyListState에 대해서 접근할 필요가 없지만 하위 컴포저블인 Composition 8에게 전달하기 위해 상태값을 파라미터로 받음

Composable 8: 상위 컴포저블인 Composable 5로 부터 상태 값을 파라미터로 받음

CompositionLocal을 사용하는 경우

compositionlocal-tree

Composable 3: CompositionLocal로 상태 관리

Composable 5, Composable 7, Composable 8: Composable 3의 하위 컴포저블로 파라미터로 상태 값을 받지 않아도 상태에 접근할 수 있음

CompositionLocal 사용하기

CompositionLocal을 이용해서 상태를 선언하려면 ProvidableCompositionalLocal 인스턴스를 생성해야 합니다. 이 인스턴스를 얻기 위해서는 compositionLocalOf() 이나 staticCompositionLocalOf() 함수를 호출해서 얻을 수 있습니다.

ProvidableCompositionalLocal 인스턴스를 생성했다면 provides() 함수로 값을 할당합니다. CompositionLocalProvider 함수를 호출하여 생성한 인스턴스를 파라미터로 전달하고 하위 컴포저블도 파라미터로 전달하여 하위 컴포저블의 모든 자손에서 ProvidableCompositionalLocal 인스턴스의 현재 프로퍼티를 통해 CompositionLocal 상태에 접근할 수 있습니다.

val localNumber = compositionLocalOf{ 0 }
//val localNumber = staticCompostionLocalOf { 0 }

val number = 1

CompositionLocalProvider(localNumber provides number) {
		ChildComposable()
}
val childNodeNumber = LocalNumber.current

두 함수의 파라미터

fun <T> compositionLocalOf(
    policy: SnapshotMutationPolicy<T> =
        structuralEqualityPolicy(),
    defaultFactory: () -> T
): ProvidableCompositionLocal<T> = DynamicProvidableCompositionLocal(policy, defaultFactory)
fun <T> staticCompositionLocalOf(defaultFactory: () -> T): ProvidableCompositionLocal<T> =
    StaticProvidableCompositionLocal(defaultFactory)

compositionLocalOf()staticCompositionLocalOf() 함수 모두 defaultFactory라는 파라미터로 기본값에 대한 람다를 받고 있기 때문에 기본값을 정의할 수 있습니다.

두 함수의 차이점

compositionLocalOf()

  • 상태 값이 변경되면 현재 상태에 접근하는 컴포저블에 대해서만 재구성을 수행합니다.
  • 변경이 잦은 상태를 다룰 때 사용합니다.

staticCompositionLocalOf()

  • 상태값이 변경되면 해당 상태에 할당된 하위 컴포저블을 모두 재구성합니다.
  • 자주 변경되지 않는 상태를 다룰 때 사용합니다.

예제

위에서 봤던 이미지의 트리 형태로 Composable 함수로 트리를 구성했습니다.

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            GeminiSampleTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background,
                ) {
                    Composable1()
                }
            }
        }
    }
}

val LocalNumber = staticCompositionLocalOf { 1 }

@Composable
fun Composable1() {
    Column {
        Composable2()
        Composable3()
    }
}

@Composable
fun Composable2() {
    Composable4()
}

@Composable
fun Composable3() {
    Composable5()
}

@Composable
fun Composable4() {
    Composable6()
}

@Composable
fun Composable5() {
    Composable7()
    Composable8()
}

@Composable
fun Composable6() {
    Text("Composable 6")
}

@Composable
fun Composable7() {
    Text("Composable 7")
}

@Composable
fun Composable8() {
    Text("Composable 8")
}

Composable1 함수 위에 staticCompositionLocalOf() 을 호출하고 Composable1 안에서 provides() 함수로 값을 할당합니다. CompositionLocalProvider() 함수에 파라미터로 하위 컴포저블인 Composable3()와 함께 전달합니다.

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            GeminiSampleTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background,
                ) {
                    Composable1()
                }
            }
        }
    }
}

private val localNumber = staticCompositionLocalOf { 0 }

@Composable
fun Composable1() {
    Column {
        Composable2()

        CompositionLocalProvider(localNumber provides 0) {
            Composable3()
        }
    }
}

@Composable
fun Composable2() {
    Composable4()
}

@Composable
fun Composable3() {
    Composable5()
}

@Composable
fun Composable4() {
    Composable6()
}

@Composable
fun Composable5() {
    Composable7()
    Composable8()
}

@Composable
fun Composable6() {
    Text("Composable 6")
}

@Composable
fun Composable7() {
    Text("Composable 7")
}

@Composable
fun Composable8() {
    val number = localNumber.current
    Text("Composable 8: $number")
}

실행 결과

compositionlocal-run-one

Composable 3의 자손 컴포저블인 Composable 8에서 파라미터로 상태 값을 받지 않고도 상위 컴포저블에서 관리하는 상태에 접근할 수 있는 것을 알 수 있습니다.

예제2

컴포저블 3, 5, 7, 8에서 각각 다른 값을 불러오도록 구현할 수 있습니다. 각 컴포저블을 호출할 때 CompositionLocalProivder() 함수에서 다른 값을 할당하면 됩니다.

@Composable
fun Composable3() {
    val number = localNumber.current
    Text("Composable 3: $number")

    CompositionLocalProvider(localNumber provides 1) {
        Composable5()
    }
}

@Composable
fun Composable4() {
    Composable6()
}

@Composable
fun Composable5() {
    val number = localNumber.current
    Text("Composable 5: $number")

    CompositionLocalProvider(localNumber provides 2) {
        Composable7()
    }
    CompositionLocalProvider(localNumber provides 3) {
        Composable8()
    }
}

@Composable
fun Composable6() {
    Text("Composable 6")
}

@Composable
fun Composable7() {
    val number = localNumber.current
    Text("Composable 7: $number")
}

@Composable
fun Composable8() {
    val number = localNumber.current
    Text("Composable 8: $number")
}

실행 결과

compositionlocal-run-two

0개의 댓글