SwiftUI

SwiftUI Firebase Realtime Database CRUD 제대로 사용하기

elisha0103 2022. 12. 8. 14:35

1. 개요

Firebase 실시간 데이터베이스는 클라우드 호스팅 데이터베이스이다.

데이터는 JSON으로 저장되며 연결된 모든 클라이언트에 실시간으로 동기화되는 장점이 있다. 또한 Firebase Realtime Database는 모든 클라이언트가 하나의 실시간 데이터베이스 인스턴스를 공유하고 자동으로 최신 데이터 업데이트를 할 수 있어서 여러 플랫폼(Android, iOS 및 JavaScript SDK)으로 교차 플랫폼 앱을 구축할 때 유용하다.

 

나는 SwiftUI로 Firebase Realtime Database를 공부하는데, CRUD(쓰기, 읽기, 수정, 삭제) 을 제대로 다루는 방법을 찾고자 했다. 

내가 직면한 문제는 CRUD는 모두 정상 작동 하지만, 실시간 데이터 읽기 시 각 이벤트가 발생할 때마다 자동으로 읽기처리가 되지 않았다.

실시간 데이터베이스를 구성하고자할 때, 서버의 데이터가 바뀌면 자동으로 데이터를 읽어와서 나의 앱에서 최신 데이터가 반영되도록 코드를 작성해보겠다.

 

나의 목표는 서버에서 데이터 쓰기, 수정, 삭제가 될 때마다 실시간으로 데이터를 앱에서 읽기 처리가 되도록 하는 것이다.


2. Firebase Realtime Database 설정

먼저 Xcode에서 프로젝트를 하나 생성해준다.

Xcode 프로젝트 생성

Firebase의 기능을 쓸 때는 Bundle Identifier를 잘 써야하고, 기억하기 편리한 것으로 하는 것이 좋다.

Bundle Identifier를 설정하기 위해서 이미 적혀있는 예시처럼 Organization Identifier를 작성해주면 Bundle Identifier는 Product Name과 Organization Identifier의 조합으로 자동으로 생성된다. 

 

프로젝트 생성 후에는 Firebase Console에서 프로젝트를 하나 추가한다.

 

Firebase Console 프로젝트 생성
Firebase Console 프로젝트 생성
Firebase Console 프로젝트 생성

위 과정 중에서 프로젝트 이름은 본인이 사용하는 앱 목적에 맞는 이름으로 설정해주고 Google 애널리틱스는 사용해도 되고 사용안해도 된다. 하지만 사용설정이 권장되어있으니 사용으로 해두자. 

구글 애널리틱스는 지금 과정에서 크게 중요하지 않지만 궁금한 사람은 아래 글을 확인

더보기

구글 애널리틱스

구글에서 제공하는 무료 웹 애널리틱스 서비스이다. 웹 애널리틱스 서비스는 웹 사이트의 트래픽을 추적하고 보고서를 만들어주는 서비스를 말하는데, 구글 애널리틱스에서 제공해주는 추적 번호를 내가 운영하는 웹 사이트에 삽입하면 사이트 방문자의 유입 소스나 사이트 내 행동과 같은 유용한 정보를 수집하고 저장, 분석할 수 있다. 내가 운영하는 사이트의 방문자들의 트래픽을 추적하여 행동을 분석한다면 앞으로 사이트를 운영할 때에 많은 도움이 되니 추적 번호를 넣을 수 있는 사이트나 블로그를 운영하고 있다면 적극 사용을 권장한다. 

더욱 자세한 글은 다음 사이트를 참고하시길

https://coding-factory.tistory.com/777

 

 

Firebase 프로젝트에 앱 추가하기

이제 프로젝트 개요 메인 화면에서 가운데 iOS+ 을 눌러 앱을 추가하여 시작한다.

 

앱 정보 입력

아까 얘기해둔 프로젝트 Bundle Identifier를 Apple 번들 ID에 입력한다. (필수)

앱 닉네임, App Store ID는 입력 해도 되고 안해도 되지만 되도록 입력 할 수 있는 정보는 모두 입력하자.

 

Google Info plist 추가

