관리 메뉴

나만을 위한 블로그

[Android Compose] LaunchedEffect란? 본문

Android

[Android Compose] LaunchedEffect란?

참깨빵위에참깨빵 2023. 7. 14. 22:35
728x90
반응형

https://developer.android.com/jetpack/compose/side-effects?hl=ko 

 

Compose의 부수 효과  |  Jetpack Compose  |  Android Developers

Compose의 부수 효과 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 부수 효과는 구성 가능한 함수의 범위 밖에서 발생하는 앱 상태에 관한 변경사항입니다.

developer.android.com

Composable 안에서 안전하게 정지 함수를 호출하려면 LauncedEffect Composable을 사용하라. LaunchedEffect가 컴포지션을 시작하면 매개변수로 전달된 코드 블록으로 코루틴이 실행된다. LaunchedEffect가 컴포지션을 종료하면 코루틴이 취소된다. LaunchedEffect가 다른 키로 재구성되면 기존 코루틴이 취소되고 새 코루틴에서 정지 함수가 실행된다. LaunchedEffect는 Composable 함수기 때문에 Composable 안에서만 쓸 수 있다

 

Compose의 LaunchedEffect는 suspend fun으로 선언된 코루틴 함수를 호출하기 위해 필요한 컴포저블 함수다.

위 링크로 들어가면 부수 효과에 대해 설명하고 있다. 부수 효과는 컴포저블 함수의 밖에서 발생하는 앱 상태에 대한 변경사항이다. 컴포저블에는 정말 필요한 게 아니라면 부수 효과는 없는 게 낫다.

LaunchedEffect를 쓰는 이유는 공식문서에서도 말하듯이 코루틴을 사용하기 위한 suspend function을 호출하기 위해 필요하다. XML + 코틀린 조합으로 뷰와 로직을 작성할 때는 suspend 키워드와 코루틴 빌더만 신경쓰면 됐지만 Compose에선 LaunchedEffect도 고려해야 한다.

아래는 LaunchedEffect를 사용한 간단한 예시 코드다.

 

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import com.example.composetodo.ui.theme.ComposeTodoTheme
import kotlinx.coroutines.delay

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

    @Composable
    fun LaunchedEffectTest() {
        val data = remember { mutableStateOf("로딩 중...") }

        LaunchedEffect(key1 = Unit) {
            data.value = loadData()
        }

        Text(text = data.value)
    }

    suspend fun loadData(): String {
        delay(2000L)
        return "데이터 로딩 완료"
    }
}

 

loadData()는 delay()를 통해 2초간 기다렸다가 "데이터 로딩 완료" 문자열을 리턴하는 일시정지 함수다. 서버 통신을 흉내내기 위해 2초 정도의 딜레이를 주었다.

LaunchedEffectTest() 안에선 remember {}와 mutableStateOf()를 써서 data 변수에 "로딩 중" 이라는 문자열을 처음 대입한다. 이후 LaunchedEffect {}를 호출해서 data 변수에 loadData()의 리턴값을 세팅하고 텍스트 컴포저블 함수를 호출해서, 그 함수의 매개변수로 data 변수를 넘긴다.

이 상태로 앱을 빌드해 실행하면 왼쪽 상단에 "로딩 중..." 문자열이 먼저 표시되고, 2초 뒤에 "데이터 로딩 완료" 문자열이 표시된다. LaunchedEffect {}가 잘 작동하는 것이다.

 

이제 LaunchedEffect의 매개변수가 뭘 의미하는지 확인해 본다.

 

https://developer.android.com/reference/kotlin/androidx/compose/runtime/package-summary#LaunchedEffect(kotlin.Any,kotlin.coroutines.SuspendFunction1) 

 

androidx.compose.runtime  |  Android Developers

androidx.car.app.managers

developer.android.com

@Composable
@NonRestartableComposable
fun LaunchedEffect(key1: Any?, block: suspend CoroutineScope.() -> Unit): Unit

@Composable
@NonRestartableComposable
fun LaunchedEffect(key1: Any?, key2: Any?, block: suspend CoroutineScope.() -> Unit): Unit

@Composable
@NonRestartableComposable
fun LaunchedEffect(key1: Any?, key2: Any?, key3: Any?, block: suspend CoroutineScope.() -> Unit): Unit

