Follow 상태 관리를 위한 노력

그렌실·2024년 4월 25일

우리 회사 앱이 sns의 성격도 강했기 때문에 인스타그램,유튜브,당근 같은 앱을 참고해보았는데, 서버 콜을 일일이 쏘지 않고도 팔로우,좋아요,차단 등의 상태관리가 잘되는 것을 느꼈다. 하긴 유저 입장에서 이미 입력한 동작이 적용되지 않아 보이면 그건 안된거다. 그래서 우리 앱에서도 어떻게 팔로우,좋아요,차단 등의 상태 관리를 할 지 생각해보았다.

  1. 첫번째는 그냥 원뎁스를 생각해보았다. 그치만 결론부터 얘기하자면 원뎁스는 그냥 임시방편 느낌이었다.
    궁극적으로 우리 앱에서는 추천 유저 리스트 -> 프로필 디테일 -> 비디오 리스트 -> 타인의 프로필 -> 타인의 팔로잉/팔로우 리스트 이 외에도 인박스 뷰에서 팔로잉/해제 등 여러가지 상황이 존재하였는데 이를 이미 불러온 api 리스트에도 적용을 해줘야 했다.
  1. 그러면 로컬로 id(string)와 상태(boolean) 리스트를 관리하고 이를 다른 뷰에 적용하면 되겠다. 싶은 생각이었다. 자 언제나 그렇듯 말로는 쉬웠지만 구체적으로 어떤 방식으로 언제 이 리스트를추가하고 언제 filter를 하고 언제 비워줄지 생각해보았다.
  1. 사용해본 로컬 저장 방식 및 후기

3-1.singleton 클래스

@Singleton
class FollowListStatusHandler @Inject constructor() {
    
    private val unFollowUserList = mutableListOf<UnFollowInfoStatus>()
    private var followChangeStatus = false

    private val _followChangeStatusFlow: MutableSharedFlow<Boolean> = MutableSharedFlow()
    val followChangeStatusFlow = _followChangeStatusFlow.asSharedFlow()
    
    fun saveUnFollowInfo(unFollowStatusInfo: UnFollowInfoStatus) {
        val existingStatus = unFollowUserList.find { it.userId == unFollowStatusInfo.userId }

        if (existingStatus != null) {
            val index = unFollowUserList.indexOf(existingStatus)
            unFollowUserList[index].isFollowed = unFollowStatusInfo.isFollowed
        } else {
            unFollowUserList.add(unFollowStatusInfo)
        }
    }
}

위 처럼 싱글톤 클래스로 리스트를 관리하였다. 사용해본 후기로는 크게 단점?이 없었다. 사실 가장 먼저 떠오른 방법은 roomDB 였는데 이걸 사용하면서 roomDB를 생각하지 않게 되었다. roomDB는 일일이 쿼리 짜야하고. 만약에 version이나 dao가 달라지게 되면.. 등의 상황을 고려했을 때 이 singleton은 성능적으로도 문제 없고 어차피 앱이 꺼지면 자동으로 사라지는 클래스이니까 따로 삭제하거나 하지 않아도 되고 잘 쓰는 방법인지는 모르겠지만 나는 굉장히 만족하면서 썼다.

3-2.preferenceUtil

fun addFollowIdList(userId: String, status: Boolean) {
        val jsonString = getData("followIdList")
        if (jsonString.isNullOrEmpty()) {
            val firstJson = JSONObject()
            firstJson.put(userId, status)
            setData("followIdList", firstJson.toString())
        } else {
            val existingJson = JSONObject(jsonString)
            existingJson.put(userId, status)
            setData("followIdList", existingJson.toString())
        }
    }

두번째 방법은 prefs에 넣은 방법인데, 내가 아는 한 string이 기본 지원 방식인데, json형식으로 string을 파싱해서 썼다. 다행히? 정상 동작하였고 저장하는 방식만 다를 뿐 전체적인 로직은 똑같다. 싱글톤 방식에 비해 prefs는 자주 사용?하는 방법이니까 개발하는 입장에서 친근했고 친근함을 넘어서 괜스레 안좋은방법?인가 싶은 마음도 있었지만 이 또한 성능은 만족스러웠다.

  1. 저장하고 나서 적용 시키는 방법