다음은 GoogleService-Info plist를 추가하는 것인데, 다운로드 한 뒤에 파일을 Xcode 프로젝트에 드래그 앤 드롭으로 넣어두자.

GoogleService-Info plist에는 Firebase와 프로젝트간 연결할 수 있는 정보들이 담겨있다.

 

다음은 Firebase SDK를 추가한다.

Xcode 상단 메뉴 바에서 File > Add Packages로 이동 

  https://github.com/firebase/firebase-ios-sdk

위 링크를 Search or Enter Package URL에 입력해준다.

Firebase 패키지 추가
Firebase SDK Product 선택 설치

firebase-ios-sdk 를 선택하고 Add Package를 클릭하고 이후에 패키지를 선택 추가 창이 나오면 

FirebaseDatabase

FirebaseDatabaseSwift

FirebaseFirestore

FirebaseFirestoreSwift

위 4개의 패키지를 선택하여 추가한다.

 

패키지까지 추가하고 Firebase에서 다음을 누르면, 

초기화 코드 프로젝트 추가

위 처럼 초기화 코드를 입력하라고 하는데,

프로젝트이름App.swift 파일로 들어가 아래처럼 작성해준다.

초기화 코드 프로젝트 추가

 

그리고 이제 프로젝트에 RealtimeDatabase를 빌드해주자.

Realtime Database 빌드 추가

프로젝트를 생성했다면 좌측 메뉴 빌드를 열어서 Realtime Database를 추가하고 중앙 Realtime Database 만들기를 클릭한다.

 

 

실시간 데이터 베이스 저장소 위치

데이터 베이스를 클릭하면 저장소 위치 국가가 나오는데, 본인은 미국으로 설정했다.

아무래도 미국이 실시간 데이터 로드 속도나 딜레이가 없는 듯하다.

 

 

데이터베이스 보안 규칙

다음으로는 데이터베이스 보안규칙 설정이 나오는데, 잠금모드로 시작해서 규칙 설정을 true로 바꿔줘도 되고, 학습 및 테스트 목적이라면 테스트 모드에서 시작을 클릭해도 된다.

다만, 테스트 모드 사용자는 기본적으로 30일 전까지 읽기 쓰기가 가능하고 그 이후에는 다시 규칙설정으로 바꿔줘야한다.

보안규칙까지 설정이 완료되면 Realtime Database를 사용할 수 있게 된다.

 

 

이제 코드를 작성해보자.


3. Realtime Database View Model

자동차 모델 정보를 저장하는 예제를 만들어보고자 한다.

먼저, Realtime Database에 접근하는 함수들을 만들어보고자 한다.

View Model, View Controller에 해당하는 클래스를 나는 CarStore라고 정의하겠다.

예제를 위한 자동차 struct 모델은 다음을 따른다.

// Car.swift

struct Car : Codable, Identifiable, Hashable {
    var id: String
    var name: String
    
    var description: String
    var isHybrid: Bool
}

 

CarStore의 내부를 정의해보자.

import Foundation
import FirebaseDatabase
import FirebaseDatabaseSwift

class CarStore: ObservableObject {
    @Published var cars: [Car] = []
    @Published var changeCount: Int = 0

    let ref: DatabaseReference? = Database.database().reference() // (1)
    
    private let encoder = JSONEncoder() // (2)
    private let decoder = JSONDecoder() // (2)
    
    func listenToRealtimeDatabase() {
        // (3)
    }
    
    func stopListening() {
        // (4)
    }
    
    func addNewCar(car: Car) {
        // (5)
    }
    
    func deleteCar(key: String) {
        // (6)
    }
    
    func editCar(car: Car) {
        // (7)
    }
    
}
  • (1): Realtime Database의 기본 경로저장하는 변수
  • (2): Realtime Database의 데이터 구조는 기본적으로 JSON 형태이다. 저장소와 데이터를 주고받을 때 JSON 형식의 데이터로 주고받기 때문에 Encoder, Decoder의 인스턴스가 필요하다.
  • (3): 데이터베이스를 실시간으로 '관찰'하여 데이터 변경 여부를 확인하여 실시간 데이터 읽기 쓰기를 할 수 있게된다.
  • (4): 데이터베이스를 실시간으로 '관찰'하는 것을 중지한다.
  • (5): 데이터베이스에 Car 인스턴스를 추가하는 함수
  • (6): 데이터베이스에서 특정 경로의 데이터를 삭제하는 함수
  • (7) 데이터베이스에서 특정 경로의 데이터를 수정하는 함수

 

