Notice
Recent Posts
Recent Comments
Link
관리 메뉴

김종권의 iOS 앱 개발 알아가기

[iOS - swift] Firebase Firestore 개념, 실시간 데이터 사용 방법 본문

iOS 응용 (swift)

[iOS - swift] Firebase Firestore 개념, 실시간 데이터 사용 방법

jake-kim 2021. 11. 22. 23:30

Firestore란?

  • NoSQL 형태의 클라우드 데이터 저장소
  • REST API와 같은 것을 쓰고, 데이터를 gRPC이나 웹 소켓과 같이 stream형태로 받고 싶은 경우 사용
  • DB에 "구독"을 할 수 있는 개념이 있어서, 앱이 DB에 구독을 하고 있을 때 서버에서 DB의 수정이 생기면 자동으로 앱에 데이터를 넘겨주는 시스템 존재

Firebase Console 앱 준비

  • Firebase 앱 생성

  • GoogleService-Info.1plist 다운 후 프로젝트에 추가

  • [Firestore Database - 데이터베이스 만들기] 클릭

  • 테스트 모드에서 시작 클릭
    • 이후 모두 디폴트로 하고 완료

프로젝트에 Firestore 연동

  • Firestore SDK 다운 (SPM으로 아래 링크로 설치)
github.com/firebase/firebase-ios-sdk.git
  • FirebaseFirestore, FirebaseFirestoreSwift-Beta 체크

  • Firebase 초기화
// AppDelegate.swift

class AppDelegate: UIResponder, UIApplicationDelegate {

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        
        FirebaseApp.configure() // <- 추가
        ...

 Firestore 데이터 모델

  • 일반 DB처럼 테이블 형태가 아닌 Json 형태
    • Json 데이터형태 이므로 앱에서 Firestore에 접근하여 쓰기할땐 [String: Any]형태로 쓰기
  • 핵심 두 가지 개념
    • Collection: 컨테이너
    • Document: 데이터

ex) channel/{id}/thread 컨테이너에, Chatting내용을 저장하고 싶은 경우

- Collection: channel / {id} / thread

- Document: {id: "97bef", content: "안녕", sentDate: "2021년"}

 

> 최종적으로 만들어진 데이터 구조는 아래와 같이 구성

Firestore에 사용될 데이터 모델 

  • struct to [String: Any] 변환 프로퍼티를 Encodable에 extension으로 추가
extension Encodable {
    /// Object to Dictionary
    /// cf) Dictionary to Object: JSONDecoder().decode(Object.self, from: dictionary)
    var asDictionary: [String: Any]? {
        guard let object = try? JSONEncoder().encode(self),
              let dictinoary = try? JSONSerialization.jsonObject(with: object, options: []) as? [String: Any] else { return nil }
        return dictinoary
    }
}
  • 모델 생성
    • Codable을 준수하게하여 decode, encode가 모두 가능하도록 설정
    • decode: data to object (바이너리 데이터를 객체로 변환하여 데이터를 수신한 경우 사용)
    • encode: object to data (객체를 바이너리 데이터로 변환하여 N/W, DB등에 입력할때 사용)
struct Message: Codable {
    let id: String
    let content: String
    let sentDate: Date
    
    init(id: String, content: String) {
        self.id = id
        self.content = content
        self.sentDate = Date()
    }
}

extension Message: Comparable {
    // 같은값이 있는지 비교할때 사용
    static func == (lhs: Message, rhs: Message) -> Bool {
        return lhs.id == rhs.id
    }

    // sort 함수에서 사용
    static func < (lhs: Message, rhs: Message) -> Bool {
        return lhs.sentDate < rhs.sentDate
    }
}
  • Date 형을 firestore에 입력하면 Unix Time Stamp형으로 변환되므로 decode형식 변환이 필요
    • Codable은 CodingKeys 프로퍼티와 init(from decoder: Decoder) 생성자가 내부적으로 불리고 있는데, 이곳에서 init(from decoder: Decoder)를 재정의해주면 해결 가능
struct Message: Codable {
    let id: String
    let content: String
    let sentDate: Date
    
    init(id: String, content: String) {
        self.id = id
        self.content = content
        self.sentDate = Date()
    }
    
    // MARK: - Date 형을 firestore에 입력하면 Unix Time Stamp형으로 변환하는 작업
    
    private enum CodingKeys: String, CodingKey {
        case id
        case content
        case sentDate
    }
    
    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        id = try values.decode(String.self, forKey: .id)
        content = try values.decode(String.self, forKey: .content)
        
