본문 바로가기
⛏️ | 개발 기록/⏰ | Schedule Planner

[Android, Kotlin] firebase 데이터베이스 변경하기 (13)

by immgga 2024. 4. 10.

오랜만에 planner v2를 만져 보았다.

지금까지의 회사생활에서 보고 배운 것을 바탕으로 데이터베이스 쪽에 새로 수정해야 할 것들이 있어서 데이터베이스 코드를 전부 갈아주는 작업을 해주었다.

지금 바로 내가 어떤 작업을 했는지 알아보도록 하자.

 

1. 기존 데이터베이스의 문제점

planner v2에서는 firebase realtime database를 사용하고 있었다.

하지만 현재 통계 화면을 만들어야 하는 나의 경우에는 저장된 plan 데이터를 바탕으로 데이터를 필터링해서 통계 데이터를 구성해야 하는 상황이었다(데이터베이스를 변경한 가장 큰 이유이다.)

하지만 realtime database에서는 데이터를 필터링하기가 힘들어서 바로 이번에 코드도 볼 겸에 데이터베이스 코드를 모두 바꿔주기로 했다.

 

2. firestore

내가 변경할 데이터베이스인 firestore는 realtime database처럼 firebase에서 제공하는 데이터베이스 중 하나이다.

firestore는 쿼리를 바탕으로 데이터를 필터링이 가능하다. 이 장점이 있어서 나중에 통계 screen을 만들 때 편할 것 같아서 바로 firestore로 바꿔주었다.

 

3. 코드 변경하기

일단 먼저 PlanData 클래스부터 바꿔주었다.

기존의 planData에서 baseDate와 createdTime을 추가해 주었다.

class PlanData(
    val baseDate: String = "",
    val title: String = "",
    val description: String = "",
    val createdTime: Long? = null,
    complete: Boolean = false
) {
    var complete by mutableStateOf(complete)
}

baseDate는 일정을 생성했을 때, 어떤 날짜에 생성했는지 판단할 수 있도록 하기 위해 생성해 주었다.

예를 들어. 2024년 4월 20일의 일정을 생성하면 2024-04-20와 같은 형식으로 날짜 데이터가 저장된다.

baseDate를 이용해 해당 날짜에 어떤 일정들이 생성되었는지 체크할 수 있다.

 

createdTime은 일정을 생성한 시간을 알기 위해 생성해 주었다.

createdTime은 unix timestamp 형식으로 저장되도록 했다(나중에 parsing 해서 변형하기 편리하게 하기 위해서이다.)

또한 createdTime을 firestore document의 이름으로도 설정 가능하게 했다.(planData에서 createdTime을 불러오면 document 이름도 불러올 수 있는 거라 편리하고 unix timestamp 형식이 밀리세컨드까지 측정을 하기에 중복되는 경우는 없다고 판단했다.)

 

이제 변경한 planData를 바탕으로 파이어베이스 코드를 변경해 보자.

그전에 firestore에서 plan 데이터의 crud 함수를 만들어 주었다.

crud는 기본적인 데이터 처리 기능인 create(생성), read(읽기), update(갱신), delete(삭제)을 뜻한다. 일단 나의 경우에는 delete 함수는 아직까지 쓸 일이 없어서 만들어두지는 않았지만 추후에 일정 삭제 기능을 구현할 때 구현해 보도록 하겠다.

crud 함수는 함부로 접근하지 못하도록 모두 확장 함수로 구현했다.

 

먼저 create 함수이다.

fun DocumentReference.createFireStoreData(
    setValue: Any,
    onSuccess: () -> Unit = {},
    onFailure: (Exception?) -> Unit = {},
) {
    this.set(setValue).addOnCompleteListener { task ->
        if (task.isSuccessful) onSuccess()
        else onFailure(task.exception)
    }
}

firestore의 document 경로(DocumentReference)를 받아서 확장 함수를 만들었다. addonCompleteListener를 이용해 성공, 실패 시의 고차 함수들을 생성해 주었다.

 

다음으로는 read 함수이다.

fun CollectionReference.readFireStoreData(
    onSuccess: (List<DocumentSnapshot>) -> Unit = {},
    onFailure: (Exception?) -> Unit = {}
) {
    this.get().addOnCompleteListener { task ->
        if (task.isSuccessful) {
            val querySnapshot = task.result
            onSuccess(querySnapshot.documents)
        } else onFailure(task.exception)
    }
}

fun DocumentReference.readFireStoreData(
    onSuccess: (DocumentSnapshot) -> Unit = {},
    onFailure: (Exception?) -> Unit = {}
) {
    this.get().addOnCompleteListener { task ->
        if (task.isSuccessful) onSuccess(task.result)
        else onFailure(task.exception)
    }
}

fun Query.readFireStoreData(
    onSuccess: (List<DocumentSnapshot>) -> Unit = {},
    onFailure: (Exception?) -> Unit = {}
) {
    this.get().addOnCompleteListener { task ->
        if (task.isSuccessful) {
            val querySnapshot = task.result
            onSuccess(querySnapshot.documents)
        } else onFailure(task.exception)
    }
}

타입이 CollectionReference, DocumentReference, Query일 때의 read 함수를 만들었다.

제네릭을 이용해 하나의 함수로 만들 방법도 생각해 보았었다.

하지만 어째선지 모르겠지만 제네릭 함수에 여러 타입을 설정하는 것이 되지 않았다.