데이터베이스 관찰 함수

아래 코드가 게시글 서두에서 얘기한 나의 목표가 된다.

서버에서 데이터 쓰기, 수정, 삭제가 될 때마다 실시간으로 데이터를 앱에서 읽기 처리가 되도록 하는 것을 구현한 코드다.

    func listenToRealtimeDatabase() {
        
        guard let databasePath = ref?.child("cars") else {
            return
        }
        
        databasePath
            .observe(.childAdded) { [weak self] snapshot, _ in
                guard
                    let self = self,
                    let json = snapshot.value as? [String: Any]
                else {
                    return
                }
                do {
                    let carData = try JSONSerialization.data(withJSONObject: json)
                    let car = try self.decoder.decode(Car.self, from: carData)
                    self.cars.append(car)
                } catch {
                    print("an error occurred", error)
                }
            }
        
        databasePath
            .observe(.childChanged){[weak self] snapshot, _ in
                guard
                    let self = self,
                    let json = snapshot.value as? [String: Any]
                else{
                    return
                }
                do{
                    let carData = try JSONSerialization.data(withJSONObject: json)
                    let car = try self.decoder.decode(Car.self, from: carData)
                    
                    var index = 0
                    for carItem in self.cars {
                        if (car.id == carItem.id){
                            break
                        }else{
                            index += 1
                        }
                    }
                    self.cars[index] = car
                } catch{
                    print("an error occurred", error)
                }
            }
        
        databasePath
            .observe(.childRemoved){[weak self] snapshot in
                guard
                    let self = self,
                    let json = snapshot.value as? [String: Any]
                else{
                    return
                }
                do{
                    let carData = try JSONSerialization.data(withJSONObject: json)
                    let car = try self.decoder.decode(Car.self, from: carData)
                    for (index, carItem) in self.cars.enumerated() where car.id == carItem.id {
                        self.cars.remove(at: index)
                    }
                } catch{
                    print("an error occurred", error)
                }
            }
        
        databasePath
            .observe(.value){[weak self] snapshot in
                guard
                    let self = self
                else {
                    return
                }
                self.changeCount += 1
            }
    }

데이터베이스를 '관찰'하여 경로내 파일중 CUD(생성, 수정, 삭제)가 감지될 때 후행 클로저 내 코드들이 실행된다.

databasePath.observe(관찰내용) 여기서 '변경내용'에 들어갈 수 있는 내용은 다음과 같다.

  • .value: 경로에 있는 컨텐츠의 모든 변경 내용을 감지하고 읽어온다.
  • .childAdded: 경로에 있는 컨텐츠 중 추가된 아이템이 있는 경우 아이템을 읽어온다.
  • .childChanged: 경로에 있는 컨텐츠 중 수정된 아이템이 있는경우 아이템을 읽어온다.
  • .childRemoved: 경로에 있는 컨텐츠 중 삭제된 아이템이 있는 경우 아이템을 읽어온다.
  • .childMoved: 경로에 있는 컨텐츠 중 아이템 순서가 변경된 경우 아이템을 읽어온다.

Realtime Database의 데이터는 기본적으로 JSON 형식의 데이터라고 말했다. 따라서 읽을 때, decoder를 이용하여 Car 구조체 형식에 맞게 변환해서 관찰 내용에 맞게 코드를 작성했다.

.childAdded 의 경우, 저장소에 추가된 데이터를 읽어와서 CarStore 내 cars라는 변수에 추가하고

.childChanged 의 경우, 저장소에 수정된 데이터를 읽어와서 CarStore 내 cars를 순회하면서 id 값이 같은 아이템을 찾아내어 재할당하는 모습이 보인다.

