본문 바로가기

프로젝트/ChatApp ver.1 (w/Firebase)

03. Firebase 초기화 및 Swift에서 사용하기

Firebase 초기화 하기


백엔드인 Firebase가 메시지를 저장할 수 있도록 DB를 초기화해 준다.
따로 서비스를 염두해 둔 앱이 아니기 때문에 간편하게 테스트 모드에서 시작한다.

사용하는 Region은 'asia-northeast3'로 한국에 위치해 있다.
Region은 서비스의 응답 속도에 영향을 끼치는 경우가 있기 때문에 가까운 곳을 고르는 것이 좋다.
Region은 Firestore를 초기화 한 이후 변경할 수 없으므로 잘 고르도록 하자.

 

Cloud Firestore 위치  |  Firebase

2022년 10월 18일에 오프라인과 온라인으로 진행될 Firebase Summit에 참여하세요. Firebase로 앱을 빠르게 개발하고 안심하고 앱을 출시하며 손쉽게 확장하는 방법을 알아보세요. 지금 등록하기 의견 보

firebase.google.com

Firestore의 Region의 위치와 이름은 위의 링크에서 확인할 수 있다.

Firestore의 데이터 구조

Firestore는 collection이라는 폴더 안에
document라는 문서가 존재하고, 그 안에 데이터를 저장하는 구조이다.

즉 우리는 messages라는 '이름(collection)'으로 데이터를 주고받게 되며,
messages의 형식은 document로, 이 각각의 document 까지 접근해야 비로소 주고받은 문자의 이름이나 보낸 일시 등을 확인할 수 있다.

새 Collection을 생성한다.
CollectionID는 앱에서 쉽게 접근할 수 있도록 명확한 의미로 지정한다.

Document는 이야기가 조금 다르다.
채팅 내역이나 전송되는 데이터가 쌓이게 되면 이들을 구별하기 위한 ID가 대량으로 필요하게 되는데,
이를 일정한 규칙이나 일일이 지정한다는 것은 불가능에 가깝다.

따라서 Firebase의 무작위 생성 기능을 사용하거나 앱에서 전달할 때도 무작위로 이름을 저장하는 방식을 사용한다.
messages Collection의 형식은 다음과 같다.

id String 메세지 식별자
text String 메세지 내용
received Boolean 메세지 수발신 여부
timestamp timestamp 메세지 생성 시점

 

Swift에서 사용하기


Firebase Querying

프로젝트 내에 새로운 Swift 파일을 생성한다.
해당 파일은 Firebase와 연결돼 전송되는 모든 것들을 관리하는 역할을 할 예정이다.

import Foundation
import FirebaseFirestore
import FirebaseFirestoreSwift

우선 Firebase를 사용하기 위해 앞서 설치한 FirebaseFirestore와 FirebaseFirestoreSwift를 import 한다.

class MessagesManager: ObservableObject {
	@Published private(set) var messages: [Message] = []
}

Published 변수를 생성한다.
해당 변수는 메세지를 받아오는 변수로 Message 구조체의 배열로 이루어져 있다.
실제로 메시지를 표시할 ContentView에서는 이 변수를 감시하며 변화가 있을 때마다 새로고침 하게 될 것이다.

class MessagesManager: ObservableObject {
	@Published private(set) var messages: [Message] = []
    
	let db = Firestore.firestore()
}

Firestore를 사용하기 위해 firestore 인스턴스를 하나 생성한다.

class MessagesManager: ObservableObject {
	@Published private(set) var messages: [Message] = []
    
	let db = Firestore.firestore()
	
	func getMessages() {
		db.collection("messages").addSnapshotListener { querySnapshot, error in
			guard let documents = querySnapshot?.documents else {
				print("Error fetching documents: \(String(describing: error))")
				return
			}
		}
	}
}

Firebase에서 메시지를 받아 올 getMessages() 메서드를 정의한다.

firestore 인스턴스의 'messages' collection에 접근해 데이터를 받아오는 역할로,
성공하면 받아오는 데이터는 querySnapshot으로, 실패 시 오류는 error로 전달된다.
받아온 데이터를 documents로 바인딩하는 데 성공하면 다음으로 이어가고,
실패한다면 error를 출력하고 동작을 종료한다.

class MessagesManager: ObservableObject {
	@Published private(set) var messages: [Message] = []
    
