단어 랜덤으로 섞기 (by 하위 View에 바인딩으로 전달하기)

SteadySlower·2022년 8월 4일
0

구현하고자 하는 기능

같은 단어장으로 계속 공부하다 보면 위아래에 위치한 단어가 은근히 힌트의 역할을 할 때가 있습니다. 이럴 때는 가끔 단어의 위치를 랜덤으로 섞어주어야 단어 학습의 능률이 오르게 됩니다. 이 점이 노트에 단어장을 작성하는 것 보다 앱으로 단어를 외우는 것이 좀 더 효과적일 수 있는 부분입니다.

따라서 우리가 구현하고자 하는 기능은 주어진 단어를 랜덤으로 정렬하는 기능입니다.

구현

배열 shuffle 해주기

구현 자체는 아주 간단합니다. view model에 단어를 저장한 배열을 그냥 shuffle 해주면 됩니다.

// StudyView (상위View)의 ViewModel
@Published var words: [Word] = []

func shuffleWords() {
    words.shuffle()
    shufflePublisher.send()
}

버그: 단어의 색깔이 사라진다?

하지만 이렇게 간단하게만 구현한 경우 하나의 버그가 발생했습니다. 앱에서는 단어의 성공 / 실패를 초록색과 노란색으로 표시하고 있었는데 이런 상태에서 랜덤 정렬을 실행했을 때 지금까지 적용한 단어의 색의 변경 사항이 적용되지 않는 것이었습니다.

원인

성공 / 실패를 표시하는 함수는 하위 View(WordCell)에서 실행됩니다. 상위 View (StudyView)의 데이터에는 아무런 영향을 끼치지 못합니다. 하지만 shuffle은 상위 View(StudyView)에서 실행됩니다. 그리고 상위 View의 데이터는 변경된 word.studyState 값을 가지고 있지 않습니다. 따라서 적용한 성공 / 실패의 색깔이 사라지게 되는 것이죠.

// WordCell ViewModel
@Published var word: Word

func updateToSuccess() {
    guard let wordBookID = wordBook.id else { return }
    guard let wordID = word.id else { return }
    WordService.updateStudyState(wordBookID: wordBookID, wordID: wordID, newState: .success) { error in
        // FIXME: handle error
        if let error = error { return }
        self.word.studyState = .success
    }
}

func updateToFail() {
    guard let wordBookID = wordBook.id else { return }
    guard let wordID = word.id else { return }
    WordService.updateStudyState(wordBookID: wordBookID, wordID: wordID, newState: .fail) { error in
        // FIXME: handle error
        if let error = error { return }
        self.word.studyState = .fail
    }
}

첫번째 해결책: 랜덤할 때 데이터 새로 fetch 해오기

처음으로 떠올린 해결책은 랜덤을 할 때마다 서버에서 새로 데이터를 fetch해오는 것이었습니다. 단어의 성공 / 실패를 표시할 때마다 API를 호출해서 DB에 저장된 상태를 수정하기 때문에 DB에는 성공 / 실패가 실시간으로 반영이 되어 있습니다. 따라서 새로 fetch 해온 후에 shuffle을 하면 랜덤 정렬을 할 때 해당 버그가 발생하지 않습니다.

func shuffleWords() {
    WordService.getWords(wordBookID: wordBook.id!) { [weak self] words, error in
        if let error = error {
            print("디버그: \(error)")
        }
        guard let words = words else { return }
        self?.words = words.shuffled()
    }
}

두번째 해결책: 하위 View(WordCell)에 word를 binding으로 전달하기

하지만 첫번째 방법은 뭔가 찝찝합니다. DB에서 데이터를 fetch해오는 일은 shuffle에 비해 훨씬 오래 걸리는 일입니다. 단순한 작업인 shuffle 때문에 더 많은 시간과 네트워크 비용을 사용해야 하는 것은 배보다 배꼽이 더 큰 상황입니다.

따라서 두번째 방법은 WordCell에 데이터를 전달할 때 binding으로 전달하는 것입니다. 이렇게 하면 상위 View(StudyView)와 하위 View(WordCell)의 word가 동일한 객체를 공유합니다. 따라서 WordCell에서 word의 성공 / 실패를 변경할 경우에 자연스럽게 StudyView의 동일한 객체의 성공 / 실패도 동일하게 변경되게 됩니다.

결과적으로 shuffle을 하더라도 성공 / 실패가 상위 View의 데이터에 반영이 되어 있기 버그가 발생하지 않습니다!

하위 View에 데이터를 전달하는 법에 대한 더 자세한 내용은 이 포스팅을 참고해주세요!

// WordCell의 initializer
init(wordBook: WordBook, word: Binding<Word>) {
    self.viewModel = ViewModel(wordBook: wordBook, word: word)
}

// WordCell ViewModel
@Binding var word: Word

init(wordBook: WordBook, word: Binding<Word>) {
    self.wordBook = wordBook
    self._word = word
}

// WordCell 인스턴스 만들기
ForEach(0..<viewModel.words.count, id: \.self) { index in
	WordCell(wordBook: viewModel.wordBook, word: $viewModel.words[index])
}
profile
백과사전 보다 항해일지(혹은 표류일지)를 지향합니다.

0개의 댓글