
안녕하세요. 주코입니다. 최근, 팀프로젝트중 비정상적인 firebase 읽기 수가 포착되어 원인을 파악하다가 Endless Scroll에 문제가 있음을 캐치했고 어떻게 해결 하고 개선 하였는지 포스팅 하려고 합니다.
---
Firestore 무료 요금제
저희는 프로젝트 상황 상 Firestore의 무료 요금제를 사용하였습니다.
위와 같이 무료 요금제는 일일 읽기가 5만회로 제한적이였습니다.
문제 상황
저희 프로젝트는 반려견 산책 트래킹 앱으로 산책 기록을 사용자가 확인할 수 있도록 설계 하였습니다.
때문에, 만약 사용자가 산택을 500번, 1000번 등 산책을 많이 하게 되면 모든 산책 기록을 view에 띄워 주기 때문에 한번에 많은 데이터를 보여줄 시 앱 성능 저하가 발생 할 수 있었습니다.
저희는 이를 개선 하기 위해 Endless Scroll(무한스크롤)을 도입하여 개발자가 임의로 정해둔 값 만큼만 view를 불러오게 하여 사용자 앱 성능 저하 문제를 개선 하게 되었습니다.
이 후 Firestore 사용량을 확인 해보니 6월 26일 ~ 6월 27일 읽기의 사용량이 너무 많았습니다.
왜 이럴까 고민 하던 중 EndlessScroll 기능을 추가한 시점에 읽기의 사용량이 급격하게 늘어났던 것이였습니다.
따라서, 문제의 코드를 분석해본 결과, scroll이 맨 밑으로 닿을 때마다 view를 load시키는데, load를 시킬 때마다 view에 보여지는 데이터들의 Query가 계속 호출 되고 있었던 것이였습니다.
문제 코드
HistoryViewModel.kt
var isLoading = false
private var currentPage = 0
private val pageSize = 5
private var allWalkData = listOf<WalkingInfo>()
private var selectedYear: Int = 0
private var selectedMonth: Int = 0
private fun loadWalkData(year: Int, month: Int, reset: Boolean = false) {
viewModelScope.launch {
try {
isLoading = true
val startDate = DateFormatter.getStartDateForAllDay(year, month)
val endDate = DateFormatter.getEndDateForAllDay(year, month)
val dogId = _selectDogState.value?.id ?: return@launch
val walkEntities = getWalkingListByDogIdAndPeriodUseCase(dogId, startDate, endDate)
val walkInfo = walkEntities.map {
WalkingInfo(
id = it.id,
dogId = it.dogId,
timeTaken = it.timeTaken,
distance = it.distance,
startDateTime = it.startDateTime,
endDateTime = it.endDateTime,
walkingImage = it.walkingImage
)
}.sortedByDescending { it.startDateTime }
if (reset) {
_walkListState.value = walkInfo.take(pageSize)
currentPage = 1
} else {
val updatedList = _walkListState.value.toMutableList()
updatedList.addAll(walkInfo.drop(currentPage * pageSize).take(pageSize))
_walkListState.value = updatedList
currentPage++
}
_loadWalkEvent.emit(DefaultEvent.Success)
} catch (exception: Exception) {
exception.printStackTrace()
_loadWalkEvent.emit(DefaultEvent.Failure(R.string.msg_load_walking_data_fail))
} finally {
isLoading = false
}
}
}
fun loadMoreWalkData() {
loadWalkData(selectedYear, selectedMonth)
}
HistoryActivity.kt
binding.rvWalkHistoryArea.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
if (!binding.rvWalkHistoryArea.canScrollVertically(1)) {
historyViewModel.loadMoreWalkData()
}
}
})
위 와 같이 query를 호출하는 UseCase를 loadWalkData() 함수가 포함하고 있는데 loadWalkData() 함수를 가져다 쓰는 loadMoreWalkData()를 scroll을 내리면서 load할 때마다 호출 하고 있어, 이와 같은 문제가 발생 하였습니다.
따라서, Firestore를 사용할 수 있는 한도가 정해져 있는 제한적인 상황에서 query호출 빈도를 줄이기 위해 UseCase를 한번만 호출하도록 함수를 분리해야 했습니다.
해결
해결 코드
HistoryViewModel.kt
var isLoading = false
private var currentPage = 0
private val pageSize = 5
private var allWalkData = listOf<WalkingInfo>()
private fun fetchWalkData(year: Int, month: Int) {
viewModelScope.launch {
try {
isLoading = true
val startDate = DateFormatter.getStartDateForAllDay(year, month)
val endDate = DateFormatter.getEndDateForAllDay(year, month)
val dogId = _selectDogState.value?.id ?: return@launch
val walkEntities = getWalkingListByDogIdAndPeriodUseCase(dogId, startDate, endDate)
allWalkData = walkEntities.map {
WalkingInfo(
id = it.id,
dogId = it.dogId,
timeTaken = it.timeTaken,
distance = it.distance,
startDateTime = it.startDateTime,
endDateTime = it.endDateTime,
walkingImage = it.walkingImage
)
}.sortedByDescending { it.startDateTime }
loadWalkData(reset = true)
_loadWalkEvent.emit(DefaultEvent.Success)
} catch (exception: Exception) {
exception.printStackTrace()
_loadWalkEvent.emit(DefaultEvent.Failure(R.string.msg_load_walking_data_fail))
} finally {
isLoading = false
}
}
}
private fun loadWalkData(reset: Boolean = false) {
if (reset) {
_walkListState.value = allWalkData.take(pageSize)
currentPage = 1
} else {
val updatedList = _walkListState.value.toMutableList()
updatedList.addAll(allWalkData.drop(currentPage * pageSize).take(pageSize))
_walkListState.value = updatedList
currentPage++
}
}
fun loadMoreWalkData() {
if (!isLoading) {
isLoading = true
loadWalkData(reset = false)
isLoading = false
}
}
HistoryActivity.kt
binding.rvWalkHistoryArea.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
if (!binding.rvWalkHistoryArea.canScrollVertically(1)) {
historyViewModel.loadMoreWalkData()
}
}
})
위 와 같이 UseCase를 담아두는 fetchWalkData() 함수를 따로 두고 boolean 람다식을 이용해 한번만 호출합니다.
이후 loadWalkData() 함수에 데이터를 저장해서 해당 함수를 가지고있는 loadMoreWalkData()를 load할 때 마다 호출하게 하여 query사용 빈도를 줄이게 되었습니다.
실제로 함수를 분리 후 개선한 후 다음날 읽기 사용량을 보니 3600회로 약 61.3% 개선되었습니다.
다만, 당시 앱을 개발하고 테스트 하는 단계이기에 꼭 이 문제 하나로 읽기 사용량이 대폭 감소했다고 판단하긴 조금 어렵습니다.
하지만, Firestore의 읽기를 사용할 수 있는 할당량이 정해져 있어 제한적인 상황에 query 호출 횟수를 최소화 시키는데 중점을 뒀지만, 할당량이 정해져 있지 않아도 빈번한 쿼리 호출은 네트워크 트래픽을 증가시키고 앱의 응답 속도를 느리게 만들 수 있습니다. 때문에 결코 헛된 작업은 아니었습니다.
이번 트러블 슈팅으로 사용자 입장에선 얼마나 쿼리를 호출할지 고민을 하는 과정이 너무나 즐거웠고 효율적인 쿼리 관리와 성능 최적화의 중요성을 깊이 이해하게 되어 만족스러운 트러블 슈팅이었습니다.
주코딩의 개발 노트!
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!