App 개발 일지/Nutri Capture

Nutri Capture 프론트엔드 - 무한 스크롤 로직 분리

interfacer_han 2024. 10. 12. 05:20

#1 개요

이전 게시글에선 불완전한 무한 스크롤을 구현했었다. 그 불완전함을 보완하는 코드를 작성하기 앞서, 먼저 기존 코드를 교통 정리하겠다. 첫째로는 초기 화면에서 보일 Item을 하나에서 20개로 늘린다. 초기 Item이 하나 뿐이면 아랫 방향 무한 스크롤 로직과 역방향(윗 방향) 무한 스크롤 로직이 동시에 작동하기에 이를 직관적으로 다루기 어렵기 때문이다. 20개라는 숫자는 Item들이 화면을 가득 채울만한 아무 숫자다. 특정 숫자로 하드 코딩하는 게 썩 내키지는 않지만, 일단 지금은 무한 스크롤의 문제를 해결하는 게 급선무다. 나중에 소프트 코딩으로 바꾸겠다.

 

또, 무한 스크롤 로직이 초기화가 완전히 완료된 후에 작동하도록 만들 것이다. 이를 위해 뷰 모델에 초기화 완료 정보를 담는 Boolean형 프로퍼티를 선언할 것이다.

 

#2 코드

#2-1 ViewModel에 isInitialized 프로퍼티 선언

...

class NutrientViewModel : ViewModel() {
    // (1) 화면 표시용 State
    ...

    // (2) ViewModel용 내부 변수
    private val _isInitialized = mutableStateOf(false)
    val isInitialized: State<Boolean>
        get() = _isInitialized

    // (3) View에서 받아 처리할 이벤트
    ...

    // (4) View로부터 받은 이벤트 처리
    fun onEvent(event: NutrientViewModelEvent) {
        ...
    }
}

State로 선언해야 Jetpack Compose Runtime이 값의 변경을 감지할 수 있다. (1) 화면 표시용 State에 isInitialized를 넣지 않은 이유는 (1)에는 눈에 보이는 UI에 관련된 것만 넣기로 규칙을 정했기 때문이다. 마치 사진들을 카테고리 별로 나눠 각각의 앨범에 분류하는 것과 같이 말이다. isInitialized는 사용자의 눈에 보이지 않는 내부적인 동작에 관련된 프로퍼티고, 앞으로도 이와 유사한 프로퍼티가 필요하다면 (2)의 영역에 선언하기로 한다.

 

#2-2 LazyColumn에서 표시할 아이템의 초기 갯수를 1개에서 20개로 변경

...

class NutrientViewModel : ViewModel() {
    // (1) 화면 표시용 State
    ...

    // (2) ViewModel용 내부 변수
    ...

    // (3) View에서 받아 처리할 이벤트
    ...

    // (4) View로부터 받은 이벤트 처리
    fun onEvent(event: NutrientViewModelEvent) {
        when (event) {
            is NutrientViewModelEvent.InitializeState -> {
                var dateToInsert = LocalDate.now()
                repeat(20) {
                    _nutrientScreenState.value.dailyMeals.add(
                        DailyMeal(
                            date = dateToInsert,
                            meals = SnapshotStateList()
                        )
                    )
                    dateToInsert = dateToInsert.plusDays(1)
                }
                _isInitialized.value = true
            }

            ...
        }
    }
}

add() 작업이 완료되면 방금 만들었던 isInitialized에 true를 할당한다.

 

#2-3 LaunchedEffect 분리

...

@Composable
fun NutrientScreen(
    ...
) {
    ...

    LaunchedEffect(key1 = true) {
        // State 초기화
        ...

        // ViewModel로부터 받은 이벤트 처리
        viewModel.nutrientScreenEventFlow.collectLatest { ... ->
            ...
        }
    }

    LaunchedEffect(key1 = viewModel.isInitialized.value) {
        if(viewModel.isInitialized.value) {
            // 무한 스크롤
            snapshotFlow { ... }.collect { ... ->
                ...
            }
        }
    }

    LazyColumn(
        ...
    ) {
        ...
    }
}

LaunchedEffect를 2개로 분리한다. 새로 만든 LaunchedEffect에 무한 스크롤 관련 코드를 옮겨 넣는다. 새로 만든 LaunchedEffect의 key는 아까 만든 isInitialized의 value를 넣고 해당 값이 true일때만 내용물이 동작하게 만든다. 결과적으로, 초기화가 완료되어야 비로소 무한 스크롤이 작동한다. 이 변경 사항은 지금으로선 큰 의미가 없어보이지만, 분명 잠재적인 에러를 줄여줄 것으로 기대한다. 

 

#3 요약

무한 스크롤 로직이 초기화 이후 작동하도록 변경했다.

 

#4 완성된 앱

#4-1 이 게시글 시점의 Commit

 

GitHub - Kanmanemone/nutri-capture-new

Contribute to Kanmanemone/nutri-capture-new development by creating an account on GitHub.

github.com

 

#4-2 본 프로젝트의 가장 최신 Commit

 

GitHub - Kanmanemone/nutri-capture-new

Contribute to Kanmanemone/nutri-capture-new development by creating an account on GitHub.

github.com