Jetpack Compose 에서 절대로 해서는 안되는 20가지 실수 (16~20)

이지훈·2023년 10월 30일
1
post-thumbnail
post-custom-banner

서두

https://pl-coding.com/jetpack-compose-mistakes

이 글은 위의 Philip Lackner 님의 글을 번역 및 정리한 글 입니다.

이어서 끝까지 작성하도록 하겠습니다.

16. Composable 함수 내에서 람다의 이름을 지정하지 않는 것

Not naming Lambdas in Composables

람다는 Jepack Compose 에서 사용자의 액션에 반응하고 상태를 끌어올리는 것(hoist state)에 있어 주요한 역할을 수행합니다. 하지만 코틀린의 함수의 매개 변수 목록의 마지막 람다 함수를 인라인하는 기능은 코드를 읽을 수 없게 만들 수 있습니다.

// BAD
@Composable
fun LoginScreen() {
    VerticalScrollContainer(
        content = {
            // ...
        }
    ) {

    }
}

위의 코드를 보았을 때 후행 람다(trailing lambda) 함수가 언제 호출되고, 어떤 쓰임이 있는지 알려줄 수 있습니까?
그럴 것 같지는 않으므로 그 의미가 명확하지 않는 경우에는 람다에 이름을 지어줘야 합니다.

// GOOD
@Composable
fun LoginScreen() {
    VerticalScrollContainer(
        content = {
            // ...
        },
        onScroll = { scrollOffset ->

        }
    ) {

    }
}

이제는 후행 람다(trailing lambda)가 어떤 쓰임이 있는지 분명해졌습니다. 만약에 람다가 Composable content 를 넣는 경우에는 trailing lambda 형식이 더 분명할 수 있습니다.
만약에 람다가 콜백으로 쓰인다면 명명된 매개변수(named paramter)를 사용하는 것이 낫습니다.

kotlin 의 후행 람다(trailing lambda) 에 대한 더 자세한 내용은 아래 링크를 참고 해주세요
https://kmdigit.github.io/2020/05/14/study-with-oss-kotlin-trailing-lambda/

17. rememberCoroutineScope 를 잘 못 사용하는 것

Misusing rememberCoroutineScope

rememberCoroutineScope() 함수를 통해, 우리는 현재 Composition을 인식하는 coroutine scope를 얻을 수 있습니다. 하지만 많은 사람들이 잘못된 방법으로 사용하고 있습니다.

// BAD
@Composable
fun LoginScreen(
    viewModel: LoginViewModel,
    navController: NavController
) {
    val scope = rememberCoroutineScope()
    Button(onClick = {
        scope.launch {
            val result = viewModel.login() // Suspending function
            if (result == Result.SUCCESS) {
                navController.navigate("home")
            }
        }
    }) {
        Text(text = "Login")
    }
}

UI 와 관련되지 않은 suspend function 을 Composable 내에 coroutine scope 에서 사용하지 마세요. 당신의 뷰모델이 UI에게 suspend function 들을 노출 하면 안됩니다. 이 coroutine scope 는 화면 회전과 같은 구성 변경(configuration change) 이후에 취소 될 것이기 때문입니다.

로그인 함수 호출 역시 취소될 것 입니다.

// GOOD
@Composable
fun LoginScreen(
    viewModel: LoginViewModel,
    navController: NavController
) {
    LaunchedEffect(key1 = viewModel.isLoggedIn) {
        if (viewModel.isLoggedIn) {
            navController.navigate("home") {
                popUpTo("home") {
                    inclusive = false
                }
            }
        }
    }

    Button(onClick = { viewModel.login() }) {
        Text(text = "Login")
    }
}

그 대신에, 위와 같은 함수들은 viewModelScope 에서 실행하세요.
그리고 예시와 같이 suspend call 이 끝났을 때, 상태를 업데이트 하세요

Composable coroutine scope 에서는 UI 와 연관된 suspend function(예를 들면 애니메이션을 유발하거나, 스낵바를 보여주는) 만을 사용하세요.

18. 하위 Composable 과 graphicsLayer 의 State 를 빈번하게 변경하는 것

Frequently changing State in Sub-Composables and graphicsLayer

'7. GraphicsLayer 외부에서의 변환 애니메이션' 에서 이미 배운 바와 같이, Composable의 변환, 클리핑(테두리를 깎는) 또는 투명도(alpha)의 변화에 영향을 미치는 상태는 재구성(Recomposition)을 필요로 하지 않습니다. graphicsLayer modifier 를 사용할 수 있기 때문입니다.

하지만 이러한 상대의 빈번한 변화가 하위 Composable 에게 전달되게 된다면, 원하지 않은 많은 재구성(Recomposition)을 발생시킬 수 있습니다.

// 공통으로 사용하는 아이템 
@Composable
fun ListItem(
    alpha: Float,
    modifier: Modifier = Modifier
) {
    Text(
        text = "List item",
        modifier = modifier
            .padding(32.dp)
            .graphicsLayer {
                this.alpha = alpha
            }
    )
}
// BAD
@Composable
fun ListScreen() {
    val scrollState = rememberScrollState()
    Column(
        modifier = Modifier
            .fillMaxSize()
            .verticalScroll(scrollState)
    ) {
        for(i in 1..50) {
            ListItem(
                alpha = scrollState.value / 50f,
                modifier = Modifier.fillMaxWidth()
            )
        }
    }
}

