코드로 UI를 구성하는 과정에서 반복된 코드를 줄이고, 보다 일관된 방법으로 ViewController을 구성하는 방법은 없을까에 대한 고민을 했다.
각 뷰에서 공통으로 사용하는 부분을 공통의 컨테이너로 만들어서 사용하고, 기본 UI 컨트롤러를 Sub-Classing 해서 가독성을 높일 수 있을 거라고 생각됐다.
이 생각을 하게 된 것은 ViewController가 많아지게 되면 많아진 View들을 어떻게 관리해야할까? 라는 것이 시작점이었다.
공통된 작업을 미리 구현할 수 있으면 좋겠다. 라는 생각이 들었고, 린생(갓생)👍으로부터 받은 피드백이 크게 작용했다.
분명 그 과정에선 내가 작성한 코드에 대해 의구심도 많이 들었다.
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
}
일반적으로 ViewController
파일을 만들면 다음과 같이 UIViewController
을 상속 받고 있는 것을 확인할 수 있다.
특정한 뷰를 사용하지 않고서는 보통 위와 같은 상속 구조를 가지게 되고, UIViewController
이 아닌 다른 뷰를 상속 받았다고 하더라도 실질적으로는 UIViewController
를 상속한 구조를 확인할 수 있었다.
import UIKit
class RootView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
setup()
setupUI()
}
required init?(coder: NSCoder) {
fatalError()
}
func setup() { }
func setupUI() {
backgroundColor = .clear
}
}
class RootViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
setup()
bind()
}
func setup() { }
func bind() { }
}
먼저 view를 viewController
를 따로 나눠 뷰를 구성할 수 있도록 UIView
를 상속한 RootView
와
UIViewController
을 상속한 RootViewController
을 선언했다.
내부에 선언된 메서드는 제 스타일의 코드이니 원하는 방향으로 코드를 작성하면 되겠다. 하지만, 위처럼 상속을 활용하게 된다면 RootView
, RootViewController
는 모든 뷰의 Root
같은 역할을 할 것 같다는 느낌을 줘야겠지만, 일뷰 뷰에서는 공통된 기능을 필수적으로 요구하지 않을 수도 있기 때문에 커스텀 클래스를 상속할 필요가 있을까? 하는 의심 또한 충분히 해볼 수 있다.
또한 코드를 override
하는 과정에서 추가로 생각해볼 수 있는 개념이라고 한다면, SOLID 원칙 중 OCP를 위반하고 있다고 말할 수 있다.
개방 폐쇄의 원칙(OCP)은 작성된 소스 코드가 확장에 대해 개방되어 있고, 수정에 대해 폐쇄되어 있는 원칙을 말한다.
OCP에 만족되는 설계를 하려면 변경점을 파악하고, 수정 시 영향 받지 않게 해야한다.
그렇다면 상속을 이용하지 않는 방법은 없을까?
프로토콜을 활용한 구현 방법에 대해서도 생각해보았다.
ViewConfiguration
이라는 프로토콜을 선언하고, protocol
을 따를 경우, 하단의 3개의 함수를 구현하도록 했다.
이중 configureViews()
의 경우엔 선택적인 구현이 가능하도록 extension
을 활용해 기본 구현에서 아무런 작업이 이뤄지지 않도록 했다.
import Foundation
protocol ViewConfiguration {
func buildHierarchy()
func setupConstraints()
func configureViews()
}
extension ViewConfiguration {
func configureViews() {}
func applyViewSettings() {
buildHierarchy()
setupConstraints()
configureViews()
}
}
결과적으로 봤을 때, 상단처럼 구현했을 때의 장점은 수평 구조로의 확장이 가능했다.
또한 뷰를 구현하는 코드 작성에 대해 선언한 프로토콜을 따르게 하여 일관된 코드 작성이 가능해졌다.
실제로 POP를 활용한 방법으로 ViewConfiguration
을 작성해서 구현했을 경우엔 하단과 같이 extension
으로 View
의 구성을 담당하는 메소드들을 일관된 형식으로 볼 수 있었다.
// MARK: ViewConfigutation
extension AdministerViewController: ViewConfiguration {
func buildHierarchy() {
view.addSubviews(userTableView)
}
func setupConstraints() {
NSLayoutConstraint.activate([
userTableView.topAnchor.constraint(equalTo: self.view.topAnchor),
userTableView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor),
userTableView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
userTableView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor)
])
}
func configureViews() {
userTableView.dataSource = self
userTableView.register(UserTableViewCell.self,
forCellReuseIdentifier: UserTableViewCell.reuseIdentifier)
}
}
프로토콜을 사용한 덕분에 뷰의 구성에 대해서 개발하는 나 또한 그 역할에 맞게 코드 위치를 결정하려고 노력했던 것 같다.
아직 정확히 POP에 대해 완벽히 이해하고 있다고 생각하지 않는다.
앞으로 프로토콜에 대해서 좀더 알아보도록 하자
(+ 이후 디자인 패턴을 공부하다가 알게 됐지만, 단순 코드의 형식이 일관되서 좋다. 라고 생각만 했었는데, 이 형식이 템플릿 메소드 패턴의 전형적인 형태임을 알 수 있었다.)
Reference