[iOS] SwiftUI에서 무한 스크롤 구현하기 (feat. LazyVStack)

Sehee·2024년 7월 23일

iOS 개발하기

목록 보기
10/16
post-thumbnail

시작하며,

데이터를 한 번에 모두 불러오게 되면 처음 로딩이 매우 오래걸릴 수 있다
그래서 일부 데이터만 먼저 불러오고, 특정 조건에 따라 데이터를 추가로 불러오는 방식이 필요하다

본인은 velog의 graphql을 활용해서 데이터를 불러오기에, cursor를 활용한 페이지네이션과 무한 스크롤을 구현했다


페이지네이션과 LazyVStack

페이지네이션

페이지네이션은 데이터를 여러 페이지로 나누어 한 번에 일부만 불러오는 방식이다
페이지네이션을 구현하는 방법은 크게 두 가지가 있다

기본 페이지네이션

각 페이지가 고유한 번호를 가지고 있고, 페이지 번호를 기준으로 데이터를 요청합니다. 예를 들어, 첫 번째 페이지는 1번, 두 번째 페이지는 2번으로 식별됩니다.

커서 기반 페이지네이션

페이지 간 이동을 커서(또는 토큰)를 사용하여 관리하는 방식입니다. 이전 페이지의 마지막 항목을 기준으로 다음 페이지를 요청합니다. 주로 정렬된 데이터나 실시간 업데이트가 필요한 경우에 유용합니다.

방법 선택

본인은 무한 스크롤로 구현할 것이기에, 스크롤 기반으로 마지막 항목을 기준으로 다음 페이지를 요청할 필요가 있다
또한 velog의 graphql에서 커서 기반 페이지네이션이 가능하다
따라서 커서 기반 페이지네이션 방법으로 구현하였다

LazyVStack

일반적으로 VStack은 화면에 보여지지 않는 부분이더라도 초기 로딩 시에 모두 그려진다
그러나 무한 스크롤로 페이지네이션을 구현하고자하는 본인의 목적과는 부합하지 않다

LazyVStack은 실제로 화면에 보여지는 부분만 렌더링한다

LazyVStack은 실제 화면에 보여지는 부분만 렌더링하기 때문에 초기 로딩 속도가 빠르고 메모리 사용도 효율적이다
많은 양의 데이터를 불러올 때에도 LazyVStack을 사용하면 화면 로딩 속도가 빠르고, 메모리 관리도 용이하여 셀룰러 데이터 환경에서도 효율적이다

무한 스크롤과 같은 페이지네이션을 구현할 때 LazyVStack을 적절히 활용하면 코드 관리도 쉬워지며, 사용자 경험도 개선할 수 있다


무한 스크롤 구현하기

ScrollView와 LazyVStack을 활용하면 무한 스크롤을 구현할 수 있다
커서 기반 페이지네이션도 함께 구현해보자

코드 요약

struct MainView: View {
	
    @State private var datas: [DataModel] = []

	var body: some View {
    	ScrollView {
        	LazyVStack {
            	ForEach(datas) { data in
                	DataItem(...)
						.onAppear {
                        
							// ... 무한 스크롤 구현 로직
                            
                        }
				}
            }
        }
		.frame(maxWidth: .infinity, maxHeight: .infinity)

    }
}

커서 기반 페이지네이션

우선 데이터를 불러오는 api에서 파라미터로 cursor값을 전달하면 된다
cursor 값은 직전에 불러온 데이터 마지막 항목의 id값이다

처음 데이터를 불러올 땐 cursor값이 업으므로, nil 값을 넣어준다

api 호출에 대한 방법은 각자 다를 것이니 넘어가도록 하겠다

무한 스크롤 구현

DataItem(...)
	.onAppear {
        guard let index = datas.firstIndex(where: {$0.id == data.id}) else { return }
        
        if index % pageSize == (pageSize - 1) {
            Task {
                do {
                    try await loadMoreData()
                } catch (let error) {
                    print("Unable to get data : \(error)")
                }
            }
        }
    }

여기서 pageSize는 한 번에 불러올 데이터 개수이므로, 상수로 넣어도 되고 변수로 선언해서 넣어줘도 된다
(변수로 선언하는 게 관리하기 편하긴 함)

DataItem(리스트 아이템) 하나가 뷰에 보여질 때 해당 아이템이 마지막 항목인지를 체크한다
마지막 항목이면 loadMoreData() 함수를 호출한다

loadMoreData()

loadMoreData() 함수는 마지막 인덱스의 cursor를 파라미터로 넘겨주어 이 다음 데이터를 불러오는 역할을 한다

api 호출이기에 async await을 활용하여 비동기처리하는 것이 좋다

private func loadMoreData() async {
    let lastDataId = datas.isEmpty ? "" : datas[datas.count - 1].id
    fetchData(cursor: lastDataId) { fetchedDatas in
        if let fetchedDatas = fetchedDatas {
            self.datas.append(contentsOf: fetchedDatas)
        }
    }
}

마치며,

생각보다 간단했지만, 엄한 곳에서 삽질하느라 좀 오래 걸렸다,,,

profile
디자인하는 개발자

0개의 댓글