.childRemoved 의 경우, 저장소에 삭제된 데이터를 읽어와서 CarStore 내 cars를 순회하면서 id 값이 같은 아이템을 찾아내어 삭제하는 모습이 보인다.

.value 의 경우, 저장소 데이터의 변경 여부를 감지한다. 위 관찰 내용에 해당하는 모든 경우가 발생할 때마다 changeCount는 1회 증가할 것이다.

 

데이터베이스 관찰 중지 함수

    func stopListening() {
        databasePath?.removeAllObservers()
    }

데이터베이스 관찰 중지 함수가 실행되면 listenToRealtimeDatabase() 함수의 기능은 상실된다.

 

데이터베이스 데이터 추가 함수

    func addNewCar(car: Car) {
        self.ref?.child("cars").child("\(car.id)").setValue([
            "id": car.id,
            "name": car.name,
            "description": car.description,
            "isHybrid": car.isHybrid
        ])
    }

데이터베이스 데이터 추가 함수가 실행되면 매개변수로 전달된 'car' 정보가 cars 하위에 car.id 값에 해당하는 계층 경로가 생성되고 하위에 car의 데이터들이 저장된다.

 

데이터베이스 데이터 삭제 함수

    func deleteCar(key: String) {
        ref?.child("cars/\(key)").removeValue()
    }

데이터베이스 데이터 삭제 함수가 실행되면 매개변수로 전달된 key(경로)에 해당하는 데이터들이 모두 삭제된다.

 

데이터베이스 데이터 수정 함수

    func editCar(car: Car) {
        let updates: [String : Any] = [
            "id": car.id,
            "name": car.name,
            "description": car.description,
            "isHybrid": car.isHybrid
        ]
        
        let childUpdates = ["cars/\(car.id)": updates]
        for (index, carItem) in cars.enumerated() where carItem.id == car.id {
            cars[index] = car
        }
        self.ref?.updateChildValues(childUpdates)
        
    }

데이터베이스 데이터 수정 함수가 실행되면 매개변수로 전달된 'car' 정보로 새로운 updates 인스턴스를 생성하고, childUpdates라는 딕셔너리 타입의 인스턴스를 정의해준다. 이때 key는 저장될 데이터베이스 경로, value는 업데이트할 새로운 정보가 된다.

 

지금 이 예제에서는 self.ref?.updateChildValues(childUpdates) 가 아닌, 데이터베이스 데이터 추가함수에 사용된 

self.ref?.child("cars").child("\(car.id)").setValue(childUpdates) 를 사용해줘도 결과는 같아보인다.

.setValue와 .updateChildValues의 차이점은 다음과 같다.

 

  •  .setValue는 해당 경로에 데이터를 추가한다. 경로에 이미 데이터가 있다면 .setValue에 있는 모든 필드 값들로 재할당을 한다.
    기존에 있는 데이터와 새로 추가하는 데이터 간 서로 중복된 필드값을 가지고 있다고 하더라도 .setValue가 적용되는 경로에는 오로지 .setValue[data] 의 data의 값들만 저장된다.
  • 반면, .updateChildValues는 해당 경로에 데이터를 업데이트하는데, 경로에 업데이트하려는 필드값이 없다면 필드값을 새로 추가해준다. 만약, 경로에 업데이트하려는 필드 값과 같은 필드값이 이미 존재한다면 해당 필드의 키 값을 새로 업데이트해준다.
  • 정리하면, .setValue는 데이터를 저장하려는 공간에 새로운 데이터만 남게되고 .updateChildValues는 데이터를 저장하려는 공간에 데이터를 '덧붙여 저장' 혹은 기존 저장경로에 데이터 수정을 해주는 것이다.

예시)

1. .setValue(data)

만약 새로 추가하려는 data에 (name, age, address) 필드가 있다고 하자. 하지만 기존 경로에는 (name, age, address, major, school) 필드가 있다. 만약, .setValue(data)로 기존 경로의 데이터를 수정하고자 한다면 데이터베이스에는 (name, age, address)의 필드 데이터만 저장된다. (major, school) 필드는 사라진다. 

