ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [포케덱스] - LazyVGrid 무한스크롤 구현하기(feat. SwiftUI + TCA)
    프로젝트/포케덱스 2025. 8. 19. 21:05
    728x90
    반응형

    오늘은 TCA를 적용하게 된 김에 무한 스크롤에 대해서 포스팅을 할 것이다.

    무한 스크롤을 구현하기 위해서는 리스트 뷰의 셀 중 마지막 요소가 보일 때 요청하도록 하는 것이 핵심이다.

     

    그것을 구현하기 위해서는 LazyVGrid랑 LazyVStack가 필요하다.

    이유는 아래에서 천천히 설명해볼 것이다.

     

    먼저 LazyVGrid에 대해서 먼저 알아보자. LazyVStack도 동일하게 적용되는 부분이다.

     

    이런 코드가 있다고 하자

    struct LazyVGridView: View {
        let columns = Array(repeating: GridItem(.flexible()), count: 3)
        
        var body: some View {
            ScrollView {
                LazyVGrid(columns: columns) {
                    ForEach((0...100), id: \.self) { num in
                        Text("\(num)")
                            .padding()
                            .background(.blue)
                            .onAppear {
                                print(num)
                            }
                    }
                }
            }
        }
    }

     

    LazyVGrid는  필요한 셀만 메모리에 로드하는 지연 로딩 그리드 뷰이다.

    ForEach로 반복되는 각 셀은 실제로 화면에 보일 때쯤 그려지고 메모리에 늦게 올라간다.

     

    프리뷰는 값이 이상하게 로딩되지만 실기기에서는 잘 동작한다.

     

    그럼 어떤 원리로 이렇게 동작할까?

     

    그건 바로 LazyVGrid의 최적화 방식 때문이다.

    핵심 원리는 성능 최적화를 위해 화면에 보일 셀들을 미리 준비(Pre-loading)한다는 것이다.


    LazyVGrid의 단계별 동작 원리 ⚙️

    LazyVGrid는 효율적인 뷰 관리를 위해 다음과 같은 단계로 동작한다.

     

    1. 초기 로딩

    LazyVGrid는 뷰가 처음 나타날 때 화면을 채우는 데 필요한 셀과, 스크롤 시 바로 다음에 보여질 버퍼 영역의 셀들을 미리 생성한다. onAppear는 뷰가 화면에 그려질 준비를 마쳤을 때, 즉 메모리에 로드될 때 호출된다. 

     

    2. 스크롤과 동적 생성

    사용자가 스크롤을 하면, LazyVGrid는 새롭게 화면에 진입할 셀들을 미리 생성하여 끊김 없는 경험을 제공한다. 이 과정에서 새롭게 생성되는 셀들의 onAppear가 순차적으로 호출된다.

     

    3. 셀의 재사용 및 제거

    화면 밖으로 완전히 벗어난 셀들은 메모리에서 제거되거나 재사용을 위해 대기한다. 셀이 메모리에서 해제될 때는 onDisappear가 호출된다. 이 메커니즘 덕분에 수많은 데이터를 다루더라도 메모리 사용량을 최소화할 수 있다.

     

    이건 UIkit을 사용할 때 셀 재사용 관련해서 배운적이 있다.


    정리한 내용을 바탕으로 무한 스크롤에 적용시켜보자

     

    지금 내가 사용하는 API는 1페이지당 20개씩 포켓몬 정보를 반환한다. 그래서 1페이지에 마지막에 도달하면 자동으로 2페이지로 도달하도록 구현했다.

    ForEach는 포켓몬 리스트의 수량만큼 셀을 반복 생성하는 역할을 한다.

    MainFeature는 각 셀의 상태와 액션을 관리하며, 이는 TCA의 특징으로 상위 Feature가 하위 Feature를 컨트롤하는 구조다.

    따라서 MainFeature가 각 셀의 상태와 액션을 보유하고 있고, 이를 ForEach를 통해 개별 셀에 바인딩함으로써 다수의 셀을 효과적으로 관리할 수 있다.

     

    MainFeature의 Reducer에서 해당 옵션으로 연결이 가능하다. 

     

    SwiftUI는 상태가 변경되면 해당 상태와 연결된 View를 다시 렌더링한다.

    따라서 ForEach로 구성된 각 셀에 대해 WithViewStore를 개별적으로 감싸 주면, 각 셀의 상태 변경이 해당 셀에만 영향을 주도록 제한할 수 있어 성능상 더 효율적이다.

     

    이는 TCA에서 View의 재렌더링 범위를 최소화하기 위한 중요한 패턴이다.

    TCA를 사용하면 이렇게 감싸주지 않으면 사용할 수 없도록 막아두는 것 같다. 이건 되게 좋은 것 같다.

     

    얘기가 잠깐 TCA쪽으로 빠졌는데, 다시 무한 스크롤쪽으로 돌아오자면 셀 아래에 조건 문이 있다.

    이 조건은 다음을 설명한다.

     

    1. 가장 아래 요소까지 스크롤업 했을 때

    2. 현재 아이템 요소 중 지금 보고 있는 아이템이 가장 마지막 아이템일 때

     

    이 조건일 때 ProgressView를 보여주고 다음 페이지가 있을 경우 다음 페이지를 호출하는 액션을 send한다.

     

    뷰는 이벤트만 보내고 비즈니스 로직은 MainFeature에서 실행된다.

     

    만약 현재 페이지가 마지막 페이지가 아닐 경우 현제 페이지를 업데이트 하고 외부 API로 다음 페이지요청을 보내게 된다.

    그렇게 되면 다음 페이지가 현재 아이템 배열에 append되고 기존에 ProgressView가 있던 자리에 그 다음 아이템이 쌓이게 된다.

     

    완료..!

Designed by Tistory.