본문 바로가기

TIL

[TIL] Kotlin Firebase - Realtime Database [ 2 ] 저장된 데이터Listener로 가져오기

Topic =  Firebase Realtime DB에 저장된 데이터를 여러가지 방식으로 가져오는 방법 학습

 

 


 

Why - DB를 가져오기 위해 사용하는 Listener에 대해서 학습해보고 차이점을 확인하기

 

Realtime DB의 데이터를 가져올 때 사용하는 Listener

  • ChildEventListener
    • ChildEventListener 인터페이스를 구현하여 특정 경로의 하위 항목에 대한 변경 사항을 감지하여 Data를 가져올 수 있지만 단일 읽기 기능을 제공하지 않는다.
    • 특정 경로 내에서 발생하는 개별 항목의 변경 사항만 감지하기 때문에 대규모 DB에서도 효율적으로 사용 가능.
    • 리스트 형태의 데이터를 처리할 때 유용하다
  • ValueEventListener
    • ValueEventListener 인터페이스를 구현하여 데이터의 변경을 감지하고 가져올 수 있으며, getValue() 메서드를 사용하여 해당 위치의 값을 읽고 조작할 수도 있다.
    • 지속적인 데이터 수신 기능도 제공하며, 지속적 수신이 아닌 단일 데이터 수신 메서드 또한 제공한다.
    • onDataChange 메서드를 사용하여 데이터 *Snapshot을 읽고 처리할 수 있다
    • 모든 data를 한번에 로드하므로 대규모 DB에서는 성능 문제가 발생할 수 있다.
    • 한번 등록하면 지속적으로 업데이트 받을 수 있기 때문에 실시간 위치, 채팅 등에 유용하다.
  • Query
    • 쿼리 방식 또한 ValueEventListener 또는 ChildEventListener를 사용하여 데이터 변경 사항을 처리한다.
    • orderByChild, orderByKey, orderByValue, startAt, equalTo 등 다양한 Query 메서드를 사용하여 필터링, 정렬을 통해 조건을 세분화하여 data를 불러올 수 있다.
    • 특정 범위 내의 원하는 조건에 따라 데이터를 필터링, 정렬할 수 있지만 학습 커브가 높은 단점이 있다.
    • Query에 대해서는 추후에 추가로 학습 후 정리해볼 예정

 

 

*Snapshot - DB의 특정 위치에 대한 데이터를 나타내는 객체 Git의 show Git Log를 생각하면 이해하기 편할 것 같다.

 

  • 이미 다른 Commit으로 프로젝트가 바뀌어도 이전의 특정 Commit으로 되돌릴 수 있는 것과 같은 개념이다.

 Firebase DB와 관련없어 보이는 개념도 있지만 알아두면 좋을 것 같다.

 

 

 

 

 

ChildEventListener

 

주요 개념

  • 실시간 업데이트 : ChildEventListener는 지정된 DB 참조 변경에 따른 하위 노드가 변경될 때마다 실시간 업데이트를 제공한다
  • Subscription Model ( Observer Pattern )
    • Firebase의 Realtime DB에서 ChildEventListener 인터페이스를 구현할 때 onChildChanged, onChildRemoved, onChildMoved와 같은 메서드를 오버라이드하여 각각 항목이 추가되거나 수정되거나 삭제될 때, 이동될 때 발생하는 호출을 감지하는 로직을 통해 데이터 처리를 할 수 있다.
  • 특정 범위 내의 원하는 변경 사항에 대한 업데이트만 수신하므로 데이터 수신, 전송 측면에서 효율적이다

 