-> 기존 데이터를 새로운 데이터로 통째로 바꿔치기한 것

 

2. .updateChildValues

새로 변경하려는 data에 (name, age, address, subMajor) 필드가 있다고 하자. 기존 경로에는 마찬가지로 (name, age, address, major, school) 필드가 있다. 만약, .updateChildValues(data)로 기존 경로의 데이터를 수정하고자 한다면 데이터베이스에는 (name, age, address, major, school, subMajor)의 필드 데이터가 저장된다.

-> .updateChildValues는 .setValue와 달리 통째로 바꾸는 것이 아니라 기존 데이터에 필요한 정보를 수정만 해주는 것이다.

 

전체 코드

 

 

// CarStore.swift

import Foundation
import FirebaseDatabase
import FirebaseDatabaseSwift

class CarStore: ObservableObject {
    @Published var cars: [Car] = []
    @Published var changeCount: Int = 0
    
    let ref: DatabaseReference? = Database.database().reference() // (1)
    
    private let encoder = JSONEncoder() // (2)
    private let decoder = JSONDecoder() // (2)
    
    func listenToRealtimeDatabase() {
        
        guard let databasePath = ref?.child("cars") else {
            return
        }
        
        databasePath
            .observe(.childAdded) { [weak self] snapshot, _ in
                guard
                    let self = self,
                    let json = snapshot.value as? [String: Any]
                else {
                    return
                }
                do {
                    let carData = try JSONSerialization.data(withJSONObject: json)
                    let car = try self.decoder.decode(Car.self, from: carData)
                    self.cars.append(car)
                } catch {
                    print("an error occurred", error)
                }
            }
        
        databasePath
            .observe(.childChanged){[weak self] snapshot, _ in
                guard
                    let self = self,
                    let json = snapshot.value as? [String: Any]
                else{
                    return
                }
                do{
                    let carData = try JSONSerialization.data(withJSONObject: json)
                    let car = try self.decoder.decode(Car.self, from: carData)
                    
                    var index = 0
                    for carItem in self.cars {
                        if (car.id == carItem.id){
                            break
                        }else{
                            index += 1
                        }
                    }
                    self.cars[index] = car
                } catch{
                    print("an error occurred", error)
                }
            }
        
        databasePath
            .observe(.childRemoved){[weak self] snapshot in
                guard
                    let self = self,
                    let json = snapshot.value as? [String: Any]
                else{
                    return
                }
                do{
                    let carData = try JSONSerialization.data(withJSONObject: json)
                    let car = try self.decoder.decode(Car.self, from: carData)
                    for (index, carItem) in self.cars.enumerated() where car.id == carItem.id {
                        self.cars.remove(at: index)
                    }
                } catch{
                    print("an error occurred", error)
                }
            }
        
        databasePath
            .observe(.value){[weak self] snapshot in
                guard
                    let self = self
                else {
                    return
                }
                self.changeCount += 1
            }
    }
    
    func stopListening() {
        ref?.removeAllObservers()
    }
    
    func addNewCar(car: Car) {
        self.ref?.child("cars").child("\(car.id)").setValue([
            "id": car.id,
            "name": car.name,
            "description": car.description,
            "isHybrid": car.isHybrid
        ])
    }
    
    func deleteCar(key: String) {
        ref?.child("cars/\(key)").removeValue()
    }
    
    func editCar(car: Car) {
        let updates: [String : Any] = [
            "id": car.id,
            "name": car.name,
            "description": car.description,
            "isHybrid": car.isHybrid
        ]
        
        let childUpdates = ["cars/\(car.id)": updates]
        for (index, carItem) in cars.enumerated() where carItem.id == car.id {
            cars[index] = car
        }
        self.ref?.updateChildValues(childUpdates)
        
    }
    
}

 


4. 예제 View 그리기

본 게시물에서 다루고자 하는 것은 Firebase Realtime Database이므로, 뷰에 대한 설명은 간략히 하도록 하겠다.

(1) ContentView.swift

// ContentView.swift

import SwiftUI

struct ContentView: View {
    @StateObject var carStore: CarStore = CarStore()
    
