iOS) 영속적인 객체 저장 방식 : UserDefaults, Codable

2021. 8. 6. 02:57iOS

UserDefaults는 호락호락하지 않다

만약 TableView의 cell이 어떤 Custom Class의 객체라고 하자.

이 객체들을 영속적으로 저장하고 불러내고 싶을 때 UserDefaults에 순진하게? 객체배열을 set하면 "Attempt to insert non-property list object" 하면서 컴파일러가 울부짖는다. 🦖 크아앙

 

 

얘가 왜이러는지 알려면 UserDefaults의 성질부터 짚고 가는게 좋겠다.

 

UserDefaults에서 저장되는 값의 타입은 기본적으로 property list 여야 한다. NSDataNSStringNSNumberNSDateNSArray, or NSDictionary. 가 여기 속한다. 그렇다면 그 외의 타입들 - 예를 들면 클래스나 구조체와 같은 객체의 인스턴스를 담을 때는 어떻게 해야할까?

 

 

파일로 저장하면 영속성을 유지할 수 있다.

Swift에는 FileManager라는 클래스가 있다. 이름값을 톡톡히 하는 이 친구는 시스템 내에 파일을 만들고, 읽어오고, 이동시키는 등 접근한 경로의 파일에 대해서 많은 일들을 할 수 있다. 말그대로 파일 매니저다.

 

그럼 이제 코드내의 객체배열을 파일로 만들어서 시스템 저 어딘가에 위치시키고 (save) 다시 불러들이면 (load) 영속적인 데이터 사용이 가능하다는 뜻이렸다.

 

* 이때 중요한 점은 인코딩 혹은 디코딩 할 클래스(혹은 구조체)에 Encoder 혹은 Decoder 프로토콜을 채용해야한다. 인코딩과 디코딩 둘다 사용하고 싶다면 Codable 프로토콜을 채용한다.

 

 

말보다는 코드!

 

 

Encoding

먼저 객체배열을 파일로 저장시켜보겠다.

하지만 더 먼저 우리가 만들 파일을 어디 폴더 밑에다 넣을지 그것부터 정해보겠다.

 

FileManager에는 urls라는 메소드가 있다. 문서에 따르면 요청된 도메인에서 지정된 공통 디렉토리에 대한 URL를 리턴한다는데

이렇게 두개의 인자를 받고 있다. 첫번째 인자는 FileManager.SearchPathDirectory인데 enum을 직접 찾아들어가면 수많은 case를 맞이하게 되지만 사실 특정 디렉토리를 찾는 용도일 뿐이다. (Document 폴더를 찾아가고 싶다면 .documentDirectory 하는 식)

 

두번째 인자는 SearchPathDomain 타입을 받고있는데 어느 도메인에서 찾을 것인지 알려주면 된다.

 

print(FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!)

이렇게 해서 출력을 해주면 좀 긴 경로가 나온다.

 

 

이걸 Finder내에서 찾아가주면 앱의 번들을 볼 수 있다.

 

여기 디렉토리중 Documents 아래에 "Items.plist" 이름의 파일을 저장할거다. 파일을 추가할때는 URL 메소드 appendingPathComponent를 추가하여 자기자신의 URL을 append 하고 return한다.

let dataFilePath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first?
					.appendingPathComponent("Items.plist")

 

이제 본격적으로 객체배열을 인코딩 해보겠다.

테이블뷰에 아이템이 추가될 때마다 itemArray라는 객체배열에 객체를 추가한다. 그리고 아래의 메소드, saveItems()를 호출한다.

    func saveItems() {
        let encoder = PropertyListEncoder()
        
        do {
            let data = try encoder.encode(itemArray)
            try data.write(to: dataFilePath!)
        } catch {
            print("Error encoding item array \(error)")
        }
        
        tableView.reloadData()
    }

 

먼저 PropertyListEncoder 인스턴스를 불러낸다. 이 친구는 데이터 타입의 인스턴스를 property type으로 인코딩해준다.

항상 그렇듯 파일시스템을 건드리는 일은 에러를 동반할 가능성이 크다. do ~ catch 구문으로 에러 핸들링 하는것은 필수적이다.

encode 메소드를 이용해 itemArray를 인코딩 한 후, data라는 변수에 저장한다. 그리고 이 data를 아까 만들어두었던 지정경로 dataFilePath에 저장한다. 

 

이렇게 하면 아래처럼 Items.plist 의 파일이 해당 디렉토리에 생성된다!

 

 

Decoding

저장된 파일을 다시 객체배열로 디코딩 하는 방법은 다음과 같다.

  func loadItems() {
        if let data = try? Data(contentsOf: dataFilePath!) {
            let decoder = PropertyListDecoder()
            do {
                itemArray = try decoder.decode([Item].self, from: data)
            } catch {
                print("Error has occured")
            }
        }
    }

 

먼저 NSData의 이니셜라이저 중 Data(contentOf:) 의 인자로 우리가 아까 디렉토리에 저장할때 쓰였던 dataFilePath를 넣는다. URL이 존재하지 않을 가능성이 있어 옵셔널 타입이다. nil 핸들링을 적절하게 해주고 PropertyListDecoder()로 디코더 인스턴스를 생성한다. 

PropertyListDecoder은 decode  메소드를 갖는데 첫번재 인자에는 "변환할 타입 인스턴스", 그리고 두번째는 Data타입의 변수를 넣는다. 지금 코드에서 decoder.decode([Item].self, from: data) 의 모양으로, Item클래스의 객체배열로 디코딩을 진행하는데, 여기서 self는 인스턴스를 의미하므로 self를 빼면 컴파일에러가 발생한다. 

 

다시 정리하자면 dataFilePath는 디코딩할 파일이 있는 타겟 URL이고, 이것을 NSData로 변환한 다음, PropertyListDecoder 의 decode 메소드를 이용해 변환해준다. 

 

 

 

 

 

마치며

데이터를 영속적으로 저장할때 UserDefaults말고 Codable을 이용하는 방식도 꽤 유용하다. 이 포스팅은 step by step이 아니라실제 구현 코드는 거의 생략됐지만 이 글을 보는 사람들이 인코딩과 디코딩에 대해 더 명확한 지식을 갖길 희망한다.

 

실제 구현된 영상은 https://github.com/naldal/readmegifs/blob/master/ssc.gif 에서 볼수있다.

 

👋