[안드로이드] Side Effect

말랑돌·2025년 1월 28일

안드로이드 학습

목록 보기
4/17
post-thumbnail

Side Effect?

  • 번역하면 부작용 혹은 부수효과 라고 불리며, 함수 내부와 외부와의 상호작용을 의미한다.

Side Effect의 정의

  • 순수 함수: 입력만으로 결과를 계산하며, 외부 상태에 영향을 미치지 않음.
  • 부작용(Side Effect): 함수가 외부 상태를 변경하거나 외부 요인에 의존할 때 발생

예시 : 전역 변수 값 변경, 파일 읽기/쓰기, 네트워크 요청, 데이터베이스 상태 변경, UI 업데이트, 예외 발생



Jetpack Compose에서 Side Effect 처리 함수

LaunchedEffect

  • Composable 함수가 리컴포지션 될 때 사용할 suspend fun(정지 함수/비동기 작업) 실행환경을 제공하는 스코프이다.

  • 해당 함수는 리컴포지션 될때마다 실행되는 것이 아닌 key가 변경될 때마다 실행된다.

  • 매개변수로 1~N개의 key를 넣을 수 있으며, 만약 매개변수를 넣지 않거나 Unit을 입력 시 최초 한 번(첫 컴포지션) 때만 실행되고, 이후엔 재실행되지 않는다.

@Composable
fun MyScreen() {
    var text by remember { mutableStateOf("초기 상태") }

    LaunchedEffect(Unit) {
        delay(2000)
        text = "변경된 상태" // 상태 업데이트
    }

    Text(text = text) // 상태 기반으로 UI 업데이트
}

DisposableEffect

  • 컴포저블이 활성화될 때 실행될 행동과, 컴포저블이 비활성화될 때 실행될 행동을 정의할 수 있는 스코프이다.

  • DisposableEffect의 실행은 첫 컴포지션이 이루어 질때와 Key가 변경될 때 실행된다. 즉, 리컴포지션이 발생한다고 무조건 실행되는 것은 아니다.

  • DisposableEffect의 onDispose는 key가 변경되지 않아도, 컴포저블이 비활성화되면 무조건 실행된다.

@Composable
fun MyScreen(show: Boolean) {
    if (show) {
        DisposableEffect(Unit) {
            println("DisposableEffect 시작")

            onDispose {
                println("컴포저블 비활성화됨 - onDispose 호출")
            }
        }
    }
}

  • 정지함수나 비동기작업을 지원하지 않는다. 동기적인 리소스 정리 목적의 함수이다.
  • 스코프 내부에서 정지함수를 사용하고 싶다면 다음과 같이 사용한다.
@Composable
fun MyScreen() {
    val coroutineScope = rememberCoroutineScope()

    DisposableEffect(Unit) {
        val job = coroutineScope.launch {
            someSuspendFunction() // 정지 함수 실행
        }

        onDispose {
            job.cancel() // 코루틴 취소 및 리소스 정리
        }
    }
}

SideEffect

  • Compoable 함수 외부의 상태를 작업 하기 위한 스코프이다.

  • 동기적인 함수이다.

  • Compoable이 리컴포지션 될 때마다 실행된다.

  • Compoable 내부의 상태는 Compoable 자체가 수행할 수 있으므로, SideEffect에서는 Compoable에서 관리하지 않는 외부 상태만 작업한다.

  • 주요 사용처 : 로그 기록, Compose 외부 시스템 상태 업데이트, 외부 객체와의 상태 동기화

//로그 기록
@Composable
fun LogExample(name: String) {
    SideEffect {
        println("현재 이름: $name")
    }
    Text(text = "Hello, $name!")
}

//Compose 외부 시스템 상태 업데이트
@Composable
fun UpdateViewExample(counter: Int, externalView: TextView) {
    SideEffect {
        externalView.text = "Counter: $counter"
    }
}

//외부 객체와의 상태 동기화
@Composable
fun SynchronizeStateWithExternalSystem(state: String, externalSystem: ExternalSystem) {
    SideEffect {
        externalSystem.updateState(state)
    }
}

rememberCoroutineScope

  • 컴포저블의 생명주기와 연동: 컴포저블이 제거되면 스코프 내의 모든 코루틴이 취소된다. (LaunchedEffect와 같다.)

  • 비동기 작업 실행: 비동기 작업(정지 함수 호출 등)을 관리하기 위해 사용된다.(LaunchedEffect와 같다.)

  • 재구성 시 스코프 재생성 없음: 리컴포지션이 발생해도 이전에 생성된 스코프를 재사용한다. (LaunchedEffect와 다르다.)

  • LaunchedEffect와의 차이점
    • LaunchedEffect :
      -컴포저블이 실행될 때만 상태를 인식하여 작동함.
      -컴포저블 내부에서만 사용 가능(onClick{ }과 같은 람다 안에서 사용 불가)
    • rememberCoroutineScope :
      -따로 변수를 만들거나, 매개변수로 넘겨줄 수 있음
      -onClick에 사용하여, 이벤트 발생 시에 작동할 수 있음.