    var body: some View {
        NavigationStack {
        VStack{
                Text("데이터베이스 변경사항: \(carStore.changeCount)")
                List {
                    ForEach(carStore.cars, id: \.self) { car in
                        NavigationLink {
                            CarDetailView(carStore: carStore, selectedCar: car)
                        } label: {
                            ListCell(car: car)
                        }
                        
                        
                    }
                }
                .navigationBarTitle(Text("EV Cars"))
                .navigationBarItems(leading:
                                        NavigationLink(destination: AddNewCarView(carStore: carStore),
                                                       label: {
                    Text("추가")
                })
                )
                
            }
        }.onAppear {
            carStore.listenToRealtimeDatabase()
        }
        .onDisappear {
            carStore.stopListening()
        }
    }
    
}
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

struct ListCell: View {
    var car: Car
    
    var body: some View {
        HStack {
            Text(car.name)
        }
    }
}

 

ContentView

여기서 주목할 점은, onAppear을 통해서 carStore의 listenToRealtimeDatabase()를 호출시키고, onDisappear를 통해서 stopListening() 함수를 호출한다는 점이다.

기본적으로 .onAppear를 통해서 listenToRealtimeDatabase()가 호출되고 .value 를 통해서 무조건 한번 읽어오게 된다 따라서 changeCount의 값은 앱을 키자마자 1 증가하게된다.

 

(2) CarDetailView.swift

// CarDetailView.swift

import SwiftUI

struct CarDetailView: View {
    @Environment(\.presentationMode) var mode: Binding<PresentationMode>
    @ObservedObject var carStore : CarStore
    @State private var isOnEditCarView: Bool = false
    @State var selectedCar: Car
    
    var body: some View {
        Form {
            Section(header: Text("Car Details")) {
                Text(selectedCar.name)
                    .font(.headline)
                
                Text(selectedCar.description)
                    .font(.body)
                
                HStack {
                    Text("Hybrid").font(.headline)
                    Spacer()
                    Image(systemName: selectedCar.isHybrid ?
                          "checkmark.circle" : "xmark.circle" )
                }
                
                Button {
                    carStore.deleteCar(key: selectedCar.id)
                    self.mode.wrappedValue.dismiss()
                } label: {
                    Text("삭제하기")
                }
                                
                NavigationLink {
                    EditCarView(carStore: carStore, selectedCar: $selectedCar, isOnEditCarView: $isOnEditCarView)
                } label: {
                    Text("수정하기")
                }
            }
        }
    }
}

struct CarDetailView_Previews: PreviewProvider {
    static var previews: some View {
        CarDetailView(carStore: CarStore(), selectedCar: Car(id: "", name: "name", description: "des", isHybrid: false))
    }
}

CarDetailView

 

(3) AddNewCarView.swift

// AddNewCarView.swift

import SwiftUI

struct AddNewCarView: View {
    @ObservedObject var carStore : CarStore
    @State var isHybrid = false
    @State var name: String = ""
    @State var description: String = ""
    
    var body: some View {
        
        Form {
            Section(header: Text("Car Details")) {
                DataInput(title: "Model", userInput: $name)
                DataInput(title: "Description", userInput: $description)
                
                Toggle(isOn: $isHybrid) {
                    Text("Hybrid").font(.headline)
                }.padding()
            }
            
            Button {
                carStore.addNewCar(car: Car(id: UUID().uuidString, name: name, description: description, isHybrid: isHybrid))
            } label: {
                Text("Add Car")
            }
        }
    }
}


struct DataInput: View {
    
    var title: String
    @Binding var userInput: String
    
    var body: some View {
        VStack(alignment: HorizontalAlignment.leading) {
            Text(title)
                .font(.headline)
            TextField("Enter \(title)", text: $userInput)
                .textFieldStyle(RoundedBorderTextFieldStyle())
        }
        .padding()
    }
}

struct AddNewCarView_Previews: PreviewProvider {
    static var previews: some View {
        AddNewCarView(carStore: CarStore())
    }
}

AddNewCarView

 

(4) EditCarView.swift

// EditCarView.swift

