SideEffect란
SideEffect는 Composable의 Composition이 성공적으로 되었을 때 발행하는 Effect이다. SideEffect는 Compose에서 관리하지 않는 객체와 Compose 내부의 데이터를 공유하기 위해 사용한다.
SideEffect의 사용
TextField를 보이지 않게(isVisible=false) 했다가. 버튼이 눌렸을 때 보이게(isVisible = true) 만드는 코드가 있다고 하자. 이때 isVisible과 관련된 값은 모두 Compose에서 관리하는 값이므로 다음과 같이 작성이 가능하다.
@Composable
fun HomeScreen() {
var isVisible by remember { mutableStateOf(false) }
Column(modifier = Modifier.fillMaxSize()) {
Button(onClick = { isVisible = true }) {
Text(text = "버튼 클릭")
}
if (isVisible) {
OutlinedTextField(value = "", onValueChange = {})
}
}
}
이번에는 isVisible일 때 TextField에 Focus를 주는 코드를 만들어보자. 단순히 생각하면 TextField에 focusRequester을 붙여놓고 isVisible일 때 focusrequester.requestFocus()를 수행하면 된다.
@Composable
fun HomeScreen() {
var isVisible by remember { mutableStateOf(false) }
val focusRequester = remember { FocusRequester() }
Column(modifier = Modifier.fillMaxSize()) {
Button(onClick = { isVisible = true }) {
Text(text = "버튼 클릭")
}
if (isVisible) {
OutlinedTextField(
modifier = Modifier
.fillMaxSize()
.focusRequester(focusRequester),
value = "", onValueChange = {})
focusRequester.requestFocus()
}
}
}
하지만 위의 코드는 다음과 같은 오류를 내며 앱을 강제 종료시킨다.,
2021-12-20 16:59:28.049 10431-10431/com.simpli.compose E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.simpli.compose, PID: 10431
java.lang.IllegalStateException:
FocusRequester is not initialized. Here are some possible fixes:
1. Remember the FocusRequester: val focusRequester = remember { FocusRequester() }
2. Did you forget to add a Modifier.focusRequester() ?
3. Are you attempting to request focus during composition? Focus requests should be made in
response to some event. Eg Modifier.clickable { focusRequester.requestFocus() }
at androidx.compose.ui.focus.FocusRequester.requestFocus(FocusRequester.kt:54)
at com.simpli.compose.MainActivityKt.HomeScreen(MainActivity.kt:49)
at com.simpli.compose.MainActivityKt$HomeScreen$2.invoke(Unknown Source:4)
at com.simpli.compose.MainActivityKt$HomeScreen$2.invoke(Unknown Source:10)
여기서 주의 깊게 봐야할 부분은 이 부분이다.
java.lang.IllegalStateException: FocusRequester is not initialized. Here are some possible fixes:
바로 FocusRequester가 Initialized 안되었다는 것이다. Composable은 선언형 프로그래밍 패러다임을 따른다. 선언형 프로그래밍 패러다임은 Composable 구성 순서를 보장하지 않으며 결과만을 보장한다. 따라서 focusRequester가 초기화되기 전에 isVisible이 true로 됨으로 인해 focusRequester에 대한 requestFocus 요청이 들어갈 경우 initialized 되지 않았다는 오류가 뜬다.
위의 FocusRequester의 requestFocus는 Composable이 아닌 시스템의 이벤트이므로 Composable이 관리하는 이벤트가 아니다. 따라서 Composable의 구성이 완료된 후에 requestFocus가 호출되도록 보장하려면 다음과 같이 SideEffect를 사용해야 한다.
@Composable
fun HomeScreen() {
var isVisible by remember { mutableStateOf(false) }
val focusRequester = remember { FocusRequester() }
Column(modifier = Modifier.fillMaxSize()) {
Button(onClick = { isVisible = true }) {
Text(text = "버튼 클릭")
}
if (isVisible) {
OutlinedTextField(
modifier = Modifier
.fillMaxSize()
.focusRequester(focusRequester),
value = "", onValueChange = {})
}
}
SideEffect {
if (isVisible) {
focusRequester.requestFocus()
}
}
}
위 코드를 수행하면 다음과 같이 작동한다.
* 안드로이드 키보드의 타이밍 이슈로 인해 이 키보드가 올라오는 코드가 동작하지 않을 수 있다. 이런 경우 requestFocus 전에 일정 시간의 delay를 주면 해결되지만, 더욱 좋은 방법을 찾기를 바란다.
* 물론 이렇게 SideEffect를 사용하는 것은 권장되지 않는다. 이유는 아래에서 마저 설명한다.
SideEffect의 한계점
SideEffect는 Composable이 구성 완료되었을 때 실행된다. 하지만 리컴포지션이 일어날 때마다 수행되며 이는 LaunchedEffect에 key값이 들어가지 않았을 때의 동작과 같다. 또한 SideEffect로 수행하는 Effect는 Composable이 Dispose될 때 정리가 불가능하다.
따라서 SideEffect는 쉽게 사용될 수 있지만, LaunchedEffect와 DisposableEffect의 열화 버전이라 key 값에 따라 제어하거나 Composable이 Dispose될 때 정리되어야 하는 Effect에는 사용하기가 어렵다.
결론은 SideEffect는 LaunchedEffect나 DisposableEffect로 충분히 대체 가능하니 이 둘을 사용하도록 하자.
참조
1. LaunchedEffect : https://kotlinworld.com/246
2. DisposableEffect : https://kotlinworld.com/257