이벤트 처리 메서드

        db.reference.child("Users").addChildEventListener(
            object : ChildEventListener {
                override fun onChildAdded(snapshot: DataSnapshot, previousChildName: String?) {
                    TODO("Not yet implemented")
                }

                override fun onChildChanged(snapshot: DataSnapshot, previousChildName: String?) {
                    TODO("Not yet implemented")
                }

                override fun onChildRemoved(snapshot: DataSnapshot) {
                    TODO("Not yet implemented")
                }

                override fun onChildMoved(snapshot: DataSnapshot, previousChildName: String?) {
                    TODO("Not yet implemented")
                }

                override fun onCancelled(error: DatabaseError) {
                    TODO("Not yet implemented")
                }
            }
        )
  • onChildAdded
    • ChildEvenetListener가 지정된 범위 내의 새 하위 노드가 추가될 때 호출된다.
    • dataSnapshot, previousChildName 파라미터를 받는다.
      • dataSnapshot - 변경된 하위 노드 데이터의 Snapshot
      • previousChildName - DB에 새로 추가된 하위 노드 바로 앞에 위치한 하위 노드의 Name
    • 새로 추가된 child 에 대한 데이터 Snapshot을 제공한다.
    • 앱이 시작될 때 목록이나 컬렉션을 초기화 하는 상황에서 유용하다.

 

  • onChildChanged
    • 기존 child 노드의 데이터가 수정될 때 호출된다.
    • dataSnapshot, previousChildName 파라미터를 받는다.
    • 데이터가 업데이트되면 UI 요소도 업데이트 하는 등 Data의 변화를 실시간으로 처리할 수 있다. 

 

  • onChildRemoved
    • Listener가 지정된 범위 내의 하위 노드가 제거될 때 호출된다.
    • 제거된 하위 노드에 대한 데이터의 스냅샷을 파라미터로 받는다.

 

  • onChildMoved
    • 목록 내에서 하위 노드의 위치가 변경될 때 호출된다. 일반적으로 데이터를 정렬할 때 사용된다.
    • 이동된 노드의 데이터의 스냅샷인 dataSnapshot과 previousChildName을 파라미터로 받는다.

 

  • onCancelled
    • Child의 Event 감시 중 오류가 발생한 경우 호출된다.
    • 보안 규칙 위반이나 네트워크 연결 끊김들과 같은 잠재적인 오류를 처리하는데 사용된다.
    • 오류 정보가 포함된 databaseError 매개 변수를 받는다.

 

 

ValueEventListener

 

주요 개념

  • ValueEventListener는 주로 특정 위치의 DB에서 data를 검색하는 데 사용된다.
  • 데이터의 단일 읽기를 수행하거나 변경 사항을 지속적으로 감시하는 데 사용할 수 있다
    • ChildEventListener는 단일 읽기 기능을 제공하지 않는다.
  • 데이터가 사용 가능하거나 변경될 때 호출되는 콜백을 제공하여 비동기로 작업이 이루어진다

 

 

 

이벤트 처리 메서드

 

addValueEventListener

  • 변경 사항을 지속적으로 수신하고 실시간으로 앱을 업데이트 하는 지속적 수신의 형식이다

 

addListenerForSingleValueEvent

  • 데이터를 한번 읽고 수신을 중지할 수 있는 단일 이벤트 수신 기능이다. 특정 시점의 데이터 스냅샷을 검색하는 데 유용하다.

 

 

        db.reference.child("Users").addValueEventListener(
            object : ValueEventListener {
                val listData: MutableList<Users> = mutableListOf<Users>()
                override fun onDataChange(snapshot: DataSnapshot) {
                    TODO("Not yet implemented")
                }

                override fun onCancelled(error: DatabaseError) {
                    TODO("Not yet implemented")
                }

            }
        )
  • onDataChange
    • 데이터 검색에 성공하거나, 지정된 위치의 데이터가 변경된 경우 호출된다.
    • dataSnapshot 매개변수에는 해당 위치의 데이터 스냅샷이 포함된다. getValue()를 사용하여 데이터를 추출하고 이를 데이터 모델에 매핑할 수 있다.

 

  • onCancelled
    • 데이터를 조회하는 과정에서 오류가 발생하면 해당 메소드가 호출된다.
    • databaseError 파라미터에 오류 메세지, 오류 코드 등 오류에 대한 정보가 포함된다.

 

 

 


 

DB에 저장된 데이터 가져오기

✔️ 데이터를 가져오기 전 Firebase Console의 규칙 탭에서 ".read" 설정을 true 로 바꿔주어야 한다 - 1시간 잡아 먹었다 

 

  • GPT도 규칙 확인하라고 했는데, 당연하게 이미 설정해둔 줄 알고 무시했었다가 고생했다.

 

먼저 넣어두었던 DB의 구성을 다시 한번 확인해두면 데이터를 가져올 때 수월해지는 것 같다

해당 DB에서 현재 앱에 접속해있는 user의 email 값을 가져오려면 어떻게 해야할까??

 

child 노드명 자체가 uid로 만들어진 것으로 child(auth.currentUser!!.uid) 를 통해 접근해서 값을 받아오면 될 것 같다. 코드를 통해서 구현해보고 확인해보자

 

