일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 | 29 | 30 |
- 등굣길
- RaceCondition
- 코테
- SRP
- swift
- MainScheduler.asyncInstance
- baseviewcontroller
- Realm
- 프로그래머스
- 청년취업사관학교
- rxswift
- GCD
- 오픈채팅방
- data_structure
- gitflow
- SwiftUI
- SeSAC
- MainScheduler
- MainScheduler.Instance
- IOS
- combine
- DependencyInjection
- DiffableDataSource
- MethodSwilzzling
- 명품cppProgramming c++
- cleanarchitecture
- leetcode
- DynamicMemberLookup
- CoreBluetooth
- DispatchQueue
- Today
- Total
Do.
데코라이크(Decorator Like)로 더 유연한 ViewController 구조 만들기 본문
데코라이크(Decorator Like)로 더 유연한 ViewController 구조 만들기
Intro
iOS 앱을 개발하다 보면, 공통된 로직(예: 화면 전환 시 네비게이션 바/탭 바 보이기·숨기기, deinit 시점 로그 남기기 등)을 여러 개의 뷰컨트롤러에 반복해서 적용해야 하는 상황이 생깁니다. 이때 종종 가장 먼저 고려되는 방법이 바로 BaseViewController를 만드는 것이죠. BaseViewController에 공통 코드를 모아두고, 모든 뷰컨트롤러에서 이를 상속받으면 편리해 보이지만, 기능이 많아질수록 점점 덩치가 커지고 유지보수가 어려워집니다.
이 글에서는 그러한 문제를 해결하고자 “데코라이크”라는 구조를 소개하려고 합니다.
문제 상황
- BaseViewController의 덩치가 커진다
- 네비게이션 바, 탭 바 제어 등 라이프 사이클에 따라 다양한 기능이 추가되다 보면 BaseViewController는 점점 비대해집니다.
- 작은 수정이 발생해도 해당 클래스를 상속하는 모든 자식 뷰컨트롤러에 영향을 미칠 수 있어, 예기치 못한 사이드 이펙트가 발생합니다.
- 특정 기능만 필요할 때도 전체를 상속받아야 한다
- 예를 들어 “deinit 시점 로그 찍기” 기능만 필요한데도, BaseViewController에 들어있는 다른 로직 전부를 상속받게 됩니다.
- 상속 구조가 점점 복잡해지면서, BaseViewController의 실제 동작을 예측하기 어려워 집니다.
목표
- 기능을 필요에 따라 선택적으로 부여
- 하단 탭 바 숨기기, 네비게이션 바 숨기기, deinit 로그 처리 등 기능들을 개별적으로 적용할 수 있게 만든다.
- ViewController 라이프 사이클 기반의 확장성
- viewDidLoad, viewDidAppear, viewWillDisappear 등 각 라이프 사이클 시점에 맞춰서 동작하는 로직을 쉽게 추가/관리한다.
- 충분히 가볍고 간단한 구조
- 대규모 상속 트리 없이도 각 기능을 조합해서 자유롭게 사용할 수 있어야 한다.
DefaultViewController와 VCDecorator 등장
DefaultViewController
우선 DefaultViewController라는 기본 뷰컨트롤러를 한 단계 만들어둡니다.
이 컨트롤러에서는 라이프 사이클마다 호출될 콜백 형태의 핸들러를 미리 정의해둡니다. 예시로는 다음과 같습니다.
class DefaultViewController: UIViewController {
var _viewDidLoad: (() -> Void)?
var _viewDidAppear: (() -> Void)?
var _viewWillDisappear: (() -> Void)?
var _deinitBlock: () -> Void)?
// ... 등등 필요한 콜백
override func viewDidLoad() {
super.viewDidLoad()
_viewDidLoad?()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
_viewDidAppear?()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
_viewWillDisappear?()
}
deinit {
deinitBlock?()
}
}
이렇게 하면, DefaultViewController를 사용하는 쪽에서 각 라이프 사이클마다 실행할 로직을 옵셔널 클로저로 주입할 수 있게 됩니다.
하지만 아직까지는 이 뷰컨 하나만으로는 “옵션별 기능”을 관리하기가 조금 번거롭습니다.
VCDecorator
VCDecorator는 말 그대로 “디자인 패턴의 Decorator”처럼, 뷰컨트롤러에 필요한 기능을 데코레이션(얹어)하는 역할을 합니다. 사용 방법은 다음과 같습니다.
enum VCDecoratorOption {
case hideTabBar
case hideNavigationBar
case logDeinit
// 필요한 옵션을 자유롭게 추가
}
class VCDecorator {
static func build(
target: DefaultViewController,
options: VCDecoratorOption...
) -> DefaultViewController {
// 옵션별로 target에 필요한 콜백을 셋업
for option in options {
switch option {
case .hideTabBar:
// viewDidAppear 시점에 tabBar 숨기기
let originalHandler = target._viewDidAppear
target._viewDidAppear = {
originalHandler?()
target.tabBarController?.tabBar.isHidden = true
}
case .hideNavigationBar:
// viewWillAppear 또는 viewDidAppear 시점에 네비게이션 바 숨기기
// 혹은 viewWillDisappear 등에서 다시 표시
// 필요 로직을 여기에서 정의
// (데모라서 간단히 예시만 적음)
let originalHandler = target._viewDidAppear
target._viewDidAppear = {
originalHandler?()
target.navigationController?.setNavigationBarHidden(true, animated: false)
}
case .logDeinit:
// 뷰컨 deinit 시점 로그 남기기
// DefaultViewController deinit 안에서 처리할 수도 있지만,
// 만약 동적으로 켜고 끄고 싶다면 별도의 방법으로 처리
// 예: Weak 래퍼 + deinit 시점 클로저 등
// 여기선 간단히 예시만 보여줌
// target.deinitLogger = { print("ViewController deinit!") }
// 같은 방식으로 구현 가능
// 필요한 구현부를 만들어두고 연결해주는 식
break
}
}
return target
}
}
위의 예시는 단순화된 형태이며, 실제 구현 시에는 DefaultViewController 내 deinit에서 콜백을 호출하게 하거나, 약한 참조(weak)를 사용해 안전하게 로그를 찍도록 할 수 있습니다. 중요한 점은 어떤 옵션이든 손쉽게 추가할 수 있고, 옵션이 많아져도 각각의 로직이 분리된다는 것입니다.
또 위 방식에서는 간단하게 열거형을 사용했지만, 열거형 사용시 option을 주입할 때 코드가 길어지므로 데코레이터 가능한 프로토콜을 정의하고 별개의 Dacorator 구조체를 만드는 것도 방법입니다.
// 1. 일반적으로 DefaultViewController를 하나 생성한다.
let vc = SomeDefaultViewController()
// 2. 필요한 옵션들을 조합해서 VCDecorator.build(...) 호출
let decoratedVC = VCDecorator.build(
target: vc,
options: .hideTabBar, .hideNavigationBar
)
// 3. 이제 decoratedVC를 화면에 표시하거나 push, present 하면
// 탭바와 네비게이션 바가 숨겨진 상태로 표시된다.
navigationController?.pushViewController(decoratedVC, animated: true)
이렇게 하면 탭바 숨기기/네비바 숨기기 등의 기능을 BaseViewController 하나로 전부 해결하는 게 아니라, 옵션을 통한 선택적 부여로 정리할 수 있게 됩니다.
장단점
장점
- 선택적 기능 부여: 필요한 기능만 추가로 얹어갈 수 있으므로, BaseViewController가 비대해지지 않습니다.
- 확장성: 새로운 공통 기능이 필요할 때, VCDecoratorOption에 case를 추가하고, VCDecorator.build 내부에서 셋업 코드를 구현하기만 하면 됩니다.
- Side Effect 최소화: 전부 상속받는 구조가 아니므로, 특정 옵션을 수정해도 다른 ViewController가 영향을 받지 않습니다(해당 옵션을 사용 중이지 않은 이상).
단점
- 초기 세팅 복잡도: 직접 DefaultViewController를 만들고, 옵션들을 늘려가며 콜백을 주입하는 구조를 잡아야 합니다.
- 옵션들 간의 충돌 가능성: 두 옵션이 동일한 라이프 사이클 메서드에 서로 상반된 명령을 내릴 수 있다면, 이를 조정하는 로직이 필요해질 수 있습니다(예: .hideTabBar와 .showTabBar가 같은 시점에 동시 적용되는 경우 등).
- 런타임에 한 번 더 감싸는 비용: 데코라이크 구조를 쓰게 되면, 작은 성능 오버헤드가 있을 수 있으나 대개 무시해도 될 수준이긴 합니다.
마치며
BaseViewController 구조를 무작정 확장하기보다, “장식(Decorator)” 패턴을 적용해 선택적으로 필요한 기능들을 얹어갈 수 있게 만들면, 복잡도가 낮아지고 유지보수가 편리해집니다.
요약하자면, 데코라이크 구조는 ViewController에 필요한 기능을 옵션으로 선언하고, build 과정을 거치며 동적으로 주입함으로써 유연하면서도 읽기 쉬운 구조를 확보할 수 있는 좋은 방법 중 하나라고 생각합니다.
'iOS' 카테고리의 다른 글
WWDC23 - What's new in swift (0) | 2023.06.07 |
---|---|
RxSwift - MainScheduler.instance vs MainScheduler.asyncInstance (0) | 2023.06.02 |
Swift - RxSwift의 withUnretained를 Combine에서도 쓰기 (0) | 2023.05.02 |
iOS Framework - 네트워크 모듈 테스트 (URLSession, Unit Test) (2) | 2023.03.31 |
SwiftUI - SwiftUI 환경에서 ViewController Life Cycle 사용하기 (6) | 2022.10.26 |