일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |
- Firebase
- unittest
- flutter
- 2기화이팅
- CI
- SwiftUI VStack
- 액션과 계산 데이터
- firestore
- IOS
- 하드디스크 삭제 원리
- ChatGPT
- 쏙쏙 들어오는 함수형 코딩
- print 단점
- swift CI 적용
- 함수형 프로그래밍
- github
- MVVM
- auto_assign
- combine
- swift github action
- XCTest
- SwiftUI
- Swift thread
- LGTM
- 오픈소스
- 함수형 코딩
- os_log
- Apple Developer Academy @ POSTECH
- xcode
- Swift
- Today
- Total
개발공방
[애니또] ViewController와 View의 분리 - 리팩토링 (1) 본문
현재 애니또 iOS팀이 직면한 문제
현재까지 개발하면서 많은 문제와 버그들을 만났다. 첫 문제들을 해결하는데는 어려움이 없었지만, 앱의 규모가 조금씩 커지면서 문제들이 서로 엮이기 시작했다. 그래서 A라는 문제를 해결하면 B라는 문제가 다시 발생하는 상황이 생겼다. 이렇게 우당탕탕 앱을 만드는 것이 아니라 정형화된 틀이 필요했다. 그리고 각자가 만드는 기능들을 다른 팀원이 테스트하는데 굉장한 어려움이 있었다. 각자가 해당 브랜치로 가서 그 상황을 똑같이 만들어야 테스트가 가능했다. (그래서 PR에 테스트 방법을 상세히 적기도 했다)
애니또 팀에서 리팩토링을 진행한 이유
이젠 그 방식이 아니라 테스트 코드가 포함된 코드를 만들고 싶었고, 팀원끼리 상의한 결과 차근차근 리팩토링을 진행하자! 였다.
현재는 그 어떤 아키텍쳐 패턴이 적용되어 있지 않다. 많은 사람들이 사용하는 MVC조차 아닌 상태이다. 가장 큰 이유는 내가 당시 UIKit으로의 첫 개발이었고 시간이 많지 않은 상태라 어떠한 패턴을 생각하고 앱을 만들기엔 실력이 부족했다. 그리고 무턱대고 남들이 다 한다는 패턴을 적용해서 만드는 것 또한 납득할 수 없었다.
우선 MVC패턴에 대해 공부하고, View를 확실히 분리한뒤 우리 앱에 적합한 패턴을 찾아보기로 했다. 그게 MVP가 될지, MVVM이 될지 VIPER가 될지는 아직 정해진게 없다. 각 패턴을 공부하고 우리 앱에 적합한 패턴을 찾을 계획이다.
각자 맡은 뷰를 전체적으로 리팩토링을 진행했다. 최종적인 목표는 테스트가 가능한 코드를 만들자 였다.
리팩토링 할 대기방 화면
현재는 DetailWaitViewController라는 곳에서 모든 역할을 다 수행한다.
- UI Component를 만드는 코드
- Layout을 잡는 코드
- 각각 실행되어야 할 action 코드
- 네트워크 통신이 필요한 코드
그래서 총 길이가 500줄 가까이 된다. (예전엔 600줄이었다.)
길이도 길이지만 역할이 무분별하게 섞여있다보니 유지보수에 조금 애를 먹었다. 그래서 이 ViewController를 작게 쪼개보려 한다.
기존 폴더 구조 (리팩토링 전)
현재는 View가 분리되어 있지 않고, 내부의 컴포넌트만 따로 분리되어 있는 정도이다.
우선 가장 큰 분리는 View를 확실히 분리하는 것이다.
위 대기방 화면에서View는 어디 부분일까?
눈에 보이는 모든 부분이 View이다. 내부 Component 는 물론이고 NavigationBar 도 마찬가지다.
View 분리
DetailWaitViewController 내부에 View가 있고 View의 action들은 ViewController로 delegate해줄 것이고 View의 UI를 그리는데 필요한 것들은 configure 해줌으로서 해결할 예정이다.
UI 코드 분리
우선 View 폴더를 하나 만들고 내부에 들어갈 UIView 를 만든다.
그리고 해당 DetailWaitView 클래스에 모든 UI코드를 옮기면 된다.
내부 Component들의 레이아웃을 잡는 방법은 단 한가지만 수정하면 된다. 애니또 팀에서는 Layout을 스토리보드가 아닌 코드로 잡았는데, 기존 UIViewController에서는 self.view.addSubview 를 해야했지만, UIView 에선 self.addSubview 를 하면 된다.
변경 전
// DetailWaitViewController.Swift
self.view.addSubview(self.titleView)
self.titleView.snp.makeConstraints {
$0.leading.trailing.equalToSuperview().inset(Size.leadingTrailingPadding)
$0.top.equalToSuperview().offset(100)
$0.height.equalTo(86)
}
변경 후
// DetailWaitView.Swift
self.addSubview(self.titleView)
self.titleView.snp.makeConstraints {
$0.leading.trailing.equalToSuperview().inset(Size.leadingTrailingPadding)
$0.top.equalToSuperview().offset(100)
$0.height.equalTo(86)
}
Layout을 잡는 코드는 view 라는 코드를 지우는 것 말곤 수정할 부분이 없다. 현재까진 아주 간단하다.
NavigationBar 설정
UIViewController에서 네비게이션 바를 설정할 땐 크게 어렵지 않았다.
self.navigationItem를 통해 접근이 가능하기 때문이었다.
변경 전
// DetailWaitViewController.swift
private func setupSettingButton() {
let rightOffsetSettingButton = super.removeBarButtonItemOffset(with: moreButton,
offsetX: -10)
let settingButton = super.makeBarButtonItem(with: rightOffsetSettingButton)
self.navigationItem.rightBarButtonItem = settingButton
}
리팩토링 전에는 위 방식으로 NavigationBar 를 설정했다.
하지만 UIView 로 분리를 했기 때문에 NavigationItem 에 바로 접근이 불가능하다.
그렇다고 NavigationBar 로 설정될 Component를 DetailWaitViewController에서 갖고있는다?? 이러면 View의 분리가 의미가 없어질 것이다.
그래서 여기선 ViewController와 View간의 소통이 필요하다.
변경 후
// DetailWaitView.swift
func configureNavigationItem(_ navigationController: UINavigationController) {
let navigationItem = navigationController.topViewController?.navigationItem
let moreButton = UIBarButtonItem(customView: self.moreButton)
navigationItem?.rightBarButtonItem = moreButton
}
DetailWaitView 에 Internal 한 함수를 만들고 DetailWaitViewController에서 해당 함수를 실행하면서 매개변수로 UINavigationController를 넘겨준다.
// DetailWaitViewController.swift
private func configureNavigationController() {
guard let navigationController = self.navigationController else { return }
self.detailWaitView.configureNavigationItem(navigationController)
}
이렇게 넘겨주면 NavigationBar를 설정할 수 있다.
Action 설정
대기방엔 여러개의 Aciton기능이 있다.
방 삭제 버튼을 예시로 들어보겠다.
해당 버튼을 눌렀을 때의 Action을 설정한다면 크게 어렵지 않다. (조금 지저분하긴 하지만..)
변경 전
// DetailWaitViewController.swift
private func setExitButtonMenu() -> UIMenu {
let children: [UIAction] = memberType == .owner
? [UIAction(title: TextLiteral.modifiedRoomInfo, handler: { [weak self] _ in
self?.presentEditRoomView()
}),UIAction(title: TextLiteral.detailWaitViewControllerDeleteRoom, handler: { [weak self] _ in
self?.makeRequestAlert(title: UserStatus.owner.alertText.title,
message: UserStatus.owner.alertText.message,
okTitle: UserStatus.owner.alertText.okTitle,
okAction: { _ in
self?.requestDeleteRoom()
})
})
]
: [UIAction(title: TextLiteral.detailWaitViewControllerLeaveRoom, handler: { [weak self] _ in
self?.makeRequestAlert(title: UserStatus.member.alertText.title,
message: UserStatus.member.alertText.message,
okTitle: UserStatus.member.alertText.okTitle,
okAction: { _ in
self?.requestDeleteLeaveRoom()
})
})]
let menu = UIMenu(children: children)
return menu
}
NavigationBar의 UIMenu에 설정하는 코든데 조금 지저분하긴 하지만, 바로 접근해서 설정할 수 있다.
하지만 View로 분리한다면 View에선 어떤 액션이 일어나는지 알면 안되기 때문에, 해당 액션을 바로 적용할 수 없다. 한 액션을 예시로 들었지만 다른 액션들도 어떤 역할을 하는지 몰라야하기 때문에, 역할을 대신해서 해줄 친구가 필요하다.
변경 후
역할을 Delegate 해줄 protocol을하나 만들어서 View에선 함수만 실행하고, ViewController에서 어떤 역할을 할지 위임받아서 실행하는 방식으로 구현한다.
// DetailWaitView.swift
protocol DetailWaitViewDelegate: AnyObject {
func startManitto()
func presentRoomEditViewController(isOnlyDateEdit: Bool)
func deleteRoom(title: String, message: String, okTitle: String)
func leaveRoom(title: String, message: String, okTitle: String)
func presentEditViewControllerAfterShowAlert()
func showAlert(title: String, message: String)
}
위처럼 protocol 을 만들어서 각각의 action 들을 함수로 만들고 이 protocol을 ViewController에서 채택해서 사용하면 된다.
// DetailWaitView.swift
final class DetailWaitView: UIView {
private weak var delegate: DetailWaitViewDelegate?
}
// DetailWaitViewController.swift
extension DetailWaitViewController: DetailWaitViewDelegate {
func startManitto() {
self.requestStartManitto() { [weak self] result in
switch result {
case .success(let nickname):
self?.presentSelectManittoViewController(nickname: nickname)
case .failure:
self?.makeAlert(title: TextLiteral.detailWaitViewControllerStartErrorTitle,
message: TextLiteral.detailWaitViewControllerStartErrorMessage)
}
}
}
func presentRoomEditViewController(isOnlyDateEdit: Bool) {
self.presentDetailEditViewController(isOnlyDateEdit: isOnlyDateEdit)
}
func deleteRoom(title: String, message: String, okTitle: String) {
self.makeRequestAlert(title: title,
message: message,
okTitle: okTitle,
okAction: { [weak self] _ in
self?.requestDeleteRoom() { result in
switch result {
case .success:
self?.navigationController?.popViewController(animated: true)
case .failure:
self?.makeAlert(title: TextLiteral.detailWaitViewControllerDeleteErrorTitle,
message: TextLiteral.detailWaitViewControllerDeleteErrorMessage)
}
}
})
}
func leaveRoom(title: String, message: String, okTitle: String) {
self.makeRequestAlert(title: title,
message: message,
okAction: { [weak self] _ in
self?.requestDeleteLeaveRoom() { result in
switch result {
case .success:
self?.navigationController?.popViewController(animated: true)
case .failure:
self?.makeAlert(title: TextLiteral.detailWaitViewControllerLeaveErrorTitle,
message: TextLiteral.detailWaitViewControllerLeaveErrorMessage)
}
}
})
}
func presentEditViewControllerAfterShowAlert() {
self.makeAlert(title: TextLiteral.detailWaitViewControllerPastAlertTitle,
message: TextLiteral.detailWaitViewControllerPastOwnerAlertMessage,
okAction: { [weak self] _ in
self?.presentDetailEditViewController(isOnlyDateEdit: true) }
)
}
func showAlert(title: String, message: String) {
self.makeAlert(title: title, message: message)
}
}
ViewController에서 어떤 기능을 수행할지 구현해주면 된다. 위 코드의 내부 함수는 주의깊게 보지 않아도 된다. 실제 어떤 역할을 할 것인지를 상황에 맞게 구현해주면 된다.
Delegate로 만들어서 연결할 때 주의해야 하는게 채택해서 함수만 구현한다고 되는게 아니라, 위임을 해줘야한다.
// DetailWaitView.swift
func configureDelegation(_ delegate: DetailWaitViewDelegate) {
self.delegate = delegate
}
// DetailWaitViewController.swift
private func configureDelegation() {
self.detailWaitView.configureDelegation(self)
}
위 과정까지 해줘야 위임이 완료된다.
위 과정들을 거쳐서 1차적으로 View분리가 완료되었다.
// DetailWaitViewController.swift
final class DetailWaitViewController: BaseViewController {
private let detailWaitView = DetailWaitView()
override func loadView() {
self.view = self.detailWaitView
}
}
최종적으로 ViewController에서 loadView 단계에서 view를 적용시켜주면 된다.
https://github.com/DeveloperAcademy-POSTECH/MC2-Team5-Firefighter/pull/444
[REFACTOR] DetailWaitController의 View를 분리했습니다. by MMMIIIN · Pull Request #444 · DeveloperAcademy-POSTECH/MC2-T
🌁 Background 제 작고 귀여운 DetailWaitViewController의 View분리를 진행했습니다. 듀나와 호야가 미리 View 분리 PR을 올려주셔서 수월하게 작업했습니다. 👩💻 Contents DetailWaitView 분리 후 ViewController
github.com
'Swift > UIKit' 카테고리의 다른 글
[애니또] MVVM + Combine Input & Output 구조 - 리팩토링 (4) (0) | 2023.07.14 |
---|---|
[애니또] MVVM + Combine - 리팩토링 (3) (2) | 2023.06.15 |
[애니또] MVVM을 선택한 이유 - 리팩토링 (2) (0) | 2023.06.07 |
Swift UIBezierPath 크리스마스 트리 (4) | 2022.12.25 |
Swift DispatchQueue 기본 원리 (8) | 2022.07.14 |