[SwiftUI] - Cloud Firestore에 Data를 Create, Read (VOL. 1)

Ben·2023년 5월 23일
0

iOS

목록 보기
3/10
post-thumbnail

최근, Apple에서 제공하는 SwiftUI Tutorials - Landmarks 프로젝트에 Firebase 연동 작업을 진행해보고 있다.

그러다가 Cloud Firestore의 Data를 관리하는 과정에 있어 Create, Read 하는 과정을

  • completionHandler 를 이용했을 때
  • async / await 를 이용했을 때

이 두 가지 경우를 모두 사용해보고 싶었다.

각각 어떻게 구현했는지에 대해 알아보고
그 과정에서 마주한 에러를 어떻게 처리했는지에 대해 작성해 보려고한다.

✋ 시작하기 앞서 아래의 조건이 만족한다는 전제하에 진행되었다.

  • Landmarks 프로젝트가 다 완성되었다는 점
  • Firebase SDK가 추가되어 세팅이 완료되었다는 점
  • Firestore에 아무런 데이터가 존재하지 않아야한다는 점

+ VOL. 1에서는 'Create' 를, VOL. 2 에서는 'Read' 를 다룰 예정이다.

Overview

Cloud Database인 Firestore의 Data를 Create 할 수 있다.

❗ SwiftUI Framework와 MVVM Design Patterm 기반으로 작성되었습니다.

Implementation

  • ModelData
import Combine

final class ModelData: ObservableObject {
	
	// MARK: - Cloud Firestore Database, Collection Reference constants
    static private let database: Firestore = Firestore.firestore()
    private let collectionRef: CollectionReference = ModelData.database.collection("Landmarks")
    
    //	생략
    
    // MARK: - Published-Prop
	@Published var landmarks: [Landmark] = []	//	기존: @Published var landmarks: [Landmark] = load("landmarkData.json")
    
    // MARK: - Stored-Prop
    var decodedLandmarks: [Landmark] = load("landmarkData.json")
}

먼저 게시 프로퍼티인 landmarks 변수를 빈 배열로 바꿔주었다.
그리고 load(_ filename: String) 함수로 가져온 JSON 데이터를 decodedLandmarks라는 새로운 변수를 만들어 할당했다.


  • (Method) uploadDataToFirestore()
extension ModelData {

	// MARK: - Used completionHandler
    private func uploadDataToFirestore() throws -> Void {
        
        decodedLandmarks.forEach { landmark in
            
            // MARK: - setData()
            collectionRef.document(landmark.name).setData([
                "name" : landmark.name,
                "category" : landmark.category.rawValue,
                "city" : landmark.city,
                "state" : landmark.state,
                "id" : landmark.id,
                "isFeatured" : landmark.isFeatured,
                "isFavorite" : landmark.isFavorite,
                "park" : landmark.park,
                "description" : landmark.description,
                "coordinates" : ["latitude": landmark.locationCoordinate.latitude,
                                 "longitude": landmark.locationCoordinate.longitude]
            ]) { error in
                
                if let error: Error = error {
                    
                    print("Error writing document: \(error.localizedDescription)")
                } else {
                    
                    print("\(landmark.id): Document successfully written!")
                }
            }
        }
        
        //	생략
        
        return
    }
}

기존의 load한 JSON Data를 Firestore에 Data를 추가하는 함수다.
이 메서드는 최초 단, 한 번만 실행시킬 메서드이다.

.forEach() 메서드를 적용시켜 배열 안의 객체 element를 Firestore에 저장시켜주었다.

그러면 Firestore에 이러한 형태로 저장이된다.

.setData() 메서드의 경우

함수 정의를 보면,
2가지의 Params를 가지고 있어서 literal 값인 documentData Parameter에
각각의 document 값들을 Key-Value 형식의 Dictionary로 지정해주면 된다.

그리고 completion 즉, trailing-closure에서 error에 대한 처리를 해주면 된다.

console에 각각의 landmark가 잘 저장되었음을 확인할수 있고,
Firestore의 document에도 각각의 Key-Value가 잘 저장되었다!

그러나 setData() 를 사용하면서 알아낸 사실이 있었다.

다양한 파라미터를 받는 setData() 메서드가 존재하지만
위 코드에서 사용한 메서드는 [String : Any] 타입의 딕셔너리 형식의 literal 값을 이용한 메서드였고,

이 메서드들 즉, from 이라는 매개변수를 갖는 함수를 사용하게 되면, Encodable 프로토콜을 채택받은 객체를 저장시킬수가 있다.

쉽게말해,
setData(_ documentData: [String : Any]) 메서드를 호출시
저장시키려는 데이터를 커스터마이징 해서 저장시킬수가 있고

setData(from: Encodable) 메서드를 호출하면
JSON 데이터 그 자체의 모든 Key-Value를 저장시킬수 있다.

setData(from: Encodable) 메서드로 호출을 해보았다.

do {

	try collectionRef.document(landmark.name).setData(from: landmark)
                
	print("from 파라미터를 가지는 setData() 메서드의 결과")
	print("\(landmark.id) Document successfully written! \n")
} catch {
                
	 print("Error writing document: \(error.localizedDescription)")
}

setData(_ documentData: [String : Any])
setData(form: Encodable) 의 결과를 비교해보면

imageName 필드가 하나 더 추가된 것을 확인할 수 있고,
형식마저 'landmarkData.json'의 JSON 데이터와 동일하다.

이번엔, uploadDataToFirestore() 메서드를 async / await 키워드를 붙혀 처리해 보고 싶었다.

Trouble-shooting 💥

  • (Method) uploadDataToFirestoreWithAsyncAwait()
// MARK: - Used 'Async/Await'
func uploadDataToFirestoreWithAsyncAwait() async throws -> Void {
	
    for landmark in decodedLandmarks {
            
		do {
                
			try await collectionRef.document(landmark.name).setData([
				"name" : landmark.name,
                "category" : landmark.category.rawValue,
                "city" : landmark.city,
                "state" : landmark.state,
                "id" : landmark.id,
                "isFeatured" : landmark.isFeatured,
                "isFavorite" : landmark.isFavorite,
                "park" : landmark.park,
                "description" : landmark.description,
                "coordinates" : ["latitude": landmark.locationCoordinate.latitude,
                                     "longitude": landmark.locationCoordinate.longitude]
            ])
                
            print("Document successfully written!")
        } catch {
                
            print("Error writing document: \(error.localizedDescription)")
        }
    }
    
    //	생략
}

.forEach() 부분을 for ~ in 으로 바꿔주었다.

처음에는 .forEach() 메서드로 접근을 했었다.
그러나 async 키워드가 붙은 .setData() 메서드를 사용하려는 순간,

컴파일 에러가 발생했다.
원인은 Swift 자체적으로 '.forEach()의 trailing-closure 안에서는 비동기를 지원하지 않기에 실행을 보장할 수 없고, 동기적으로 동작하기 때문' 이라고 한다.

ref ) https://stackoverflow.com/questions/73717218/why-is-it-not-possible-to-use-foreach-in-async-function

결국 for ~ in 루프로 접근해
setData(_ documentData: [String : Any]) async throws 메서드를 사용하니

문제를 해결할 수 있었다.

VOL. 2 에서는 Data를 Read (이하 Fetch) 하는법에 대해 작성할 예정이다.

끝ㅌ!

profile
 iOS Developer

0개의 댓글