        let dataDouble = try values.decode(Double.self, forKey: .sentDate)
        sentDate = Date(timeIntervalSince1970: dataDouble)
    }
}

Firestore 접근 모듈

  • Firestore에 접근하는 클래스 생성
import FirebaseFirestore
import FirebaseFirestoreSwift

final class MyFirestore {

}
  • documentListener를 class의 전역으로 선언해놓고 listener를 언제든 remove할 수 있도록 설정
private var documentListener: ListenerRegistration?
  • Firebase에 접근하여 데이터를 저장하는 메소드 구현 save(_:completion:)
    • collectionPath를 설정한 후 이곳에다가 document 입력
func save(_ message: Message, completion: ((Error?) -> Void)? = nil) {
    let collectionPath = "channels/\(message.id)/thread"
    let collectionListener = Firestore.firestore().collection(collectionPath)
    
    guard let dictionary = message.asDictionary else {
        print("decode error")
        return
    }
    collectionListener.addDocument(data: dictionary) { error in
        completion?(error)
    }
}
  • Firestore에 접근하여 실시간으로 데이터를 가져오는 메소드 구현 subscribe(id:completion:)
func subscribe(id: String, completion: @escaping (Result<[Message], FirestoreError>) -> Void) {
    let collectionPath = "channels/\(id)/thread"
    removeListener()
    let collectionListener = Firestore.firestore().collection(collectionPath)
    
    documentListener = collectionListener
        .addSnapshotListener { snapshot, error in
            guard let snapshot = snapshot else {
                completion(.failure(FirestoreError.firestoreError(error)))
                return
            }
            
            var messages = [Message]()
            snapshot.documentChanges.forEach { change in
                switch change.type {
                case .added, .modified:
                    do {
                        if let message = try change.document.data(as: Message.self) {
                            messages.append(message)
                        }
                    } catch {
                        completion(.failure(.decodedError(error)))
                    }
                default: break
                }
            }
            completion(.success(messages))
        }
}
  • documentListener 구독을 해지시키는 함수 추가
func removeListener() {
    documentListener?.remove()
}
  • 전체 MyFirestore 클래스 코드
import FirebaseFirestore
import FirebaseFirestoreSwift

final class MyFirestore {
    
    private var documentListener: ListenerRegistration?
    
    func save(_ message: Message, completion: ((Error?) -> Void)? = nil) {
        let collectionPath = "channels/\(message.id)/thread"
        let collectionListener = Firestore.firestore().collection(collectionPath)
        
        guard let dictionary = message.asDictionary else {
            print("decode error")
            return
        }
        collectionListener.addDocument(data: dictionary) { error in
            completion?(error)
        }
    }

    func subscribe(id: String, completion: @escaping (Result<[Message], FirestoreError>) -> Void) {
        let collectionPath = "channels/\(id)/thread"
        removeListener()
        let collectionListener = Firestore.firestore().collection(collectionPath)
        
        documentListener = collectionListener
            .addSnapshotListener { snapshot, error in
                guard let snapshot = snapshot else {
                    completion(.failure(FirestoreError.firestoreError(error)))
                    return
                }
                
                var messages = [Message]()
                snapshot.documentChanges.forEach { change in
                    switch change.type {
                    case .added, .modified:
                        do {
                            if let message = try change.document.data(as: Message.self) {
                                messages.append(message)
                            }
                        } catch {
                            completion(.failure(.decodedError(error)))
                        }
                    default: break
                    }
                }
                completion(.success(messages))
            }
    }
    
    func removeListener() {
        documentListener?.remove()
    }
}

사용하는 쪽

  • firestore에 저장
let myFirestore = MyFirestore()

...

@objc private func didTapSentButton() {
    guard let content = communicationTextField.text else { return }
    let message = Message(id: "123", content: content)
    
    myFirestore.save(message) { error in
        print("error: \(error)")
    }
}
  • firestore 구독, 데이터 로드
private func subscribeFirestore() {
    myFirestore.subscribe(id: "123") { [weak self] result in
        switch result {
        case .success(let messages):
            messages.forEach {
                self?.firestoreTextView.textColor = .darkGray
                self?.firestoreTextView.text = $0.content + "\n" + (self?.firestoreTextView.text ?? "")
            }
        case .failure(let error):
            print(error)
        }
    }
}

Firestore에 저장, 로드
실시간 Firestore 데이터 갱신

 

* 전체 소스 코드: https://github.com/JK0369/ExFirestore

 

* 참고

- https://firebase.google.com/docs/firestore/data-model?hl=ko 

- https://firebase.google.com/docs/firestore/quickstart?hl=ko 

Comments