KPT 회고에 키오스크 프로젝트에 대한 많은 아쉬움과 미련을 남겼었다. 팀원들과의 소통도 좋았고 눈에 보이는 결과물도 귀엽고 마음에 들었지만 한편으로 들었던 마음을 회상해보면 제대로 공부하고 고민하는 코딩을 못한 거에 대한 좌절이 있었던 것 같다. 그걸 해소하기 위해서는 못했던 공부를 하면 되는 건데, 새로운 주차가 시작되면서 새로운 커리큘럼에 집중해야 하다보니 또 못하고 넘어간다는 생각에 무력감을 많이 느꼈다. 그런데 의외로 빠르게 영상 강의를 완강하게 되어 과제가 주어지기 전, 이 시간을 활용해 프로젝트를 되돌아보며 리팩토링 해보려 한다. 마침 어제 저녁에 튜터님과의 면담을 통해 공부 방향성과 리팩토링 가이드라인을 받게 되어 이 좋은 기회를 꼭 활용하고 싶어졌다. 여러가지 리팩토링 목표가 있지만 MVC
패턴을 제대로 이해하는 것과 delegation
을 우선 순위로 삼았다.
튜터님이 제공해주신 다이어그램을 따라 그린 것이다
패턴 이름은 Model
-View
-Controller
지만 관계도를 보면 Model
과 View
는 서로 연결되어 있지 않으며, Controller
를 통해 소통하고 있다. 흐름을 따라가 보면, View
가 사용자의 상호 작용을 인식하여 Controller
에 알리면 Controller
는 Model
에게 데이터의 업데이트를 요구한다. Model
이 데이터 변경 로직을 수행한 뒤 Controller
에게 알리면 Controller
는 View
가 변경된 데이터를 화면에 띄우도록 처리한다.
View → Controller → Model → Controller → View
치킨 주문 키오스크 앱인 우리 팀의 꼬끼오스크의 경우로 풀어보겠다.
허니시리즈가 보이는 상태에서 레드시리즈 버튼을 누르자 레드시리즈 메뉴가 보이는 모습
- 레드시리즈
버튼 탭
발생 (User action
:View
인UIButton
의addTarget
을 통해Controller
에게 알림)- 메뉴 뷰가 어떤 치킨들을 보여줄 지 정보(
Model
)를 갖고 있는 건Controller
다. (Controller
→Model
Update
)- 보여줘야 하는 치킨 시리즈 데이터가 변경되면 (
Model
→Controller
Notify
)Contoller
는 메뉴 뷰인UICollectionView
에게 새로운 데이터를 표시하도록 알려준다. (Controller
→View
Update
:UICollectionViewDataSource
)- 화면에 레드시리즈 치킨들이 보인다.
이 흐름을 생각하며, CategoryView
에 대한 상호작용 인식을 시작으로 MenuView
업데이트로 끝나는 MVC
패턴 적용을 해보려고 한다. 이 때, delegate
를 통해 객체 간 소통이 이루어지도록 구현할 것이다.
본격적으로 리팩토링을 시작하기 전에, 이미 한번 진행됐었던 나의 CollectionView
리팩토링에 대해서도 회상하고 넘어가려 한다. MVC를 아예 몰랐던 상태
→ 처음으로 MVC를 알고 적용
한 내용이다.
UICollectionView
를 하위 뷰로 정의하면서컬렉션 뷰 클래스니까 컬렉션 뷰 관련 코드를 다 넣어야지
라는 사고방식으로 UICollectionViewDataSource
와 UICollectionViewDelegate
를 함께 정의하였다.class ChickenCollectionView: UICollectionView {
// ... //
// init 호출 함수
private func setup() {
delegate = self
dataSource = self
register(ChickenCell.self, forCellWithReuseIdentifier: ChickenCell.identifier)
}
}
extension ChickenCollectionView: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
// preview를 띄우기 위해 임시로 연동한 mock data
MockChicken.honeySeries.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = dequeueReusableCell(withReuseIdentifier: ChickenCell.identifier, for: indexPath) as? ChickenCell else { return UICollectionViewCell() }
// 여기서도 역시 임의의 데이터와 연동
cell.bind(MockChicken.honeySeries[indexPath.item])
return cell
}
}
extension ChickenCollectionView: UICollectionViewDelegate {
}
UI
를 그리는 게 목적이었던 처음 구현 시점에는 임의의 데이터를 바로 갖다 썼기 때문에 어떤 불편함과 문제를 느끼지 못했다. 프리뷰에 허니시리즈가 잘 뜨는 것에 만족한 채로 뷰 그리기는 마쳤다고 생각했던 기억이다. 하지만, Model
을 정의하여 데이터와 뷰를 연동하는 과정에서 View
객체와 Model
객체의 결합도가 높아지는 것을 체감했다.
class ChickenCollectionView: UICollectionView {
// data source에 어떤 치킨 시리즈를 전달해줄지 주입 받음
var series: ChickenSeries?
}
extension ChickenCollectionView: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
guard let series = series else { return 0 }
series.chickens.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard
let cell = dequeueReusableCell(withReuseIdentifier: ChickenCell.identifier, for: indexPath) as? ChickenCell,
let series = series
else { return UICollectionViewCell() }
cell.bind(series.chickens[indexPath.item])
return cell
}
}
이렇게 View
가 Model(series)
을 직접적으로 알아야만 화면을 그릴 수 있게 되는 것이다.
UICollectionView
의 DataSource
와 Delegate
코드를 Controller
로 옮기는 리팩토링을 진행하였다.이때, CollectionView와 PageControl을 하나의 컨테이너에 넣게 되면서
컬렉션 뷰를 따로 클래스로 분리했던 것(ChickenCollectionView)을 지우고, MenuView 안에 프로퍼티로 정의했다.
class MenuView: UIView {
// controller에서 접근할 수 있게 private 제한자 삭제
lazy var collectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView.register(ChickenCell.self, forCellWithReuseIdentifier: ChickenCell.identifier)
return collectionView
}()
private lazy var pageControl: UIPageControl = {
let pageControl = UIPageControl()
// ... //
return pageControl
}()
// ... //
// init 호출 함수 : ui 그리는 코드, auto layout 관련 코드만 갖고 있음
private func layout() {
// ... //
}
}
// controller
class KioskViewController: UIViewController {
// model (기본값 허니시리즈)
var series: ChickenSeries = .honey
// view
private lazy var menuView = MenuView()
override func viewDidLoad() {
super.viewDidLoad()
menuView.collectionView.dataSource = self
menuView.collectionView.delegate = self
}
}
extension ChickenCollectionView: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
series.chickens.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = dequeueReusableCell(withReuseIdentifier: ChickenCell.identifier, for: indexPath) as? ChickenCell else { return UICollectionViewCell() }
cell.bind(series.chickens[indexPath.item])
return cell
}
}
이렇게 적용하고 나니, View
클래스는 Model
과 직접 소통하지 않고 Controller
를 통해 데이터를 전달 받는 모습이다. 여기까지가 나의 첫 MVC
경험이다. 이 시점에서 형성된 나의 가치관과 MVC
에 대한 이해도는 어땠냐하면,
View 클래스가 Model을 모르게 철저하게 분리해야겠다.
View 클래스는 UI를 그리는 코드만 갖도록 해야되는구나
특히, 사용자의 탭 동작을 인식하는 UIButton
의 addTarget
이나 UICollectionViewDelegate
의 didSelectItemAt
같은 코드도 꼭 Controller가 가져가야 하는 코드구나
라고 생각했다. 왜냐하면 이 메소드들이 곧 데이터 변경 로직을 호출하는 코드를 포함하게 되기 때문이다. 이 메소드들이 뷰 클래스 안에 있게 되면 로직을 호출하기 위한 데이터 관리 모델을 주입 받아야 하기 때문에 바람직하지 않다. 위에 예시로 DataSource
만 다뤘지만 Delegate
의 경우 컬렉션 뷰 셀을 탭했을 때 치킨을 장바구니에 담아야 하기 때문에 MenuView
가 OrderManager
라는 데이터 관리 모델을 갖고 있어야 한다. 그래서
View가 Model을 주입 받을 필요가 없도록 User action을 인식하는 코드는 Controller에 보내야 된다.
라는 생각을 바탕으로 리팩토링을 하게되었고, 그 결과 Controller
에 코드가 정말 많아졌다. 이래서 MVC 패턴에서는 Controller가 무겁다고 하는 거구나
. 팀원들과도 이 생각을 바탕으로 대화를 나누었었다. 발표 때에도 프로젝트를 통해 MVC의 단점을 체감했다
는 소감을 말하기도 했다.
저렇게 MVC
를 충분히 겪었다고 생각하고 넘길 수도 있는 거였다. 특히 User action
인식 코드를 View
가 가져서는 안된다는 생각이 머릿속에 박혔던 참이었다. 그런 찰나에 그렇지 않은 다른 코드들을 보고 혼란에 빠지고, 제대로 된 코드는 뭘까 싶어서 너무 답답한 상황이었다. (정답인 코드는 없다지만 바람직한 코드의 방향성을 알고 싶은데 방향을 어디로 잡아야 할지 모르겠어서 고통스러웠다.) 그런데 무슨 행운이었을까, 내가 처한 상황을 어떻게 아시기라도 한 걸까? 튜터님과 면담을 통해 저 생각을 깰 수 있게 되었다. 공부할 주제로 1) MVC 패턴 다이어그램
과 2) UIView의 공식문서
를 주셨다.
먼저, 다이어그램
을 정신 차리고 다시 보면 View
가 User action
을 인식하고 Controller
에게 전달하는 모습이다. 그렇다면 버튼이 탭 동작을 인식하는 addTarget
과 같은 코드를 들고 있어도 된다는 의미인가? 근데 그러면서 어떻게 Model
과의 분리가 유지될 수 있을까? 그 방법에 대한 힌트로 delegation
을 보여주셨다.
callback 패턴이 아마 또다른 방법 중 하나인 것 같은데, 이것도 나중에 적용해보는 연습을 꼭 해보도록 할 것이다.
하지만 듣기로는 좀더 iOS적인 것은 delegate 패턴이라고 한다. (Swift에 protocol이 있기 때문인 것 같다.)
뭐가 맞고 틀리고는 아니다.
앞서 말했듯이 MVC
패턴을 접하고 난 뒤 생각이 액션 코드를 뷰가 가지면 안된다에 이르렀었는데, 튜터님께서 어떤 의견을 가질 때는 그 의견을 뒷받침하는 레퍼런스가 있으면 좋겠다고 하셨고 그 레퍼런스가 주로 공식 문서가 되도록 하라는 조언을 해주셨다. 그러면서 UIView
의 공식 문서를 읽어보라고 권하셨다.
https://developer.apple.com/documentation/uikit/uiview/
view
객체는 앱이 사용자와 상호작용하는 주요 수단이다.view
객체의 역할과 책임 몇 가지 :
UI 그리기
와 애니메이션 : 직사각형 영역에UIKit
과Core Graphics
를 사용하여 컨텐츠를 그린다. (Core Graphics
:CGFloat
,CGColor
할 때 그CG
) 뷰 프로퍼티에 대한 애니메이션을 적용할 수 있다.layout
적용과subview
관리 :view
는subview
를 갖고 있지 않을 수도 있고, 여러개 가질 수도 있으며subview
의 크기와 위치를 적용한다. 화면에 변화가 생길 때마다 컴포넌트들이 크기와 위치를 다시 잡을 수 있도록 지정해주는Auto Layout
을 활용한다.event
처리 :view
는UIResponder
의 하위 클래스로서,touch
를 포함한 다른 이벤트들에반응할 수 있다
. 여러가지 제스쳐를 처리할 수 있도록Gesture Recognizer
를 사용할 수 있다.
View
가 event 처리
역할을 갖고 있다. 그 이유는 UIResponder
, 이벤트에 반응하고 처리하는 클래스를 상속하고 있기 때문이다.
https://developer.apple.com/documentation/uikit/uiresponder
Responder
객체들은UIKit
으로 만들어진 앱에서event 핸들링
의 근간이 된다.UIApplication
,UIViewController
,UIView
까지 (which includesUIWindow
) 여기에 포함된다.UIKit
은 이벤트 발생 시Responder
객체들에게 이벤트를 전달한다(이벤트 처리를 위해서). 여기서이벤트
에는터치
,기기 움직임
,연결된 기기로부터 받은 입력(ex. 헤드셋 재생/일시정지 버튼, 키보드 입력, Apple TV 리모콘 버튼...)
등이 있다. 각 이벤트들을 다루는 메소드의 오버라이딩을 통해 처리한다.
아하. View
는 이벤트 처리
역할을 갖고 있다. 튜터님은 이걸 뷰가 원래 이벤트 처리 역할이 있어요 라고 그냥 말해주시는 게 아니라, 다이어그램과 공식 문서를 찾아보라고 말하셨다. 직접 보고, 알고 나니 인사이트를 수동으로 넓혀주는 게 아니라 넓히는 법을 알려주신 튜터님께 감사한 마음이 들었다.
제목이 리팩토링 챕터 원인데 Xcode
는 키지도 않았다🤭. 리팩토링 방향을 잡는 서사 정도 남긴 거 같다. 이제 깨달은 내용을 바탕으로 코딩을 한번 해볼까? 아맞다 델리게이트 공부