struct EditCarView: View {
    @Environment(\.presentationMode) var mode: Binding<PresentationMode>
    @ObservedObject var carStore : CarStore
    @State var isHybrid = false
    @State var name: String = ""
    @State var description: String = ""
    @Binding var selectedCar: Car
    @Binding var isOnEditCarView: Bool

    var body: some View {
        VStack {
            DataInput(title: "Name", userInput: $selectedCar.name)
            DataInput(title: "Description", userInput: $selectedCar.description)
            
            Toggle(isOn: $selectedCar.isHybrid) {
                Text("Hybrid").font(.headline)
            }.padding()
            Button  {
                let editCar = Car(
                    id: selectedCar.id,
                    name: selectedCar.name,
                    description: selectedCar.description,
                    isHybrid: selectedCar.isHybrid
                )
                carStore.editCar(car: editCar)
                self.mode.wrappedValue.dismiss()
                isOnEditCarView.toggle()
            } label: {
                Text("확인")
            }
        }
    }
}


struct EditCarView_Previews: PreviewProvider {
    static var previews: some View {
        EditCarView(carStore: CarStore(), selectedCar: .constant(Car(id: "", name: "name", description: "des", isHybrid: false)), isOnEditCarView: .constant(true))
    }
}

EditCarView

 


5. 결과

(1) 앱에서 데이터 이벤트 발생

앱에서 데이터 추가

앱에서 데이터 추가할 때 실시간으로 변하는 저장소 상태이다.

사실 AddNewCarView에서 Add Car 버튼을 눌렀을 때 carStore의 cars 배열에 새로운 Car 인스턴스가 append 되는 코드는 없다.

그러면 데이터 베이스 저장소에 새로운 Car 인스턴스 정보를 추가하면, carStore의 cars에는 데이터가 어떻게 추가가 되는 걸까?

 

바로 carStore의 listenToRealtimeDatabase() 함수 덕분이다.

내가 데이터베이스 저장소에 데이터를 추가했더라도, cars에 새로운 Car 데이터가 없으면 리스트를 통해 추가된 인스턴스를 보여주지 못하는데, 내가 데이터베이스 저장소에 데이터를 추가한 것을 리스너 함수가 감지하고 바로 .observe(.childAdded)가 실행되어 carStore의 cars 배열에 새로 추가된 인스턴스가 append 된 것이다.

 

시나리오를 쉽게 풀어보면,

carStore 내부에 listenToRealtimeDatabase 함수와 addNewCar 함수가 있을 때,

  • addNewCar는 '저장소에 새로운 Car 데이터를 추가해야지~' 라고 하면서 새로운 Car 데이터를 JSON 형식으로 추가해주었다.
  • listenToRealtimeDatabase는 '내 경로에 어떤 데이터가 변경되는지 찾아내겠어! // 어? addNewCar가 새로운 데이터를 추가했네? 이거 바로 전달해줘야지!' 

위와 같은 과정으로 carStore.cars에 새로운 데이터가 append된 것이다.

 

 

앱에서 데이터 수정

AddNewCar와 마찬가지로 앱에서 데이터가 수정되면, carStore의 editCar(car: Car) 함수가 실행되고 수정된 데이터 경로를 관찰하고 있는 listenToRealtimeDatabase가 데이터가 수정된 이벤트를 확인하고 .observe(.childUpdated) 블록 내 코드가 실행된다.

 

 

앱에서 데이터 삭제

데이터 삭제시에는 carStore의 deleteCar(key: String)의 함수가 실행되고 삭제된 데이터 경로를 관찰하고 있는 listenToRealtimeDatabase가 데이터가 삭제된 이벤트를 확인하고 .observe(.childDelete) 블록 내 코드가 실행된다.

 

(2) 데이터베이스 저장소에서 데이터 이벤트 발생

 

저장소에서 데이터 수정
저장소에서 데이터 삭제

데이터베이스 저장소에서 직접 데이터를 수정, 삭제 하여도 해당 이벤트를 carStore의 listenToRealtimeDatabase() 함수가 관찰하여 바로 해당 이벤트에 맞는 코드를 실행한다.