Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

프로젝트 매니저 [STEP 2-1] BMO #311

Draft
wants to merge 14 commits into
base: ic_9_bmo94
Choose a base branch
from

Conversation

bubblecocoa
Copy link

@havilog
안녕하세요 하비!
연휴는 잘 보내셨나요? 😄
프로젝트 매니저 STEP 2-1 PR 전송드립니다.
이번 스텝에서는 뷰(완전하지 않은 상태) + MVVM 적용까지 진행했습니다.

고민했던 점

MVVM 구현

Closure를 이용하는 방법

final class WorkViewModel {
    var didChangeWorks: ((WorkViewModel) -> Void)?

    var works: [Work] {
        didSet {
            didChangeWorks?(self)
        }
    }

    init() {
        works = (1...10).map {
            Work(title: "\($0) 번째", description: "\($0) 번째 일 설명", deadline: Date())
        }
    }

    func updateWorks(_ works: [Work]) {
        self.works = works
    }
}

Observable을 이용하는 방법

final class Observable<T> {
    private var listener: ((T?) -> Void)?
    
    var value: T? {
        didSet {
            self.listener?(value)
        }
    }
    
    init(_ value: T?) {
        self.value = value
    }
    
    func bind(_ listener: @escaping (T?) -> Void) {
        listener(value)
        self.listener = listener
    }
}
final class WorkViewModel {    
    var works: Observable<[Work]> = Observable([])
    
    init() {
        works.value = (1...10).map {
            Work(title: "\($0) 번째", description: "\($0) 번째 일 설명", deadline: Date())
        }
    }
    
    func updateWorks(_ works: [Work]) {
        self.works.value = works
    }
}
  • 클로저를 이용하는 경우 didChangeWorks에, Observable을 이용하는 경우 listener에 바인드 할 작업을 지정합니다.
  • 클로저를 이용하는 경우 work가 변동될 때, Observable을 이용하는 경우 value가 변동될 때 didSet에서 바인딩 된 작업을 호출합니다.

저는 두 방식이 거의 유사하다고 생각하여 처음에는 코드 량이 좀 더 적은 클로저를 이용하는 방식으로 코드를 작성했습니다. 하지만 도중에 Observable을 사용하는 방법으로 코드를 변경하게 되었습니다.
사유는 초기화 시점에 있었습니다.
클로저 방식은 초기화시 works에 값이 대입된다 하더라도, 바인딩 되는 시점에 ViewModel에서 works 값을 직접 조회하지 않는 이상 해당 ViewModel을 가지고 있는 곳에서 works와 바인딩 되어있는 값에 영향을 줄 수 없는 상태입니다.
그래서 직접 ViewModel로부터 값을 조회하거나, ViewModel에게 초기화가 끝났다는 노티를 전송해야 합니다.
Observable을 사용하는 경우 모든 과정이 클로저의 경우와 같으나 bind에 있는 listener(value)로 인해 별도 조회나 노티 전송 없이 현재 값이 바로 바인딩 된 상태가 됩니다.
별도로 바인드 후 값을 대입하는 과정이나, 노티를 전송하는 작업 없이 해당 코드만으로 데이터 바인딩 되는 것이 깔끔하다고 생각해 Observable을 활용하는 방식을 택했습니다.

조언을 구하는 점

여러개의 ViewModel

처음에는 하나의 ViewModel이 여러개의 Model을 가진 상태로 해당 ViewModelView의 의존성으로 전달하는 방식을 생각했습니다. 하지만 이 경우 View가 추가되거나 제거될 때 해당 ViewModel 내의 코드를 변경시켜야 된다고 생각해 각각의 뷰가 각각의 ViewModel을 가지고 있는 형태로 변경했습니다.
처음에 시도한 방식과, 수정 한 방식 중 어떤 식으로 사용하는 것이 좀 더 보편적인(?) 방식인지 궁금합니다.

Copy link

@havilog havilog left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

뷰를 어떻게 그리느냐 보다는
뷰모델과 뷰를 어떻게 바인딩 하고
스트림이나, 이벤트, 상태 관리를 어떻게 할지를 조금 더 중점적으로
고민해보시면 좋을 듯 싶습니다


import Foundation

struct Work {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

모델들은 앵간하면 equatable, identifiable, sendable하면 좋아요
설계할 때 고려해주세요

import UIKit

final class WorkCell: UITableViewCell {
static let identifier: String = "WorkCell"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요런건 항상 프로토콜을 사용하는 습관이 좋을 듯 합니당


private let titleLabel: UILabel = {
let label = UILabel()
label.numberOfLines = 1
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1은 굳이 안해줘도 될 걸요?

super.init(style: style, reuseIdentifier: reuseIdentifier)
setUI()
}

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

prepare for reuse를 처리하는 부분이 없네요

Comment on lines 10 to 14
enum WorkType: String {
case todo = "TODO"
case doing = "DOING"
case done = "DONE"
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

work에 nested로 있어도 괜찮을거 같다는 생각이 드네요


import Foundation

final class WorkViewModel {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

보통 뷰 모델을 정의할 때에는
뷰에서 받을 입력값
뷰의 상태를 업데이트 해줄 수 있는 방법(함수를 실행, observable을 구독, 상태 값을 구독) 등이 필요할거 같고,
해당 방법 들에 대해서는 조금 더 고민해보시면 좋을 것 같아요

Copy link

@havilog havilog left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여러개의 ViewModel
일반적으로는 뷰 하나에 뷰모델 하나를 매핑해서 사용하는게 좋을 것 같아요
다만 뷰컨 즉 화면 하나랑은 다른 얘기인데
뷰를 쪼개서 해당 뷰의 기능만 하는 뷰모델을 두고
여러개의 뷰로 화면을 구현해야해요

또한 @published를 쓸 경우 observable은 의미가 없어질거고,
ios17부터는 @observable이라는 매크로가 나와서
요걸 활용하면 좋을 것 같아요

@bubblecocoa bubblecocoa marked this pull request as draft October 3, 2023 06:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants