일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- auto_assign
- 액션과 계산 데이터
- MVVM
- 하드디스크 삭제 원리
- os_log
- ChatGPT
- print 단점
- flutter
- combine
- swift github action
- LGTM
- Apple Developer Academy @ POSTECH
- 쏙쏙 들어오는 함수형 코딩
- Swift
- SwiftUI
- 함수형 프로그래밍
- 함수형 코딩
- firestore
- Firebase
- SwiftUI VStack
- github
- Swift thread
- XCTest
- xcode
- CI
- IOS
- unittest
- swift CI 적용
- 오픈소스
- 2기화이팅
- Today
- Total
개발공방
[애니또] 상속 → 프로토콜 통한 낭비되는 코드 줄이기 - 리팩토링 (5) 본문
BaseViewController를 만든 이유
BaseViewController는 프로젝트가 시작될 때부터 만들어 사용하던 클래스였다. 팀원 중 한 분께서 공통으로 사용하는 BaseViewController를 만들고, 각 ViewController에서 상속받아 사용했을 때의 장점을 말씀해 주셨다. 그에 팀원 모두 납득하고 장점이 확실해서 따르기로 했다.
우리 프로젝트에서 공통으로 적용되는 부분은 세 가지가 있었다.
- View의 배경색 통일
- 커스텀 네비게이션
- 터치해서 키보드 내리기
위 기능들을 BaseViewController에 구현해두고 각 화면별 ViewController마다 상속받아 사용해서 불필요한 중복 코드를 줄일 수 있었다.
장점은 확실했다. 개발 시간도 줄게 되고, 내가 구현해야 하는 부분에만 집중할 수 있었다.
하지만 단점도 분명 있었다. 특정 화면에서만 상속받아 사용하는 부분 중 일부를 수정해서 사용해야 하는 경우가 있었다. 그 화면만 BaseViewController를 상속받아 사용하지 않는 것도 컨벤션에 안 맞는 것 같고, Base 코드를 수정하기도 불가능했다. 여차저차 문제를 다른 방식으로 해결하긴 했지만, 좋은 방법이라 생각했던 상속에 대해 다시 한번 생각해 보게 되는 계기가 되었다.
BaseViewController를 제거하게 된 계기
위의 3가지 이유로 상속을 사용하게 되었다. 하지만 코드를 리팩토링하는 과정에서 기존 BaseViewController의 아쉬움이 생겨났다.
1. View의 배경색 통일
class BaseViewController: UIViewController {
// 생략
override func viewDidLoad() {
super.viewDidLoad()
self.configure()
}
func configureUI() {
self.view.backgroundColor = .backgroundGrey
}
}
원래는 위 코드에서 볼 수 있듯이 배경색을 ViewController에서 설정하는 방식이었다. 하지만 리팩토링을 진행하면서 ViewController와 View의 역할을 나누기로 했고, View 내부에서 배경색을 지정하게 되었다. 그래서 배경색을 공통으로 지정하는 코드는 더 이상 필요가 없어졌다.
2. 커스텀 네비게이션
사진과 같은 네비게이션을 사용하고 있었다. 네비게이션 설정 코드가 항상 BaseViewController에 있다 보니까 네비게이션을 사용하지 않는 ViewController에서도 해당 코드를 가지게 되는 상황이 있었다. 맨 처음 로그인 화면이나, 메인화면, 모달로 올라오는 화면 등 여러 곳에서 불필요하게 코드를 사용하게 되는 상황이 있었다.
3. 터치해서 키보드 내리기
func hidekeyboardWhenTappedAround() {
let tap = UITapGestureRecognizer(target: self, action: #selector(endEditingView))
tap.cancelsTouchesInView = false
view.addGestureRecognizer(tap)
}
@objc func endEditingView() {
view.endEditing(true)
}
이 상황도 2번의 상황과 같다. 키보드를 사용하지 않는 곳에서도 해당 코드를 들고 있어야 했고, 해당 view에 사용하지도 않는 Gesture를 추가하고 있으니 참 낭비인 상황이었다.
이와 같은 이유로 BaseViewController를 대체할 필요가 생겼다.
상속 → 프로토콜
모두가 공통으로 사용하진 않지만 특정 ViewController에서 사용하는 기능들을 미리 정의해두고 간편하게 사용하는 방법은 어떤 방식이 있을까??
내가 생각한 방법은 각각을 프로토콜로 만들어서 필요한 것을 사용하는 쪽에서 채택해 사용하는 방법을 생각했다.
네비게이션 관련 프로토콜, 키보드 관련 프로토콜을 만들고 필요하다면 채택해서 사용하는 방식으로 만들면 편하겠다고 생각했다.
protocol 만들기
protocol Navigationable {
func setupNavigation()
}
protocol Keyboardable {
func setupKeyboardGesture()
}
네비게이션이 필요한 ViewController에선 해당 프로토콜을 채택해 사용할 것이다. 하지만 위와 같은 코드로만 만들면 사용하는 부분에서 필요한 코드를 직접 다 추가해야 할 것이다.
Navigationable을 채택한 ViewController가 10개라면 10군데 모두 backButton을 추가하고, action을 추가하고, popGesture를 추가해야 할 것이다.
말도 안 되게 비효율적이다.
이 문제를 해결할 방법은 물론 존재한다.
protocol + Extension
protocol Extension이 뭔지 간단히 설명하자면
프로토콜을 채택했을 때 정의해야 하는 함수의 기본 구현을 정의할 수 있다. 쉽게 말해 디폴트 구현을 추가하는 것이다.
예를들어
extension Navigationable {
func setupNavigation() {
print("짜잔")
}
}
Navigationable이란 프로토콜에 extension 이용해 기본 구현을 정의한다면
final class ExampleViewController: UIViewController, Navigationable {
override func viewDidLoad() {
super.viewDidLoad()
setupNavigation() // 짜잔
}
}
Navigationable 을 채택하는 곳에서 구현부를 정의하지 않고도 바로 사용할 수 있다.
Navigationable
protocol + extension을 활용해 Navigationable 을 구현해보겠다.
protocol Navigationable: UIGestureRecognizerDelegate {
func setupNavigation()
}
extension Navigationable where Self: UIViewController {
func setupNavigation() {
self.setupNavigationBar()
self.setupBackButton()
self.setDragPopGesture(self)
}
private func backButtonItem() -> UIBarButtonItem {
let button = BackButton()
let buttonAction = UIAction { [weak self] _ in
self?.back()
}
button.addAction(buttonAction, for: .touchUpInside)
let leftOffsetBackButton = self.removeBarButtonItemOffset(with: button, offsetX: 10)
let backButton = self.makeBarButtonItem(with: leftOffsetBackButton)
return backButton
}
private func setupBackButton() {
let backButton = self.backButtonItem()
self.navigationItem.leftBarButtonItem = backButton
}
private func back() {
if let navigation = self.navigationController {
navigation.popViewController(animated: true)
}
}
private func setupNavigationBar() {
guard let navigationBar = navigationController?.navigationBar else { return }
let appearance = UINavigationBarAppearance()
let font = UIFont.font(.regular, ofSize: 14)
let largeFont = UIFont.font(.regular, ofSize: 34)
appearance.titleTextAttributes = [.font: font]
appearance.largeTitleTextAttributes = [.font: largeFont]
appearance.shadowColor = .clear
appearance.backgroundColor = .backgroundGrey
navigationBar.standardAppearance = appearance
navigationBar.compactAppearance = appearance
navigationBar.scrollEdgeAppearance = appearance
}
func setDragPopGesture(_ viewController: Navigationable) {
self.navigationController?.interactivePopGestureRecognizer?.delegate = viewController
}
}
코드를 살펴보자
extension의 where을 활용해 UIViewController에만 사용하게끔 제한을 뒀다. 또, 쓸어서 뒤로가는 기능을 구현하기 위해 UIGestureRecognizerDelegate 프로토콜을 채택했다.
setupNavigation() 함수에 기본 구현을 정의해서 다른 함수들을 호출하게 만들었다.
실제 사용하는 ViewController에서는 어떻게 사용할까??
final class DetailWaitViewController: UIViewController, Navigationable {
// 생략
override func viewDidLoad() {
// 생략
setupNavigation()
}
}
Navigationable 프로토콜을 채택한 뒤 setupNavigation() 함수를 호출해주면 끝이다.
위 작업을 통해 상속으로 사용하던 네비게이션을 똑같이 사용할 수 있다.
Keyboardable
protocol Keyboardable {
func setupKeyboardGesture()
}
extension Keyboardable where Self: UIViewController {
func setupKeyboardGesture() {
self.hidekeyboardWhenTappedAround()
}
}
키보드도 Navigationable 과 똑같다.
where UIViewController 덕분에 프로토콜 사용 범위를 제한하고, UIViewController의 extension에도 접근할 수 있다.
사용하는 ViewController에서도 같은 방식이다.
final class ParticipateRoomViewController: UIViewController, Keyboardable {
// 생략
override func viewDidLoad() {
// 생략
setupKeyboardGesture()
}
}
마무리
상속보다 프로토콜이 무조건 좋은 것은 아니다. 어떤 상황에선 상속이 더 유리하고, 어떤 상황에선 프로토콜이 더 유리할 수 있다. 상속과 프로토콜의 장단점을 알고 상황에 맞게 적절히 사용한다면 낭비 없고, 재사용성이 뛰어난 코드가 될 것이다.
느낀점
상속으로 구현되었던 BaseViewController를 프로토콜로 변경해 보았다. 상속은 통째로 만들어서 너도나도 사용하면 된다는 장점이 있었지만, 사용하지 않는 코드에 대한 낭비가 발생했다. 프로토콜은 필요한 기능만 채택해서 사용하면 된다는 장점이 있었지만, 코드를 작성하는 단계에서 세부적인 기능을 미리 알고 있어야 하는 문제가 있었다.
실제 서비스에는 어떤 방식으로 사용하는지는 잘 모르겠지만, 규모가 커질수록 프로토콜을 사용해 낭비를 줄이는 게 좋은 방법인 것 같다. 일일이 채택해야 하는 귀찮음이 있지 않냐? 라고 질문할 수 있을 것 같다. 하지만 낭비를 줄이고 조금이나마 메모리를 아낄 수 있다면 귀찮음을 감수할만하다고 생각한다.
해당 PR
https://github.com/DeveloperAcademy-POSTECH/MC2-Team5-Firefighter/pull/535
참고 자료
'Swift' 카테고리의 다른 글
가볍게 시작하는 Unit Test (with. XCTest) (0) | 2023.09.19 |
---|---|
[iOS] Moya가 테스트에 용이한 이유 (with. Stub) (0) | 2023.08.22 |
[Swift] print()와 같은 디버깅 코드들이 앱에 어떤 영향을 미칠까? (0) | 2023.04.27 |
Xcode library import 하는 법 (2) | 2022.05.07 |