본문 바로가기

iOS/DataParsing

[iOS] FireStore Database document에 값을 Codable타입으로 디코딩, 인코딩으로 저장, 꺼내오기!!

728x90

 이번 글을 통해 Codable의 Decode를 통해 파이어베이스 Firestore Database의 collection document 필드 값을 쿼리를 통해 인스턴스로 얻거나 Encode를 통해 파이어베이스의 문서와 필드값을 추가하는 글을 다뤄볼 것이다.

 

사진1

 사진 1과 같이 파이어베이스에 저장된 형식을 멤버 프로퍼티로 하는 구조체를 Codable타입으로 생성하면 된다. 이때 Encodable과 Decodable을 구현해야 한다. 파이어베이스를 이번 인스타그램 클론을 하면서 처음 다뤄보는데 Codable타입으로 받을 수 있지 않을까 하고 시도해 봤는데 성공적이어서 글로 남긴다.ㅎㅎ

 

 users 컬랙션의 문서는 uid로 등록했다. 문서의 필드는 JSON으로 아래와 같은 형식으로 사용자의 정보를 담도록 했다. Codable의 개념을 모른다면 이전 블로그에서 설명했는데,, 자세한건 공식 문서가 최고다.

email : String,
fullname : String,
profileImageUrl : String,
uid : String,
username : String

 

1. Codable타입의 구조체를 만든다.

2. 파이어베이스를 통해 값을 받는다.

 

 이 두가지를 하면 된다.

 

 약간 주의할 점이라면 Firestore의 필드값으로 등록된 key(email, fullname, uid ...)들이 Codable타입의 구조체 멤버 변수명과 같게 네이밍 하거나 다르게 하고 싶은 경우 CodingKey타입의 CodingKeys enum을 정의해서 명확하게 해야 한다. (초기에 profilemageUrl을 profileImgaeUrl로 하는 CodingKey타입의 enum을 정의해서 디코딩이 되지 않았다.)

struct UserInfoModel: Codable {
    var email: String
    var fullname: String
    var profileURL: String
    var uid: String
    var username: String
    
    enum CodingKeys: String,CodingKey {
        case email
        case fullname
        case profileURL = "profileImageUrl"
        case uid
        case username
    }
}

 디코딩을 위한 Codable 구조체를 선언했다. 구조체의 변수명은  Firestore Database의 도큐먼트 필드 key 명과 대부분 같게 했다. profileImageUrl은 너무 길어서 profileURL로 쓰고 싶은 마음에 CodingKeys로 추가 정의를 했다. 근데 여기서 아직도 Encodable 타입과 Decodable타입이 정의 되지 않았다고 했다. 그래서 Encodable의 encode(to:), Decodable의 init(from:) 를 추가 구현했다.

//MARK: - Decodable
extension UserInfoModel {
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        email = try container.decode(String.self, forKey: .email)
        fullname = try container.decode(String.self, forKey: .fullname)
        profileURL = try container.decode(String.self, forKey: .profileURL)
        uid = try container.decode(String.self, forKey: .uid)
        username = try container.decode(String.self, forKey: .username)
    }

}

//MARK: - Encodable
extension UserInfoModel {
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(email, forKey: .email)
        try container.encode(uid, forKey: .uid)
        try container.encode(profileURL, forKey: .profileURL)
        try container.encode(fullname, forKey: .fullname)
        try container.encode(username, forKey: .username)
    }
}

여기까지 query로 값을 받아오거나 파이어스토어의 컬랙션의 문서에 값을 저장하기 위한 준비는 끝났다.

 

Firestore Database의 문서에 있는 필드 값을 요청해 Decodable타입의 구조체 인스턴스 얻기

사진1에서 내 파이어베이스 데이터베이스의 설명을 간략히 하자면 컬랙션(users)는 uid를 값으로 하는 여러 문서가 저장되고 특정 문서는 uid로 식별할 수 있다. 그 uid는 UserInfoModel타입의 5개 필드값을 가지고 있다. 그리고 내가 지금 할 것은 모든 문서 정보를 Decode하는게 아니라 특정한 uid만 꺼내오기 위한 과정을 설명할 것이다!!(특정 uid의 username이 얻고 싶었기 때문이다)

 

guard let userUID = Auth.auth().currentUser?.uid else {fatalError()}
let db = Firestore.firestore()
let usersCollection = db.collection("users")

1. 특정 user uid를 얻어온다.

2. 간단히 부를 수  있도록 db 프로퍼티 만든 후에 

3. 특정 컬랙션 인스턴스를 얻는다.

usersCollection.document(userUID).getDocument { docuemnt, error in
	guard error == nil else { return }
    guard let docuemnt = document else { return }
    do {
	let userInfo = try document.data(as: UserInfoModel.self)    	
    } catch let e {
    	print("Fail to get uid document data")
    }
}