@Composable
@NonRestartableComposable
fun LaunchedEffect(vararg keys: Any?, block: suspend CoroutineScope.() -> Unit): Unit
LaunchedEffect가 컴포지션에 진입하면 컴포지션의 CoroutineContext로 블록을 시작한다. LaunchedEffect가 다른 key1로 재구성되면 코루틴이 취소되고 재시작된다. LaunchedEffect가 컴포지션을 벗어나면 코루틴이 취소된다. 이 함수는 key1에 전달된 MutableState에 콜백 데이터를 저장하는 방식으로 콜백 이벤트의 응답으로 진행 중인 작업을 (재)실행하는 데 사용해선 안 된다. 대신 이벤트 콜백에 대한 응답으로 컴포지션으로 범위가 지정된 진행 중인 작업을 시작하는 데 사용할 수 있는 CoroutineScope를 얻으려면 rememberCoroutineScope를 참조하라

 

key1 매개변수에 아무것도 전달하지 않으려면 null이나 Unit을 넘기면 된다. 그래서 위의 예시 코드에선 뭔가 전달할 것이 없기 때문에 Unit을 넣었다. null을 넣더라도 똑같이 작동한다. 그냥 취향 차이다.

 

또한 key1부터 key3은 모두 특정 값이 변경될 때 LaunchedEffect를 재사용하기 위한 메모이제이션 키로 사용된다. 3개의 값을 설정했을 경우, 하나라도 변하면 LaunchedEffect 안의 코드 블럭이 재실행된다.

만약 2가지 중 어떤 값이라도 변경이 일어나면 일시정지 함수를 호출해야 할 경우 아래와 같이 코드를 구성할 수 있다.

 

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import com.example.composetodo.ui.theme.ComposeTodoTheme
import kotlinx.coroutines.delay

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            ComposeTodoTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colors.background
                ) {
                    LaunchedEffectTest(10, "김영희")
                }
            }
        }
    }

    @Composable
    fun LaunchedEffectTest(userId: Int, userName: String) {
        var userProfile by remember { mutableStateOf(UserProfile(1, "김철수")) }

        LaunchedEffect(key1 = userId, key2 = userName) {
            userProfile = loadData(userId, userName)
        }

        Text(
            text = "userId : ${userProfile.userId}\nuserName : ${userProfile.userName}"
        )
    }

    suspend fun loadData(userId: Int, userName: String): UserProfile {
        delay(2000L)
        return UserProfile(userId, userName)
    }

    data class UserProfile(
        val userId: Int,
        val userName: String
    )
}

 

대충 data class하나 만들고 LaunchedEffectTest()의 매개변수로 이 data class의 필드들을 받도록 설정하고 여차여차해서 Text 컴포저블에 표시하는 예제다.

다른 자잘한 변화는 치우고 LaunchedEffect {}를 잘 보면, key1과 key2에 각각 userId, userName을 지정했다. 또한 후행 람다 안에서 loadData()에 해당 userId, userName을 넘겨서 새로 데이터를 요청한다.

즉 userId 또는 userName 값이 변하면 loadData()를 호출해서 userProfile의 필드들에 담긴 값들을 10, "김영희"로 변경한다.

 

추가로 LaunchedEffect의 형태 중 1번 매개변수로 가변 인자인 vararg를 넘길 수도 있는 걸 볼 수 있는데, 이 말은 key 값들을 무제한으로 줄 수 있다는 뜻이다.

가변 인자를 사용하게 된다면 key1 같은 이름 있는 매개변수를 쓸 필요 없이 쭉 나열하면 된다.

 

@Composable
fun LaunchedEffectTest(userId: Int, userName: String, isRefreshNeed: Boolean) {
    var userProfile by remember { mutableStateOf(UserProfile(1, "김철수")) }

    LaunchedEffect(userId, userName, isRefreshNeed) {
        userProfile = loadData(userId, userName)
    }

    Text(
        text = "userId : ${userProfile.userId}\nuserName : ${userProfile.userName}"
    )
}

 

위 코드의 경우 userId, userName, isRefreshNeed 3가지 중 하나라도 값이 바뀌면 LaunchedEffect {}가 실행된다.

반응형
Comments