메이트 리스트에 페이지네이션 적용하기 ‐ 허혜민 - Team-HGD/SniffMEET GitHub Wiki
현재 페이지네이션 기법을 적용할 수 있는 부분은 메이트 리스트 뷰, 알림 리스트 뷰입니다.
이미지 사용량이 많은 메이트 리스트 뷰에 먼저 적용해보고자 했습니다.
request에 두 가지 쿼리 파라미터를 추가하면 됩니다.
불러올 데이터의 첫번째 위치(인덱스)를 정하는 offset과 불러올 데이터의 최대 개수를 정하는 limit입니다.
case .fetchList(let table, _, _, let page, let size):
return Endpoint(
baseURL: SupabaseConfig.baseURL,
path: "rest/v1/\(table)",
method: .post, // SQL 함수 호출은 POST 요청으로 수행
query: [
"limit": "\(size)",
"offset": "\(page * size)"
]
)
}
그리고 한가지 더 신경써야 할 부분은 마지막 페이지인지 확인할 수 있어야 한다는 점입니다.
Supabase 서버에는 총 페이지 사이즈를 반환하는 기능은 없기 때문에 다른 방식으로 접근해야 했습니다.
rpc 함수의 응답 헤더에 Content-Range 값은 현재 조회하고 있는 페이지 범위를 반환하고 있습니다.
페이지 범위가 출력되지 않는 상태라면 마지막 페이지라는 의미가 됩니다.
리스트를 불러오는 코드에 다음과 같은 코드를 추가해주었습니다.
// 마지막 페이지인지 확인하는 로직
if let range = response.header?["content-range"] as? String,
range == "*/*" {
throw SupabaseDBError.noMoreData
}
func fetchList(
into table: String,
with data: Data,
page: Int = 0,
pageSize: Int = 100
) async throws -> Data {
do {
if SessionManager.shared.isExpired {
try await SupabaseAuthManager.shared.refreshSession()
}
guard let session = SessionManager.shared.session else {
throw SupabaseAuthError.sessionNotExist
}
let response = try await networkProvider.request(
with: SupabaseDatabaseRequest.fetchList(
table: table,
data: data,
accessToken: session.accessToken,
page: page,
pageSize: pageSize
)
)
// 마지막 페이지인지 확인하는 로직
if let range = response.header?["content-range"] as? String,
range == "*/*" {
throw SupabaseDBError.noMoreData
}
//
return response.data
} catch let error as SupabaseDBError {
throw error
} catch {
throw SupabaseDBError.fetchDataFailed
}
}
.noMoreData
에러가 발생하면 더이상 네트워크 요청을 보내지 않도록 했습니다.
// MateListInteractor
func requestMateList(page: Int, pageSize: Int) {
Task { @MainActor in
do {
let mateList = try await requestMateListUseCase.execute(
page: page,
pageSize: pageSize)
presenter?.didFetchMateList(mateList: mateList)
} catch let error as SNMError {
SNMLogger.error(error.localizedDescription)
// 더이상 네트워크 요청을 보내지 않도록 설정하는 코드
presenter?.didReachEndOfMateList()
}
}
}
스크롤하던 중 메인스레드에서 다음과 같은 에러가 발생했습니다.
업데이트 전 row의 개수와 업데이트 후 row의 개수가 동일하지 않아 생기는 에러입니다.
presenter?.output.profileImageData
.receive(on: RunLoop.main)
.sink { [weak self] (mateID, imageData) in
self?.imageDataSource[mateID] = imageData
guard let index = self?.presenter?.output.mates.value.firstIndex(where: {
$0.userID == mateID
}) else { return }
let indexPath = IndexPath(item: index, section: 0)
// 에러 발생 부분
self?.tableView.reloadRows(at: [indexPath], with: .none)
}
.store(in: &cancellables)
원인은 tableView(_ tableView: UITableView, numberOfRowsInSection section: Int)
에 있었습니다.
reloadRows는 다음과 같이 설명이 되어 있습니다.
When this method is called in an animation block defined by the
beginUpdates()
andendUpdates()
methods, it behaves similarly todeleteRows(at:with:)
. The indexes thatUITableView
passes to the method are specified in the state of the table view prior to any updates. This happens regardless of ordering of the insertion, deletion, and reloading method calls within the animation block.
beginUpdates
와 endUpdates
메서드로 정의된 애니메이션 블록 안에서 reloadRows
를 호출하면 deleteRow
와 비슷한 동작을 한다는 설명입니다.
deleteRows
의 경우 endUpdates
전 dataSource의 count가 변경되면 exception이 발생하는 특징을 갖습니다.
이러한 특징을 적용해보면 endUpdates
가 호출되기 전 mates의 count가 변경된다면 numberOfRowsInSection
과 값이 동일하지 않아 exception이 발생한다고 볼 수 있습니다.
현재 reloadRows
의 with가 .none
으로 설정되어 있어 애니메이션이 적용되지 않은 것으로 보일 수 있지만, 실제로는 애니메이션이 적용되어 있는 상태입니다. .none
의 설명을 보면 다음과 같이 적혀있습니다.
The inserted or deleted rows use the default animations.
reloadData
를 호출해 단순하게 해결할 수 있지만 하나의 프로필 이미지가 변경될 때마다 전체 리스트를 다시 불러와야 하는 문제가 발생합니다.
reloadData
를 적용하고 앱을 다시 실행해보니 메이트 리스트를 스크롤 할 때마다 버벅거림이 발생했습니다.
Instruments를 확인해보니 우려한대로 cellForRow가 발생한 시점마다 메인스레드가 300% 이상 사용되는 현상이 발생했습니다.
이후 내용은 이미지의 비동기 처리와 더 관련이 깊기 때문에 아래 글에서 잇겠습니다.