4. uid의 특정 document를 얻어온 다음에 에러 처리 후 data(as:)를 사용하면 된다. 

끝!!

 

여기서 주의할 점이 data(as:) 를 사용하기 위해서는 pod 'FirebaseFirestoreSwift' 설치 해야한다. 그렇지 않으면 이 함수를 사용할 수 없다. 정말 간단하고 간결하다.

 

Firestore Database의 문서에 필드 값을 저장하기 with Encodable

이번엔 반대로 Codable타입의 인스턴스를 Firestore Database의 문서 값으로 저장해 볼 것이다. 아직 decode의 data(as:)와 달리 [String : Any] 타입으로 encode해주는 함수는 찾지 못했다. 그래도 최선을 다해 encode를 활용해 보려고 한다. (더 좋은 방법이 있을텐데..)  위에서 반환받은 Codable타입의 userInfo를 firestore db값으로 새로 저장한다고 가정할 것이다. 우선 내 생각으로는 encodable를 활용한다는 것은 encode()함수를 쓰는 것이라 생각했다. 

JSONEncoder().encode()

하지만 이 함수를 쓰면 data로 변환된다..

let data = try JSONEncoder().encode(userInfo)
guard let jsonData = String(data: data, encoding: .utf8) else {return}
print(jsonData)

예전에 배웠던 기억을 떠올리자면 data의 값을 확인하려면 String(data:encoding:)을 썼었다. 

그래서 String(data:encoding:)을 통해 변환된 문자열을 출력하니 JSON형식으로 잘 정리되어 있었다.

호호 역시 Codable 이 좋긴하다. 근데 Firestore에 값을 저장할 때는 [String : Any]타입이어야 한다. 방금 난 문자열로 바꿨고 JSONEncoder.encode()를 쓸 경우에 data 형식인 byte로 값이 반환된다. 어떻게 [String : Any]타입의 값으로 바꿀 수 있을까? 그때 떠올린 것이 JSONSerialization이다. 이것도 이전에 몇번 사용해 본적이 있어 블로그의 글(맨 마자막)로 남겼었는다. JSONSerialization은 json형식의 data를 Object로 변환하거나 그 반대를 jsonObject로 변환시켜 주는 함수들이 있다. 그래서 jsonObject(with:options:)를 사용해서 [String : Any]타입으로 바꿔주기로 했다. 근데 프로젝트에서 자주 사용할 것 같아서 Encodable로 extenstion했다.

extension Encodable {
    var encodeToDictionary: [String:Any]? {
        guard let data = try? JSONEncoder().encode(self) else { return nil }
        return try? JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String : Any]
    }
}

정말 다 왔다. 아까 위에서 decode로 만들어진 구조체 userInfo는 Codable타입이고 위에서 encode를 구현하는 Encodable타입도 포함하기에 userInfo.encodeToDictionary를 사용하면 [String : Any]타입의 json object가 반환된다. 옵셔널 타입을 반환하기에 아래의 함수를 추가로 만들어서 옵셔널 바인딩 해제를 한 json object를 반환하는 함수를 만들었다.

static func encodeToNSDictionary(codableType info: Codable) -> [String : Any] {
    guard let dataDictionary = info.encodeToDictionary else { fatalError() }
    return dataDictionary
}

let userJsonObj = encodeToNSDictionary(codableType: userInfo) 이 값을 보내면 된다.

Firestore.firestore().collection("users").docuemnt(특정uid).setData( userJsonObj, completion:completion)

 

Codable타입이 아니라면 해당 인스턴스를 [String : Any]타입의 형태의 값을 갖는 프로퍼티에 새로 키 벨류를 정해야 하겠지만?? Codable타입의 구조체를 사용했기에 간편하게 값을 보낼 수 있다!!! 


여담으로 처음에 codable타입을 구현하기 위해 pod 'FirebaseFirestoreSwift' 이걸 했었는데 뭔가 잘못 설치되서 갑자기 cocoapods에 문제가 생겨서 sudo gem arch -x86_64 등등 여러러러ㅓ 시행착오를 겪고 급기야 실수로 프로젝트의 파일들까지 전부 삭제 버튼을 꾹 눌러버렸다. 그 후로 아무리 pod 삭제 라이브러리의 xcode 특정 파일들 삭제 과정을 거쳤었는데 계속 Command CompileSwift failed with a nonzero exit code가 발생했다. 아무튼 결론은 원래 깃에 특정 기능을 구현했을 때 커밋을 하는 습관을 갖고 있었지만 좀 더 상세하게 커밋을 해야겠다..

728x90