사실 이게 제일 주요했다고 본다. 이미 a Fragment에는 리스트가 불러져 와있는 상태에서 b Fragment에서 어떤 액션을 취하고 다시 a Fragment로 돌아왔을 때, 이 뿐 아니라 앞서 얘기했듯.. a->c->b->d->다시 a 로 돌아왔을 때 c,b,d 에서 한 액션이 어떻게든 a에서 적용되었어야했다.
더 자세히 얘기하면 앱 내에서 액션 스토리가

a -> c -> b -> d -> a 라고 했을 때
a -> c의 어떤 액션 -> b -> d -> a 일때에는 b,d,a에서 c의 액션이 적용 적용되어야함
a -> c의 어떤 액션 -> b -> d(여기서 c의 액션을 해제) -> a에는 결론적으로 아무 액션이 없는 상태

이런 느낌?으로 유저의 액션을 반영해야했다. 진짜 간단히 얘기하면 팔로우 상태관리라고 해야할까..(uiState 아님)

자 그러면 본격적으로 어떻게 반영시켰느냐.

 private fun setRecommendDancerList(it: List<RecommendDancerItem>?) {
        it?.let {
            if (it.isNotEmpty()) {
                noMoreItemInList = false

                it.forEachIndexed { index, item ->
                    recommendDancerList.add(item)
                }
                val savedList = MyApplication.prefs.getFollowIdList()
                savedFollowList = savedList

                recommendDancerList.removeIf { dancerListItem ->
                    savedFollowList.any { saveListItem ->
                        dancerListItem.dancerInfo.id == saveListItem.first && saveListItem.second
                    }
                }

                binding.recommendDancerRcview.apply {
                    (adapter as RecommendDancerAdapter).apply {
                        submitList(recommendDancerList) { notifyDataSetChanged() }
                    }
                }
            } else {
                noMoreItemInList = true
            }
        }
        isLoading = false
    }

위 코드는 list가 넘어올때 adapter로 submitList 하는 과정인데 submitList 전에 followList를 가져와서 제거하는 경우이다.(추천 유저를 이전 뷰에서 팔로우 하고 들어온 경우 이미 팔로우한 유저들은 추천리스트에서 보이지 않겠다는 의도) 이 경우는 a 에서 b로 넘어갈떄 b에 대한 액션 처리이지만 반대의 경우도 생각해야 했다.
그렇다는건 api 콜에 의존할게 아니라 내 로직이 정상동작한다면 돌아왔을 때 미리 선반영이 되어있어야 하는건데..

3가지 경우가 있었다.

4-1. 같은 viewModel을 쓰는 경우
같은 viewModel을 쓰는 view면 얘기는 쉽다.같은 viewModel 이니까 collect 시켜서 뷰로 돌아오기도 전에 이미 처리하는 방법. 제일 근본이라고 생각한다.
4-2.registerForActivityResult가 사용이 가능하거나 onResume이 타는 경우
어쩔수 없이 아주 약간의 바뀌는 과정이 살짝 보이긴하지만 eventBus(sharedViewModel)를 쓰고싶진 않았기에.. 이걸 사용했다.
4-3. 콜백
4-2를 사용하지 않게 된 이유이다. 만약 b Fragment 를 add().commit() 한다고 했을때
bFragment(private val followOnClick ((String,Boolean)->Unit))

이런식으로 콜백을 넣고 원래의 뷰에서 람다 함수의 액션을 정의했다. 그 액션은 1번에서 말한 저장하거나 하는 로직 말하는 것이다. 개인적으로 4-1이 가능하면 4-1의 방법이 제일 깔끔하고 그렇지 않다고해도 콜백을 이용해서 깔끔하게 처리되는 과정을 굳이 보일 필요없이 즉각 반영이 가능했었다.

쓰고 나니 굉장히 별거 아니고.. 그냥 리스트 개념만 있으면 될것 같지만 나름 볼륨이 있는 전체 앱 단에서 모든 상태 관리한다는 중압감?이 있었고 결론적으로는 만족하는 성능이다. 자잘한 오류와 시행착오가 있었지만 생략한거라구,,

0개의 댓글