새로운 프로젝트의 테마 부분을 구현 하다가 그냥 상태 전달 용으로 별 생각없이 사용했던 그냥 매개변수로 넘겨주면 되지 다른 이유가 있나? 라고 생각이 들어서 CompositionLocal에 대해 좀 더 자세히 공부해봐야겠다고 생각했다.
결론만 말하자면 CompositionLocal이 아닌 매개변수로 넘겨줘도된다.
하지만 그렇게되면 하위에 모든 곳에 매개변수로 넘겨주면서 해당 상태가 필요없는 곳 까지 해당 코드를 매개변수로 가지고 있어야 하는 의미 없이 파이프 역할만하는 코드가 많이 생긴다는 것이 문제이다.
아래 글을 참고하면서 작성하였다.
https://www.charlezz.com/?p=46403
컴포저블 함수는 컴포넌트 기반의 UI를 구성할 때 트리 구조로 조직됩니다. 이 구조에서 상태 호이스팅은 상태를 트리의 가능한 가장 상위 노드에 선언하는 패턴을 말합니다. Jetpack Compose를 포함한 많은 UI 프레임워크에서 이 방법을 권장하는데, 그 이유는 다음과 같습니다.
1. 재사용성 향상
상태를 하위 컴포넌트가 아닌 상위 컴포넌트에 선언함으로써, 해당 상태를 필요로 하는 다른 컴포넌트들이 쉽게 접근하고 사용할 수 있게 됩니다. 이는 컴포넌트의 재사용성을 향상시키며, 여러 곳에서 같은 상태에 의존하는 UI를 구성할 때 효과적입니다.
2. UI의 일관성 유지
상태를 공유하는 여러 컴포넌트들이 하나의 상태 소스를 참조함으로써, UI의 일관성을 쉽게 유지할 수 있습니다. 상태가 변경될 때 해당 상태에 의존하는 모든 컴포넌트들이 자동으로 업데이트되므로, UI의 동기화 문제를 방지할 수 있습니다.
3. 상태 관리의 단순화
상태를 상위에서 관리함으로써 상태 변경 로직을 한 곳에 집중시킬 수 있습니다.
이는 코드의 가독성을 향상시키고, 상태 변경에 따른 부수 효과(Side-effects)를 관리하기 더 쉽게 만듭니다. 상태 변경이 발생했을 때, 그에 따른 부수 효과를 처리하는 로직도 중앙에서 관리할 수 있습니다. 예를 들어, 사용자의 로그인 상태를 관리하는 기능이 있고, 로그인 상태에 따라 다른 동작이 이루어져야 할 때
4. 유연한 상태 공유 및 업데이트
상태 호이스팅을 통해 상위 컴포넌트에서 하위 컴포넌트로 상태를 전달하면, 하위 컴포넌트는 해당 상태를 읽기 전용으로 사용하거나, 필요에 따라 상태 업데이트 함수를 통해 상태를 변경할 수 있습니다. 이는 하위 컴포넌트가 상태를 어떻게 사용할지에 대한 유연성을 제공합니다. 각 화면(로그인 화면, 메인 화면 등)에서 로그인 상태와 관련된 로직을 따로 관리한다면, 로그인 상태 변경에 따른 동작들(데이터 로딩, 화면 이동 등)을 각 화면에서 개별적으로 처리해야 합니다.
이는 중복 코드 및 일관성 유지에 어려움이 있고 디버깅시에도 어려워질 수 있습니다.
5. 테스트와 디버깅의 용이성
상태와 관련된 로직이 상위 레벨에 집중되어 있을 때, 해당 로직을 테스트하고 디버깅하기가 더 쉽습니다. 상태 관련 문제가 발생했을 때, 상태를 관리하는 코드 부분을 먼저 살펴보면 문제의 원인을 빠르게 파악할 수 있습니다.
그러나 트리의 깊이가 깊어질수록 상태를 전달하기 위해 모든 중간 단계에 매개변수를 추가해야 하는 번거로움이 생깁니다. 예를 들어, 트리의 깊이가 100이라면, 상태를 최하위 노드까지 전달하기 위해 100개의 매개변수를 추가해야 합니다.
이러한 문제는 CompositionLocal을 활용하여 해결할 수 있습니다. CompositionLocal은 트리 구조에서 상태를 효율적으로 전달하고 공유할 수 있는 방법을 제공하여, 상태를 쉽게 관리하고 접근할 수 있게 해줍니다.
CompositionLocal은 컴포저블 트리 상위에서 선언된 상태를 하위에서 접근할 수 있는 방법을 제공한다.
그림처럼 Composable4 한정으로 CompositionLocal의 범위를 지정했다면, Composition4,5,6만 선언된 상태에 접근할 수 있다. 즉, 트리 구조상의 위치에 따라 접근 가능한 데이터가 결정됩니다. CompositionLocal의 값을 제공하는 컴포저블 함수 내부에서만 해당 값을 사용할 수 있으며, 이는 하위 컴포저블 함수들에게도 동일하게 적용됩니다.
이를 통해 여러 계층에 걸쳐 파라미터를 전달하는 대신, 상위에서 한 번만 값을 설정해주면 하위 컴포저블 함수들이 필요할 때 해당 값을 접근하여 사용하면된다.
CompositionLocal을 정의할 때는 staticCompositionLocalOf 또는 compositionLocalOf 함수를 사용합니다.
// StaticProvidableCompositionLocal로 만들기
val staticCompositionLocal = staticCompositionLocalOf {
${상태}
}
// DynamicProvidableCompositionLocal로 만들기
val dynamicCompositionLocal = compositionLocalOf {
${상태}
}
상위 컴포저블 함수부터 하위까지 CompositionLocal을 제공하기 위해 다음과 같이 CompositionLocalProvider를 사용한다.
이를 통해 CompositionLocal 값을 CompositionLocalProvider를 호출한 곳의 하위 컴포지션에서 접근할 수 있게 됩니다.
예로 들어 하위로 전달하고자 하는 것이 빨간색(상태)이라고 가정한다면 다음과 같이 코드를 작성할 수 있다.
val ColorCompositionLocal = staticCompositionLocalOf {
Color.Blue // 기본값을 정의
}
@Composable
fun Composable1() {
...
CompositionLocalProvider(ColorCompositionLocal.provides(Color.Red)) {
Composable4()
}
}
@Composable
fun Composable4() {
...
}
CompositionLocalProvider 함수 내부 구현을 보면, CompositionLocal은 1개 이상 제공할 수 있다.
fun CompositionLocalProvider(
vararg values: ProvidedValue<*>,
content: @Composable () -> Unit)
CompositionLocalProvider로 감싼 하위 컴포저블함수들은 이제 CompositionLocal로부터 현재 값을 읽을 수 있다.## staticCompositionLocalOf와 compositionLocalOf의 차이
@Composable
fun Composable3() {
Text(
modifier = Modifier.background(color = ColorCompositionLocal.current), // 빨간색
text = "Composable3"
)
...
}
staticCompositionLocalOf() 는 주로 변화가 적은, 즉 자주 변경되지 않는 데이터를 저장하는 데 사용됩니다. 이 함수로 생성된 CompositionLocal에 저장된 데이터가 변경될 경우, 이 데이터를 사용하는 컴포저블 함수뿐만 아니라 해당 컴포저블의 모든 하위 컴포저블 함수들도 재구성됩니다. 이는 데이터 변경 시 상당히 많은 재구성이 발생할 수 있음을 의미합니다. 따라서, 정말로 변화가 적은 데이터에 사용하는 것이 바람직합니다.
반면, compositionLocalOf()는 변화가 빈번한 데이터를 저장하는 데 적합합니다. 이 함수로 생성된 CompositionLocal에 저장된 데이터가 변경되면, 해당 데이터를 직접 사용하는 컴포저블 함수만 재구성됩니다. 이는 필요한 부분만 재구성되어 성능 효율성을 높일 수 있습니다.
두 함수의 주된 차이는 재구성의 범위와 관련 있습니다. staticCompositionLocalOf()를 사용할 경우, 데이터 변경이 상대적으로 드물 때 전체 컴포지션 트리의 재구성이 필요할 수 있습니다. 이는 메모리 사용량이나 성능에 영향을 미칠 수 있습니다. compositionLocalOf()는 데이터 변경 시 해당 데이터에 의존하는 컴포저블만 재구성되므로, 성능에 미치는 영향이 더 적습니다.
그러면 무조건 영향을 미치는 곳만 재구성하는 compositionLocalOf 만이 이득이 아닌가? 라는 생각이 든다.
이에 관련된 부분을 많이 찾아봤지만 staticCompositionLocalOf의 이점은 딱히 나오지 않는다. 그냥 공식문서에서 적혀있듯이 변화가 적은 데이터에 staticCompositionLocalOf의을 사용하면 성능상 이점이 있다고만 표시되어있다.
그래서 유추해보건데 아마 compositionLocalOf는 해당 데이터가 영향을 주는 곳들을 추적하고 있으므로 메모리 사용량이 증가하지 않을까 라는게 나의 생각이다. 잦은 변화가 있으므로 캐싱 느낌으로 그 부분만 재구성하여 빠르게 변경이 필요할 때 compositionLocalOf를 사용하는 것 같다
반면, 변화가 적은 데이터에 대해 staticCompositionLocalOf는 메모리 사용을 최소화하는 대신, 변경 시 전체적인 재구성으로 인해 더 많은 시간이 소요하는 것 같다.
그래서 이 부분은 개발자의 판단으로 현재 상황을 고려해서 적절히 적용하면 될듯하다..?