Composable에서 올바른 CoroutineScope을 선택하는 것이 중요한 이유
Composable 내부에서 코루틴을 수행할 경우 Composable에 대한 Recomposition이 일어날 때 정리되어야 하는 Coroutine이 정리가 안된 상태로 계속해서 Coroutine이 쌓일 수 있다. Recomposition은 자주 일어나는 동작이므로 Recomposition마다 Coroutine을 생성하는 것은 위험하며 심할 경우 앱 크래시를 발생시킬 수도 있다.
따라서 Composable에서 Coroutine을 생성한다면 Recomposition이 일어날 때 취소되어야 한다(꼭 그렇지 않은 경우도 있지만 그래야 하는 경우가 대부분이다). Compose는 이를 위해 Composable의 Lifecycle을 따르는 CoroutineScope을 반환하는 rememberCoroutineScope() 함수를 제공한다.
자 아래에서 Activity의 lifecycle을 따르는 Coroutine을 Composable에서 생성할 때의 문제점에 대해 살펴보고, 이것이 rememberCoroutineScope을 이용해 해결되는지 살펴보자.
예시를 위한 Screen
우리는 예시를 위해 다음 2개 Screen을 사용한다.
첫번째 Screen은 2개 버튼을 가지고 있다. 하나는 Button이 눌렸을 때 snackbar을 출력시키는 것이고, 다른 하나는 다른 Screen으로 넘어가는 버튼이다.
두번째 Screen은 간단한 Text("Another Screen")가 있는 Screen이다.
*코드는 접은 글에 있다.
Screen1 코드
@Composable
fun KotlinWorldScreen(
scaffoldState: ScaffoldState,
onScreenChange: () -> Unit,
coroutineScope: CoroutineScope = rememberCoroutineScope() // 인자가 안넘어오면 rememberCoroutineScope 사용
) {
Column() {
Button(
onClick = {
coroutineScope.launch {
scaffoldState.snackbarHostState
.showSnackbar("Show Snackbar!")
}
}
) {
Text("Show Snackbar")
}
Button(
onClick = {
onScreenChange()
}
) {
Text("Navigate to another Screen")
}
}
}
Screen2 코드
@Composable
fun NewScreen() {
Text(text = "Another Screen")
}
우리는 위 2개 Screen에서 Snackbar을 출력시키기 위해 lifecycleScope을 썼을 때랑 rememberCoroutineScope을 사용했을 때의 동작을 비교하면서 언제 각 lifecycleScope이 필요한지를 살펴볼 것이다.
lifecycleScope을 Compose에서 사용할 때의 문제점
우리는 activity에서 snackbar을 출력하는데 사용할 coroutineScope으로 Activity의 lifecycleScope을 넘겨보도록 하자.
*lifecycleScope은 Activity의 생명 주기를 따른다.
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val scaffoldState: ScaffoldState = rememberScaffoldState()
var isFirstScreen by remember { mutableStateOf(true) }
Scaffold(
scaffoldState = scaffoldState
) {
if (isFirstScreen) {
KotlinWorldScreen(
scaffoldState = scaffoldState,
onScreenChange = {
isFirstScreen = false
},
coroutineScope = lifecycleScope // lifecycleScope 넘기기
)
} else {
NewScreen()
}
}
}
}
}
위 코드는 생명주기가 Activity의 생명주기를 따르다보니, 첫번 째 Screen Composable이 파괴 될때도 위의 coroutine Job이 유지된다.
만약 버튼을 계속 클릭해서 Coroutine Job이 계속해서 쌓은다음 화면을 넘기면 어떻게 될까? lifecycleScope은 Activity의 onCreate에서 생성되고 onDestroy에서 파괴되는 생명주기를 가진 CoroutineScope 이므로 다른 Screen으로 넘어갔을 때도 snackbar 보여지는 것이 유지되고 계속해서 보여진다.
즉 다른 스크린으로 넘어가서 정지되어야 하는 Coroutine 작업임에도 정리가 안되고 있는 것을 확인할 수 있다. 이 문제를 해결하는 방법은 Composable의 생명주기를 따르는 rememberCoroutineScope을 사용하는 것이다.
rememberCoroutineScope의 동작
rememberCoroutineScope은 Composable의 생명주기를 따르는 CoroutineScope이다. 따라서 Composable이 파괴될 때 파괴되어야 하는 Coroutine은 rememberCoroutineScope의 범위에서 실행시켜 컴포저블이 파괴될 때 파괴해야 한다.
예를 들어 위 그림3의 Snackbar은 Screen이 넘어갈 때 파괴되어야 할 것으로 보인다. 위의 코드를 다음과 같이 rememberCoroutineScope을 사용하도록 바꿔보자.
*coroutineScope 인자를 넘기지 않으면 rememberCoroutineScope을 사용하도록 하는 것을 위의 접은 글 Screen1코드에서 확인할 수 있다.
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val scaffoldState: ScaffoldState = rememberScaffoldState()
var isFirstScreen by remember { mutableStateOf(true) }
Scaffold(
scaffoldState = scaffoldState
) {
if (isFirstScreen) {
KotlinWorldScreen(
scaffoldState = scaffoldState,
onScreenChange = {
isFirstScreen = false
}
//CoroutineScope 넘기지 않음
)
} else {
NewScreen()
}
}
}
}
}
rememberCoroutineScope은 Screen1 Composable의 생명주기를 따르다 보니, Screen1이 파괴될 때 rememberCoroutineScope에서 실행되는 코루틴 또한 취소된다.
따라서 Another Screen으로 넘어갔을 때 Snackbar가 사라지는 것을 그림4에서 확인할 수 있다.
정리
이 내용은 다음의 한 줄로 정리될 수 있다.
Composable이 파괴될 때 파괴되는 코루틴을 생성 해야될 때는 rememberCoroutineScope을 사용하도록 하자.