사용자가 스크롤을 할 때마다, scollState 는 변화할 것이고, 이는 ListItem 의 재구성(Recomposition)을 유발할 것입니다. 상태가 변했기 때문입니다.

// GOOD
@Composable
fun ListScreen() {
    val scrollState = rememberScrollState()
    Column(
        modifier = Modifier
            .fillMaxSize()
            .verticalScroll(scrollState)
    ) {
        for(i in 1..50) {
            ListItem(
                alpha = { scrollState.value / 50f }, // <- 차이점 
                modifier = Modifier.fillMaxWidth()
            )
        }
    }
}

그 대신에 람다의 결과로서 상대가 전달된다면, ListItem Composable 의 composition 단계를 스킵하고 layout 단계로 바로 진입할 수 있습니다.
람다는 그 자체로 변화하지 않았고 그러므로 재구성(Recomposition)을 유발하지 않기 때문입니다.
이 것은 상태를 다른 Composable 함수로 내릴 때만 관려이 있습니다. ListItem 의 content 를 for문 내부에 인라인 했다면, 람다함수를 만들 필요가 없을 것 입니다.

19. Scaffold 의 content padding 을 사용하지 않는 것

Not Using content padding of Scaffold

Scaffold 는 주로 사용하는 Android UI 구성요소들을 화면에 적절하게 배치할 수 있게 도움을 주는 layout 입니다. navigation drawer, snackbar, toolbar 등이 그 예 입니다. 하지만 사람들이 자주하는 실수는 Scaffold 에서 제공되는 contentPadding 매개변수를 무시하는 것입니다.

// BAD
@Composable
fun LoginScreen() {
    Scaffold(
        modifier = Modifier.fillMaxSize()
    ) {
        Box(
            modifier = Modfier.fillMaxSize()
        ) {
            // Screen content
        }
    }
}

Scaffold 가 BottomAppBar 와 같은 전형적인 Scaffold 구성요소를 포함할 때, 위의 예시처럼contentPadding 을 무시할 경우 남은 공간의 크기가 줄어들 수 있습니다.

Scaffold 의 남은 공간을 고려하기 위해, 아래 코드와 같이 Scaffold 의 contentPadding 매개변수를 사용하세요.

// GOOD
@Composable
fun LoginScreen() {
    Scaffold(
        modifier = Modifier.fillMaxSize()
    ) { paddingValues ->
        Box(
            modifier = Modfier
                .fillMaxSize()
                .paddin(paddingValues)
        ) {
            // Screen content
        }
    }
}

Box Composable 에 paddingValues 를 적용 함으로써, Box의 content 가 Scaffold Composable 뒤로 감춰지는 것이 없게 되었습디다.(비교 화면 첨부 해야 함)

20. Composable 함수 내에서 return 을 수행 하는 것

Returning in Composable functions

return keyword 를 Composable 함수 내에서 사용하는 것을 피하세요. 이것은 Composition 단계가 스킵된 경우, 정의되지 않은 동작(행동)을 발생 시킬 수 있습니다.

// BAD
@Composable
fun LoginScreen(state: LoginState) {
    Column(
        modifier = Modifier.fillMaxSize()
    ) {
        EmailTextField(/* ... */)
        PasswordTextField(/* ... */)
        Text(
            text = state.LoginError ?: return@Column
        )
    }
}

어떤 형태의 return 이든 @Composable annotation 이 붙은 함수 내에서 사용하지 마세요.

// GOOD
@Composable
fun LoginScreen(state: LoginState) {
    Column(
        modifier = Modifier.fillMaxSize()
    ) {
        EmailTextField(/* ... */)
        PasswordTextField(/* ... */)
        state.loginError?.let { error ->
            Text(
                text = error
            )
        }
    }
}

null check 를 위한 let 스코프 함수나 if 문을 사용하는 것이 더 좋습니다.

Compose Compiler 1.4버전 부터 ealry return 을 수행하여도 앱이 죽지 않게 변경되었습니다.
https://velog.io/@beokbeok/Compose-early-return

profile
실력은 고통의 총합이다. Android Developer
post-custom-banner

5개의 댓글

comment-user-thumbnail
2024년 1월 28일

굳굳굳굳 잘 읽고 갑니다.
감사합니다 !!

1개의 답글
comment-user-thumbnail
2024년 10월 18일

번역해주신 것에 대해 감사히 읽었습니다.

그런데 관련 내용 중 궁금한게 있어서 논의를 위해 댓글을 남깁니다.

8번에서는 ViewModel을 파라미터 또는 screen 내부에 선언하는걸 피해서 사용하는 것을 권장하고
17번에서는 파라미터로 삽입이 된 상태인데 예시여서 그런걸까요?

아니면 8번의 경우가 우선시 되는 경우가 아닐까요..?

1개의 답글