Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
Tags
- Xcode
- SWIFT
- swift documentation
- 리팩토링
- UITextView
- 클린 코드
- uiscrollview
- collectionview
- tableView
- 스위프트
- 애니메이션
- 리펙토링
- rxswift
- uitableview
- RxCocoa
- UICollectionView
- Observable
- Clean Code
- Protocol
- Refactoring
- Human interface guide
- ios
- MVVM
- 리펙터링
- combine
- map
- clean architecture
- HIG
- ribs
- swiftUI
Archives
- Today
- Total
김종권의 iOS 앱 개발 알아가기
[iOS - swift] Firebase Firestore 개념, 실시간 데이터 사용 방법 본문
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)
}
}
}
* 전체 소스 코드: https://github.com/JK0369/ExFirestore
* 참고
- https://firebase.google.com/docs/firestore/data-model?hl=ko
- https://firebase.google.com/docs/firestore/quickstart?hl=ko
'iOS 응용 (swift)' 카테고리의 다른 글
Comments