코틀린 제네릭은 나중에 천천히 다시 공부해 봐야겠다.

 

마지막으로 update 함수이다.

fun DocumentReference.updateFireStoreData(
    updateValue: Map<String, Any>,
    onSuccess: () -> Unit = {},
    onFailure: (Exception?) -> Unit = {}
) {
    this.update(updateValue).addOnCompleteListener { task ->
        if (task.isSuccessful) onSuccess()
        else onFailure(task.exception)
    }
}

updateValue를 받아서 1개부터 여러 개의 document 필드를 변경할 수 있도록 map형식으로 받아오게 만들었다.

그것 말고는 위에 코드들과 다를 게 없다.

 

이제 crud 함수도 만들었으니, 본격적으로 realtime database 코드를 firestore 코드로 바꿔주겠다.

변경된 함수들이 많아서 그중 몇 개만 소개해 주겠다.

먼저 일정을 저장하는 기능인 savePlan 함수부터 보자.

fun savePlan(plans: List<PlanData>, uid: String, baseDate: String?, navigateToPlan: () -> Unit) {
    val saveRef = FirebaseFirestore.getInstance()
        .collection("schedule")
        .document(uid)
        .collection("plans")

    plans.forEach { planData ->
        // 생성될 시점의 unix timestamp 구하기(document id, createdTime에 써먹기 위함)
        val savedTimeMillis = System.currentTimeMillis()
        val resultPlan = PlanData(
            baseDate = baseDate.toString(),
            title = planData.title,
            description = planData.description,
            createdTime = savedTimeMillis,
            complete = false
        )
        // documentLength와 생성된 plans들의 길이를 구해서 document의 name을 정함.
        saveRef.document(savedTimeMillis.toString()).createFireStoreData(
            setValue = resultPlan,
            onSuccess = navigateToPlan,
            onFailure = {}
        )
    }
}

받아온 plans를 이용해 반복문을 돌린 후 이전에 생성한 create 함수로 firestore에 저장해 주는 코드이다.

반복될 때마다 System.currentTimeMillis()를 이용해 unix timestamp를 구해준 후 createdTime에 저장해 주었다.

처음 구현할 때는 System.currentTimeMillis()를 반복문 바깥에 써서 일정을 여러 개 생성하면 생성한 것들 중 마지막 plan 만 데이터베이스에 들어가는 해프닝도 있었다 ㅎㅎ.

반복을 돌리면서 resultPlan에 firestore로 저장할 데이터를 모두 담아서 create 함수로 저장해 주는 것이 끝이다.

 

다음은 일정을 불러오는 함수인 getPlans 함수이다.

fun getPlans(uid: String, date: String) {
    val getPlansRef = FirebaseFirestore.getInstance()
        .collection("schedule")
        .document(uid)
        .collection("plans")

    // baseDate와 날짜가 같은 document들만 불러오기.
    getPlansRef.whereEqualTo("baseDate", date).readFireStoreData(
        onSuccess = {
            _plans.clear()
            it.forEach { documentSnapshot ->
                val planObj = documentSnapshot.toObject(PlanData::class.java)
                if (planObj != null) _plans.add(planObj)
            }
        }
    )
}

일정 화면에서 달력의 날짜를 클릭하면 클릭된 날짜의 데이터가 불러와질 수 있도록 해주는 함수인데, 이 함수에서 이전에 생성한 baseDate와 firestore의 where을 이용해 내가 원하는 데이터를 쉽게 불러올 수 있었다.

 

마지막으로 일정의 완료 여부 체크박스에 사용되는 planCheck 함수이다.

fun planCheck(uid: String, documentId: String) {
    val checkRef = FirebaseFirestore.getInstance()
        .collection("schedule")
        .document(uid)
        .collection("plans")

    checkRef.document(documentId).readFireStoreData(
        onSuccess = {
            val planInfo = it.data
            val completed = planInfo?.get("complete") as Boolean?

            if (completed != null) {
                checkRef.document(documentId).updateFireStoreData(
                    updateValue = mapOf("complete" to !completed)
                )
            }
        }
    )
}

이 함수에서도 이전에 생성한 createdTime 필드를 이용했다.

document 이름이 createdTime과 동일한 정보를 담고 있기 때문에 createdTime을 이용해 쉽게 document 이름에 접근해 complete 필드를 변경할 document에 접근할 수 있었다.

기존에 realtime database를 사용했을 때는 해당 날짜에 생성한 날짜의 position 쪽으로 이동한 후, update를 적용할 map을 생성해서 적용해 주었어야 했었는데

createdTime이랑 document 이름이 같기 때문에 혹시 참조를 실수할 가능성이 줄어든 것 같다.

 

4. 마무리

최근에 회사생활을 마무리해서 planner v2를 다시 볼 시간이 생겼는데, 이전의 realtime database 코드를 firestore로 변경하면서 내 코드를 다시 볼 수 있었다.

아직은 기억 안나는 것들도 꽤 있지만 이건 나중에 기능 개발을 하면서 점점 나아질 거라고 생각한다.

그리고 체감상 firestore 코드가 realtime database 코드보다 보기 좋은 것도 있는 것 같다.

앞으로 시간도 많으니 여태까지 못했던 블로그도 좀 관리하면서 planner v2 개발도 재개해 보겠다.

728x90

댓글