본문 바로가기

개발일기

POP 프로젝트에 적용하기

OOP에서 POP를 적용하면서 느꼈던 새로운 관점을 나누고 싶어서 정리했습니다.

처음 작성해본 POP이기 때문에 "이렇게 작성해야 POP야"가 아닌

"이런 방식으로도 생각했구나"라고 봐주셨으면 좋겠습니다.

 

코드 스쿼드를 졸업하고, 완성하지 못한 부분이 자꾸 아쉬움이 남아서 IssueTracker를 구현하고 있었습니다.

콤바인을 원하는대로 구현할 수 있다는 자신감,

재사용성을 충분히 생각했다는 자만감에 빠져있어서 코드의 품질은 좋다고 생각했었습니다.

 

하지만 부엉이 말 한마디에 부족한 부분이 보였고,  Protocol에 대해서 공부하다 보니

새로운 접근을 할 수 있을 거 같아서 시작하게 되었습니다.

 


 

아래 그림을 보면 Label 화면과 MileStone 화면의 구성은 완벽하게 일치하고, UITableView Cell, 모델 부분에서 차이가 있습니다.

공통적인 기능을 하는 부모 클래스를 각각 화면에서 해당 클래스를 상속받고, 화면에서 필요한 작업을 처리했습니다.

 

 

 

이 과정에서 모델만 다르고 유사한 처리를 하는 코드를 생성했습니다.

같은 코드를 두 번 작성하면서 불편함에 대해서 모르고 있었습니다.(복붙만 하면 돼서 잘 구현한 코드로 착각했습니다.)

LabelFormViewController Code
MileStoneFormViewController Code

 

이렇게 중복되는 코드들이 '냄새'가 나는 코드의 유형이란 걸 알게 되었고

해결하기 위해서 제가 선택한 방법은 Protocol입니다.

 

 

처음으로 해결하고 싶은 문제는 "LabelTableViewController 과 MileStoneTableViewController 을 합칠 수 있냐" 였습니다.

공통으로 필요한 부분을 Protocol에 담기 시작했습니다. 

모델만 다른 viewModel, headerViewTitle, cancellables 마지막으로 fetchLabels()까지

 

 

protocol을 사용하면서 가장 중요한 부분은 "추상적이여야 한다" 입니다.

지금의 viewModel은 LabelViewModel이라는 구체 타입이어서 Milestone이 사용할 수 없게 생겼습니다.

 

 

우선 LabelTableViewController를 추상화 하기 전에

LabelViewModel 부터 추상화를 시키도록 하겠습니다.

 

 

공통으로 필요한 cancellable, applySnapshot(), 모델을 담을 labels을 protocol에 담을 겁니다.

override한 메소드들은 UITableViewDataSource의 메소드로

프로토콜 기본 구현을 사용하면 호출되지 않기 때문에 담지 않았습니다.

 

 

associatedtype을 사용해서 해당 프로토콜을 채택하고 있는 누구든 올 수 있게 만들었습니다.

(Label, MileStone이든 상관없도록)

applySnapshot 메소드도 동일합니다.

protocol Modelable {
    associatedtype Item: Hashable, Codable
    var cancellable: AnyCancellable? { get }
    var items: [Item] { get set }
}

extension Modelable {
    func applySnapshot(_ diffable: UITableViewDiffableDataSource<Section, Item>) {
        var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
		...
    }
}

 

 

LabelViewModel이 Modelable을 채택하면서 applySnapshot 메소드는 구현할 필요가 없어졌습니다.

Item의 구체타입을 지정해서 셀을 꾸며줄 수 있도록합니다.

 

 

막힌 viewModel 부분을 Modelable을 채택한 누구든지 올 수 있게 만들어서 처리해줬습니다.

그 과정에서 디코드하는 부분도 제네릭 타입을 사용해서 Label, Milestone 누가 오든 정상적으로 처리할 수 있도록 구현했습니다.

protocol Controllable: class {
    associatedtype ViewModel: Modelable
    var headerViewTitle: String { get }
    var viewModel: ViewModel { get set }
    var cancellables: Set<AnyCancellable> { get set }
}

extension Controllable where Self: UIViewController {
    func fetch(endpoint: RequestProviding) {
        UseCase.shared
            .fetch(type: [ViewModel.Item].self,
                    endpoint: endpoint,
                    method: .get)
            .receive(on: RunLoop.main)
            .sink(receiveCompletion: { [weak self] in
                guard case let .failure(error) = $0 else { return }
                let alertController = UIAlertController(message: error.message)
                self?.present(alertController,
                              animated: true)
            }) { [weak self] items in
            **모델을 사용하는 부분**
                self?.viewModel.items = items
                						  
        }
        .store(in: &cancellables)
    }
}

 

 

Controllabel을 채택한 LabelTableViewController는 아래와 같습니다.

bindViewModelToView 메소드의 프로토콜로 @Published를 처리할 수 없어서 각자 구현했습니다.

 

 

Protocol을 적용하기 전에는 LabelTableViewController가 fetch() 메소드를 갖고 있어서

파라미터를 구체 타입으로 받아서 해당 메소드를 사용했었습니다.

 

 

하지만 protocol을 적용함으로써 LabelTableViewController가 아닌

조건을 지킨 Controllable를 채택한 누구든지 상관없게 되었습니다.

    private func checkHTTPMethod<C>(controllable: C, method: HTTPMethod, updateItem: Item) where C: Controllable, C: UIViewController, Item == C.ViewModel.Item {
        switch method {
        case .post:
            guard selectItem is Label else {
                controllable
                    .fetch(endpoint: Endpoint(path: .mileStone()))
                
                return
            }
            
            controllable
                .fetch(endpoint: Endpoint(path: .labels()))
        default:
            for (index, item) in controllable.viewModel.items.enumerated() {
                guard updateItem.id == item.id else { return }
                controllable.viewModel.items
                    .remove(at: index)
                controllable.viewModel.items
                    .insert(updateItem, at: index)
            }
        }
    }

 

 

 

"박스안에 있는 사과 주세요가" 아닌

"그 노란색 형태 안에 있는 붉은 색 주세요"가 가능해진 겁니다.

이렇게 코딩함으로써 코드는 더 많은 상황을 대처할 수 있게 됩니다.

 

 


 

 

프로토콜을 사용하면서 제한된 부분도 많았고 

구현하면서 힘들었던 부분도 많았지만 POP를 사용하면서

왜 swfit가 POP 언어인지 알게되는 공부였습니다.

 

 

많이 미흡한 글이기 때문에

글을 읽으면서 이해가 안된 부분은 댓글로 남기시면 직접 설명해드릴려고 노력하겠습니다.

 

 

Github: github.com/codesquad-member-2020/issue-tracker-09

'개발일기' 카테고리의 다른 글

UIAlertController Layout issue  (0) 2021.07.26