Firebase는 제작한 어플리케이션을 구동하기 위해 필요한 클라우드 서비스를 무료로 제공하기 때문에, 실제 구동 테스트를 할 때 용이하게 사용할 수 있다.
이번에 만들어 본 어플리케이션은 게시글을 업로드하는 기능과 피드에서 업로드 한 게시물들을 볼 수 있는 페이지가 있으므로 간단하게 Firebase를 이용해보려한다. 일차적으로 구현하고자 하는 기능은 다음과 같다.
- 복잡한 로그인 대신 익명으로 로그인하여 게시글 작성 등의 권한을 얻는다(Auth)
- 게시글을 업로드 하되, 사진도 같이 업로드 할 수 있게 한다(Firestore, Storage)
- 피드에서는 업로드 된 모든 게시물이 표시될 수 있게 한다(Fetching Data from above)
- 게시물을 삭제하는 기능을 추가한다(Delete)
1. 익명의 권한 획득
Authentication > Sign in method > 익명 을 활성화시켜준다.
다음은 로그인 코드를 작성한다. 코드는 로그인 함수를 만들고 버튼을 누르거나, 뷰가 생성될 시 호출될 수 있게 배치한다.
func login() {
Auth.auth().signInAnonymously { (res, err) in
if err != nil {
print(err!.localizedDescription)
return
}
print("Login Success: \(res!.user.uid)")
}
}
2. 게시글 업로드
< Trial 1: Firestore 이용 >
우선 Firebase를 떠나서, Cloud Service에 대한 지식이 부족한 상태임으로 이런 저런 검색 끝에 Firestore을 사용하기로 했다. Firestore은 NoSQL Cloud Service이다. 공식 문서에는 다음과 같이 소개되어 있다.
Cloud Firestore는 Firebase 및 Google Cloud의 모바일, 웹, 서버 개발에 사용되는 유연하고 확장 가능한 데이터베이스입니다. Firebase 실시간 데이터베이스와 마찬가지로 실시간 리스너를 통해 클라이언트 애플리케이션 간에 데이터의 동기화를 유지하고 모바일 및 웹에 대한 오프라인 지원을 제공해 네트워크 지연 시간이나 인터넷 연결에 상관없이 원활하게 반응하는 앱을 개발할 수 있습니다.
음 .. 굉장히 좋아보인다. 실제로 Firestore을 이용해 보니 게시글 업로드와 동시에 피드에 게시물이 나타나는 것을 확인할 수 있었다.
< Trial 2: Firestore + Storage >
하지만 Firestore을 통해서 사진을 업로드하는 방법을 찾다가 애초에 사진 등의 용량이 큰 데이터를 저장하고 관리하는 db가 아니라는 사실을 알게되었다. 그래서 사진은 storage에 저장하되, 사진의 이름을 id로 만들고, 그 id를 db에 저장하여 참조하는 형식으로 만들기로 했다. 그러기 위해 일단 firestore에 "Post"라는 이름의 "컬렉션"을 생성해준다. 여기에 하나의 게시물을 하나의 "문서"로 만들고 그 안에 필드들에 제목, 설명, 이미지 id 등의 속성을 저장한다.
그리고 Storage로 이동하여 간단하게 images라는 폴더를 하나 생성해 준다. 게시물을 작성하면, post id를 이름으로 하는 하위 폴더를 생성하여 그 내부에 사진을 저장할 것이다.
우선, 게시물을 다루기 때문에 Post Model을 구현한다.
import SwiftUI
struct Post: Identifiable {
var id = UUID().uuidString
var title: String = ""
var description: String = ""
var timeStamp: Date = Date()
var images: [String]
// For easy upload to firebase
var dictionary: [String: Any] {
return [
"id": id,
"title": title,
"description": description,
"timeStamp": timeStamp,
"images": images,
]
}
}
주의할 점은 images에는 이미지의 id가 저장된다.
다음은 게시물을 업로드 하는 코드이다. 이제부터의 코드는 따로 ViewModel 파일을 만들어 그 안에 작성했다.
import Firebase
import FirebaseStorage // for saving image to Storage
...
private var db = Firestore.firestore()
...
func uploadPost(title: String, description: String, images: [UIImage]) {
// when uploading, init image reference name
let postId = UUID().uuidString
// create image name list
var imgNameList: [String] = []
// iterate over images
for img in images {
let imgName = UUID().uuidString
imgNameList.append(imgName)
uploadImage(image: img, name: (postId + "/" + imgName))
}
// create post object
let post = Post(id: postId, title: title, description: description, timeStamp: Date(), images: imgNameList)
// Storing to DB
let _ = db.collection("Post").document(postId).setData(post.dictionary)
}
코드는 간단하다. 우선 업로드 함수를 만들고, 저장할 데이터를 인자로 받도록 설정한다. 내부에서는 이미지마다 id를 만든후 Post 객체를 생성한 후 업로드한다. 이미지를 storage에 저장하는 uploadImage함수는 다음과 같다. 하나 주의할 점은 uploadImage 함수의 name parameter에 full reference path를 넘겨주게 했다. 위의 코드는 "/"를 통해 postId 폴더의 imageName이라는 이름으로 저장할 수 있게 된다.
...
private var storage = Storage.storage()
...
// function works for each image
func uploadImage(image: UIImage, name: String) {
let storageRef = storage.reference().child("images/\(name)")
let data = image.jpegData(compressionQuality: 0.1)
let metadata = StorageMetadata()
metadata.contentType = "image/jpg"
// uploda data
if let data = data {
storageRef.putData(data, metadata: metadata) { (metadata, err) in
if let err = err {
print("err when uploading jpg\n\(err)")
}
if let metadata = metadata {
print("metadata: \(metadata)")
}
}
}
}
3. 게시글 보기: Fetching data
저장된 데이터를 앱에서 불러오는 방법도 비슷하다.
함수가 구현된 ViewModel을 ObservableObject 로 만들었기 때문에, fetching할 때는 단순히 posts 변수에 데이터를 할당하는 것이 목적이 된다.
func fetchPost() {
db.collection("Post").addSnapshotListener { [self] (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No Documents")
return
}
self.posts = documents.map({ (queryDocumentSnapshot) -> Post in
let data = queryDocumentSnapshot.data()
let id = data["id"] as? String ?? "blank id"
let title = data["title"] as? String ?? "blank title"
let description = data["description"] as? String ?? "blank description"
let timeStamp = data["timeStamp"] as? Date ?? Date()
let images = data["images"] as? [String] ?? []
// fetch image set
for imageName in images {
fetchImage(postId: id, imageName: imageName)
}
return Post(id: id, title: title, description: description, timeStamp: timeStamp, images: images)
})
}
}
이제 이미지를 storage에서 fetch하는 코드를 마저 작성해준다.
func fetchImage(postId: String, imageName: String) {
let ref = storage.reference().child("images/\(postId)/\(imageName)")
// Download in memory with a maximum allowed size of 1MB (1 * 1024 * 1024 bytes)
ref.getData(maxSize: 1 * 1024 * 1024) { data, error in
if let error = error {
print("error while downloading image\n\(error.localizedDescription)")
return
} else {
let image = UIImage(data: data!)
self.imageDict[imageName] = image
}
}
}
4. 게시물 삭제: Delete
// remove post
func deletePost(post: Post) {
// remove from firebase db (firestore)
db.collection("Post").document(post.id).delete() { err in
if let err = err {
print("Error removing document: \(err.localizedDescription)")
} else {
print("Document successfully removed!")
}
}
// remove photos from storage
let imagesRef = storage.reference().child("images/\(post.id)")
imagesRef.delete { error in
if let error = error {
print("Error removing image from storage\n\(error.localizedDescription)")
} else {
print("images directory deleted successfully")
}
}
// remove from posts list
let index = posts.firstIndex { currentPost in
return currentPost.id == post.id
} ?? 0
// remove post from posts variable
posts.remove(at: index)
}
삭제 기능도 upload, fetch와 비슷하게 reference 객체를 만들어 간편하게 구현할 수 있다. 우선 db의 document 참조 객체를 만들어 delete() 메서드를 이용하면 참조하고 있는 데이터가 삭제된다. 이후, storage에 이미지가 저장되어 있고, Post 객체의 images에는 이미지의 이름들이 저장되어 있으므로 이를 이용해 참조 객체를 만들고 이미지도 삭제한다. 마지막으로 ViewModel 내부의 posts 에서 해당 객체를 제거해 주면 끝이다.
ios15, xcode13 부터는 async await 등의 함수를 간편하게 이용할 수 있는 듯하다.
관련 공부를 하고, 개념과 사용 법을 정리해야겠다.
+ async
+ await
'SwiftUI > 기타' 카테고리의 다른 글
SwiftUI에서 UIKit 이용하기: UIViewRepresentable (0) | 2021.08.10 |
---|---|
SwiftUI 내장 이미지 이용하기 (0) | 2021.08.10 |