[Swift] 메모 앱 만들기 심화 (7) : UserDefaults 로 데이터 저장하기

Oni·2023년 9월 4일
0

TIL

목록 보기
37/47
post-thumbnail

원문 포스팅 🔗

스토리보드 없이 메모앱 만들기를 진행하고 있는데, Todo에 해당하는 프로퍼티를 최대한 간단하게 정의하였다.

struct Todo {
    var todo: String
    var isCompleted: Bool
}

기초부터 차근차근 해보려고 todo 내용과 완료여부만 정의했고, Todo를 관리할 Manager를 구조체로 정의하였다.

struct TodoManager {
    static let userDefaults = UserDefaults.standard
    
    // MARK: - Variables
    static var todoList: [Todo] = [
        Todo(todo: "킬링보이스 악뮤 보기", isCompleted: true),
        Todo(todo: "개인 과제 코드로만 해보기", isCompleted: false)
    ]
    
    // MARK: - Load
    static func loadTodo() {
        
    }
    
    // MARK: - Add
    static func addTodo(_ newTodo: Todo) {
        var updatedTodoList = todoList
        updatedTodoList.append(newTodo)
        todoList = updatedTodoList
        print(todoList)
    }
    
    // MARK: - Save
    static func saveTodo(_ saveTodo: Todo) {
        
    }
    
    // MARK: - Delete
    
}

UserDefaults를 이용해서 간단하게 저장과 읽어오기를 구현하기 위해 addTodo 함수에 아래와 같은 코드를 추가했는데 오류가 발생했다.

userDefaults.set(todoList, forKey: "todoList")

ERROR: Attempt to insert non-property list object

아니 이게 뭔데...
너무나 당연하게 입력받는 Todo를 append 하듯이 set에 value로 넣었는데 UserDefaults는 기본 데이터 형식(property list)만 저장할 수 있다고 안된단다...

그렇다 UserDefaults는 Array나 Dictionary와 같은 복잡한 데이터 구조를 직접 저장하지 못하는 문제가 있는 것이다...
만약에 내가 의도한 것과 같이 배열의 형태로 저장하고 싶으면 Codable 프로토콜을 활용하여 데이터를 인코딩 및 디코딩해야 한다는 점이다.

그치만, Codable 프로토콜을 채택하지 않고 프로퍼티를 개별로 저장하면 될 것 같아서 아래와 같이 수정했다.

userDefaults.set(newTodo.todo, forKey: "todoTitle")
userDefaults.set(newTodo.isCompleted, forKey: "isCompleted")

새로 입력받는 Todo를 newTodo라고 정의하고 각 프로퍼티들을 별도로 저장해준다.

근데 여기서 문제가 있다.
프로퍼티들을 개별로 저장할 수는 있으나, 나중에 todoList를 load 할 때 이 데이터들을 다시 불러와서 Todo 객체로 조합해야 하는 것이다. 쉽게 말해서 또 작업이 추가된다는 뜻이다.

이렇게 분리된 저장 방식은 데이터를 관리하거나 활용하기에 복잡해져서 객체를 한번에 저장하거나 로드하기 위해서는 Codable 프로토콜을 사용하는 것이 더 효율적이다.

하지만 무시하고 loadTodo를 짰다.

static func loadTodo() {
    if let todo = userDefaults.string(forKey: "todoTitle"),
       let isCompleted = userDefaults.bool(forKey: "isCompleted") {
       	// 로드될 todoList를 정의하려고 했으나...
        let loadTodoList = Todo(todo: todo, isCompleted: isCompleted)
    }
}

바로 에러 발생
ERROR: Initializer for conditional binding must have Optional type, not 'Bool'

이 에러가 무엇인고 하니,
userDefaults.bool(forKey:) 메서드는 Bool 값을 반환하며, 이 값은 옵셔널이 아니기때문에 조건부 바인딩으로 추출할 수 없다는 말이다...

이 문제를 해결하기 위해서는 아래와 같이 코드를 수정할 수 있다.

저장

// 여러 개의 개별 값들을 저장(순서를 기억해야 함)
userDefaults.set(newTodo.todo, forKey: "todoTitle\(todoList.count)")
userDefaults.set(newTodo.isCompleted, forKey: "isCompleted\(todoList.count)")

// todoList 배열의 길이 갱신
let newCount = todoList.count + 1
userDefaults.set(newCount, forKey: "todoListCount")

// 로컬 배열 갱신
todoList.append(newTodo)

불러오기

// todoList 배열 불러오기
if let count = userDefaults.value(forKey: "todoListCount") as? Int {
    for index in 0..<count {
        if let todo = userDefaults.string(forKey: "todoTitle\(index)"),
           let isCompleted = userDefaults.bool(forKey: "isCompleted\(index)") {
            let loadedTodo = Todo(todo: todo, isCompleted: isCompleted)
            todoList.append(loadedTodo)
        }
    }
}

그렇다.. 다시 수정해봤는데 겁나 복잡하다.....

..... Codable 프로토콜을 채택했다. 진작에 할껄.. 너무 쉽게 저장되네

// MARK: - Load
static func loadTodo() {
    if let data = userDefaults.data(forKey: "todoList"),
       let loadTodoList = try? JSONDecoder().decode([Todo].self, from: data) {
        todoList = loadTodoList
    }
}

// MARK: - Add
static func addTodo(_ newTodo: Todo) {
    var updatedTodoList = todoList
    updatedTodoList.append(newTodo)
    todoList = updatedTodoList
    saveTodo(newTodo)
    loadTodo()
}

// MARK: - Save
static func saveTodo(_ saveTodo: Todo) {
    if let saveData = try? JSONEncoder().encode(todoList) {
        userDefaults.set(saveData, forKey: "todoList")
    }
}

데이터를 저장하는 부분은 saveTodo 함수안에 구현하여 저장하고, loadTodo 함수에 저장된 데이터를 디코딩해서 불러온다.
이렇게 되면 todo를 추가할 때나 삭제할 때 해당 로직을 작성하고 saveTodo()만 호출해주면 너무나 쉽게 todo를 저장할 수 있다.

profile
하지만 나는 끝까지 살아남을 거야!

0개의 댓글