	let db = Firestore.firestore()
	
	init() {
		getMessages()
	}
	
	func getMessages() {
		db.collection("messages").addSnapshotListener { querySnapshot, error in
			guard let documents = querySnapshot?.documents else {
				print("Error fetching documents: \(String(describing: error))")
				return
			}
			
			self.messages = documents.compactMap { document -> Message? in
				do {
					return try document.data(as: Message.self)
				} catch {
					print("Error decoding document into Message: \(error)")
					return nil
				}
			}
		}
	}
}

다음은 바인딩된 documents를 실제 Message 구조체 형식의 messaged 배열로 변환하는 과정으로,
성공하면 완전히 변환된 messages 배열을 반환하지만, 실패하면 nil을 반환함과 동시에 에러를 출력한다.

 

데이터 출력하기


View에 연결하기

struct ContentView: View {
	@StateObject var messagesManager = MessagesManager()
	
    var body: some View {
		VStack {
			VStack {
				TitleRow()
				
				ScrollViewReader { proxy in
					ScrollView {
						ForEach(messagesManager.messages, id: \.id) { message in
							MessageBubble(message: message)
						}
					}
					.padding(.top, 10)
					.background(.white)
					.cornerRadius(30, corners: [.topLeft, .topRight])
					.onChange(of: messagesManager.lastMessageId) { id in
						withAnimation {
							proxy.scrollTo(id, anchor: .bottom)
						}
					}
				}
			}
			.background(Color("Yellow"))
			
			MessageField()
		}
		.onTapGesture {
			hideKeyboard()
		}
    }
}

그다음에는 데이터를 실제로 출력할 ContentView에서 MessagesManager의 결과를 확인하도록 조치하기만 하면 된다.
더미 데이터가 아닌 실제 데이터를 출력하기 위해 StateObject 변수를 생성하고 MessagesManager 인스턴스를 생성한다.
이후, 데이터를 열거하며 출력하는 ForEach에서 messagedManager의 데이터에 접근하기만 하면 된다.

데이터 정렬하기

문제는 이렇게 가져온 데이터가 전송된 순서가 아닌 무작위로 배열이 된다는 점인데,
이를 해결하기 위한 두 가지 방법이 존재한다.

 

Apple Developer Documentation

 

developer.apple.com

첫 번째 방법은 sorted 메서드를 사용하는 것이다.
해당 메서드는 전달된 배열을 훼손하지 않고 새롭게 정렬된 배열을 반환한다는 특징이 있다.
원본을 훼손시키지 않는다는 장점이 있지만, 별도의 배열을 따로 생성한다는 점에서 자원을 더 많이 사용한다는 단점이 있다.

 

Apple Developer Documentation

 

developer.apple.com

두 번째 방법은 sort 메서드이다.
해당 메서드는 전달된 배열 자체를 정렬한다는 특징이 있다.
결과적으로 원본을 훼손하게 되지만 자원을 조금 더 효율적으로 사용한다는 장점이 있다.

import Foundation
import FirebaseFirestore
import FirebaseFirestoreSwift

class MessagesManager: ObservableObject {
	@Published private(set) var messages: [Message] = []
    
	let db = Firestore.firestore()
	
	init() {
		getMessages()
	}
	
	func getMessages() {
		db.collection("messages").addSnapshotListener { querySnapshot, error in
			guard let documents = querySnapshot?.documents else {
				print("Error fetching documents: \(String(describing: error))")
				return
			}
			
			self.messages = documents.compactMap { document -> Message? in
				do {
					return try document.data(as: Message.self)
				} catch {
					print("Error decoding document into Message: \(error)")
					return nil
				}
			}
			
			self.messages.sort { $0.timestamp < $1.timestamp }
		}
	}
}

이번엔 간단하게 sort 메서드를 사용했다.
배열상 앞에 존재하는 데이터가 더 작도록 정렬을 시도하게 되므로 timestamp 기준으로 오래된 데이터가 더 앞에 올 수 있게 된다.

'프로젝트 > ChatApp ver.1 (w/Firebase)' 카테고리의 다른 글

05. 더 나아가기  (0) 2022.10.15
04. Firebase에 쓰기  (0) 2022.10.15
02. Firebase 연결하기  (0) 2022.10.13
01. 인터페이스 디자인  (0) 2022.10.11
00. 시작하며  (0) 2022.10.11