@Composable
fun UpdateExternalView(externalView: TextView) {
    val coroutineScope = rememberCoroutineScope()

    Button(onClick = {
        coroutineScope.launch {
            delay(500)
            externalView.text = "Updated after delay!"
            Toast.makeText(context, "executed in SideEffectScreen Lifecycle", Toast.LENGTH_SHORT).show()
        }
    }) {
        Text("Update TextView")
    }
}

rememberUpdateState

  • 상태변경을 추적하는 함수

  • remember{ mutableState(초기값) }은 state를 만들고 캐싱하여 상태를 유지한다. 값을 변경하려면 value를 사용하여 변경해야지 적용된다.

  • 하지만 만약 초기값으로 함수의 매개변수를 넣게 된다면, remember은 이를 추적하지 않으므로, 매개변수 값이 변해도 value가 변경되지 않는다.

@Composable
private fun ResultText(result: Int) {
    //초기의 result로 state가 만들어지고, result가 아무리 변해도 변수값은 변경되지 않음  
    val rememberResult by remember { mutableIntStateOf(result) } 

  • rememberUpdateState는 remember에서 초기화과정을 거친다.
@Composable
fun <T> rememberUpdatedState(newValue: T): State<T> = remember {
    mutableStateOf(newValue)
}.apply { value = newValue }

  • 덕분에 상위 컴포저블에서 state값이 변하면, 하위 컴포저블에서도 state값을 변경시킬 수 있다.(최신값으로 동기화 할 수 있는 방법)
  • 일반적인 리컴포지션에서는 기존 값을 유지하며, state 값이 변경될 때만 value를 바꿀 수 있는 방법이다.

produceState

  • produceState는 비동기적으로 데이터를 로드하고, 해당 데이터를 상태로 관리할 수 있도록 해주는 State 객체를 반환

  • 비동기 작업이 완료되면 상태를 자동으로 업데이트하여 UI가 최신 상태를 반영할 수 있게 돕는다.

  • 컴포지션 및 리컴포지션 때마다 실행 : 상태값이 바뀌지는 않아도, 비동기 작업으로 인한 자원소모가 될 수 있음

  • produceState는 두 개의 매개변수를 받는다:

    • initialValue: 초기값을 제공 (상태의 초기 값)
    • producer: 비동기 작업을 수행하는 코드 블록{}으로, 여기서 비동기 작업을 시작하고, 해당 작업이 완료되면 결과로 상태를 업데이트한다.
@Composable
fun FetchNetworkData() {
    val networkData by produceState(initialValue = "Loading...") {
        // 네트워크 요청 시뮬레이션
        val data = fetchDataFromNetwork()  // 비동기 네트워크 요청 함수
        value = data  // 결과 값 업데이트
    }

    Text(networkData)  // 상태를 텍스트로 표시
}

// 비동기 네트워크 요청 함수
suspend fun fetchDataFromNetwork(): String {
    delay(3000)  // 네트워크 요청 시뮬레이션
    return "Fetched Network Data!"
}

snapshotFlow

  • Compose 상태를 Flow로 변환 : Compose의 상태를 읽고, 상태가 변경될 때마다 Flow가 새로운 값을 발행

  • 읽기 작업만 지원 : snapshotFlow 내부에서 Compose 상태를 읽기만 가능하며, 상태를 수정하는 작업은 허용되지 않는다.

  • Compose 상태 변경을 비동기적으로 처리 : UI 상태가 변할 때 이를 Flow를 통해 비동기 작업(예: 네트워크 요청, 데이터 로깅 등)과 연결할 수 있다.

  • snapshotFlow 내부에서 외부 상태를 읽는 경우 일관성 문제가 발생할 수 있음.

  • snapshotFlow는 Compose 상태 변경을 감지하는 데 특화되어 있으므로, 다른 일반적인 Flow 작업에는 적합하지 않다.

@Composable
fun SnapshotFlowExample() {
    val count = remember { mutableStateOf(0) }

    // count 상태를 Flow로 변환
    val countFlow = snapshotFlow { count.value }

    // Flow 수집
    LaunchedEffect(Unit) {
        countFlow.collect { value ->
        	//값이 변경될 때마다 Flow를 관찰하여 로그를 출력
            Log.d("SnapshotFlow", "Count value: $value")
        }
    }

    // UI
    Column {
        Text("Count: ${count.value}")
        Button(onClick = { count.value++ }) {
            Text("Increment")
        }
    }
}

0개의 댓글