MyPageFragment.kt

 

[ 1 ] - 가져오고자 하는 DB reference에 접근하여 Listener 지정해주기

db.reference.child("Users").child("${auth.currentUser!!.uid}").addValueEventListener(
            object : ValueEventListener {
                override fun onDataChange(snapshot: DataSnapshot) {
                   ...
                }
  • onCreate에서 Firebase.database로 초기화 해둔 db를 onViewCreate() 내부에서 db.reference.child ~ 를 통해 현재 앱을 사용중인 사용자의 uid와 일치하는 노드를 가진 User 노드 상위에 reference에 접근할 수 있도록 해준다.
  • addValueEventListener와 onDataChange() 메서드를 통해 로그인 정보가 바뀌거나 Nickname 값이 변경될 경우 새로운 Data를 받을 수 있도록 해줄 것이다.

 

[ 2 ] -  DB에 접근하여 data 가져오는 로직 완성시키기

        db.reference.child("Users").child("${auth.currentUser!!.uid}").addValueEventListener(
            object : ValueEventListener {
                override fun onDataChange(snapshot: DataSnapshot) {
                    Log.d("xxxx", "snapshot: ${snapshot}")
                    if (snapshot.exists()) {
                        val email = snapshot.child("email").getValue(String::class.java)
                        val nickname = snapshot.child("nickname").getValue(String::class.java)

                        if (email != null && nickname != null){
                            // 변경된 데이터를 처리하거나 UI 업데이트 로직 구현
                            binding.mypageId.text = nickname
                            binding.mypageExplain.text = email
                        }
                    }

                }

                override fun onCancelled(error: DatabaseError) {
                    Log.d("xxxx", "onCancelled: 데이터 호출 실패")
                }

            }
        )
  • snapshot.exists() 메서드를 통해 Snapshot 객체가 데이터베이스에서 실제로 존재하는 데이터를 나타내는지에 대한 여부를 확인한다 데이터를 포함하고 있을 경우 true를, 포함하고 있지 않을 경우 false를 반환.

 

정상적으로 적용이 된 모습

 

 

 

전체 코드

class MyPageFragment: Fragment() {
    private var _binding : FragmentMypageBinding? = null
    private val binding get() = _binding!!

    private lateinit var auth : FirebaseAuth

    private lateinit var db : FirebaseDatabase

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        auth = Firebase.auth

        db = Firebase.database
    }
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        _binding = FragmentMypageBinding.inflate(inflater,container, false)


        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        db.reference.child("Users").child("${auth.currentUser!!.uid}").addValueEventListener(
            object : ValueEventListener {
                override fun onDataChange(snapshot: DataSnapshot) {
                    Log.d("xxxx", "snapshot: ${snapshot}")
                    if (snapshot.exists()) {
                        val email = snapshot.child("email").getValue(String::class.java)
                        val nickname = snapshot.child("nickname").getValue(String::class.java)
                        
                        if (email != null && nickname != null){
                            // 변경된 데이터를 처리하거나 UI 업데이트 로직 구현
                            binding.mypageId.text = nickname
                            binding.mypageExplain.text = email
                        }
                    }
                    
                }

                override fun onCancelled(error: DatabaseError) {
                    Log.d("xxxx", "onCancelled: 데이터 호출 실패")
                }

            }
        )
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}

 

 

 

 

 


 

[ A. 오늘 복습한 내용 / B. 다음에 학습할 내용 ]

A. Realtime DB에 전반적인 내용에 대해 복습된 것 같다.

 

B. Snapshot 개념에 대한 추가 학습

 

B. CloudFirestore 추가 학습

 


 

[오류,에러 등등]

1. Firebase Console의 규칙에 read 값이 false 인 상태로 디버그 하면서 값이 왜 안들어오는지 의아해했다.

 

 

 

 


 

[느낀 점]

1. 기능 하나 끝내기전에 해당 기능에 부가적인 요소 등등 궁금한 게 자꾸 생겨서 파해치다 보면 이해한 것은 적거나 없는데 시간이 훌쩍 지나버린다;;

 

2. 조급해 하지 않고 싶은데 남들이 다 잘하니까 그게 쉽지 않다;;

 

3. ;;;;;;;;;......

 

 


[Reference]

 

 

// Listener

chat.openai.com

https://suyeonoeyus.tistory.com/81

https://firebase.google.com/docs/reference/kotlin/com/google/firebase/database/ChildEventListener