https://github.com/SpartaCoding-iOS05-i/PokeContact/pull/8
큰 틀 구성 |
---|
PokeContact/
├── App/
│ ├── AppDelegate.swift
│ ├── SceneDelegate.swift
│ ...
├── Coordinators/
├── Models/
├── Views/
├── ViewModels/
...
Coordinator
프로토콜 정의protocol Coordinator {
var childCoordinators: [Coordinator] { get set }
func start()
}
childCoordinators
: 하위 Coordinator를 관리하여 메모리 누수를 방지start()
: 각 Coordinator가 수행해야 할 초기 동작앱의 진입점이 되는 AppCoordinator
를 생성한다:
import UIKit
class AppCoordinator: Coordinator {
var childCoordinators: [Coordinator] = []
private let window: UIWindow
init(window: UIWindow) {
self.window = window
}
func start() {
let navigationController = UINavigationController()
let mainCoordinator = MainCoordinator(navigationController: navigationController)
childCoordinators.append(mainCoordinator)
mainCoordinator.start()
window.rootViewController = navigationController
window.makeKeyAndVisible()
}
}
화면마다 별도의 Coordinator를 만든다: MainCoordinator
import UIKit
class MainCoordinator: Coordinator {
var childCoordinators: [Coordinator] = []
private let navigationController: UINavigationController
init(navigationController: UINavigationController) {
self.navigationController = navigationController
}
internal func start() {
let viewModel = MainViewModel()
let viewController = MainViewController(viewModel: viewModel)
viewModel.coordinator = self
navigationController.pushViewController(viewController, animated: true)
}
}
ViewModel은 View의 상태 및 로직을 관리한다:
class MainViewModel {
weak var coordinator: MainCoordinator?
func didTapNext() {
coordinator?.navigateToDetail()
}
}
import UIKit
import SnapKit
import Then
class MainViewController: UIViewController {
private let viewModel: MainViewModel
init(viewModel: MainViewModel) {
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
configureUI()
}
private func configureUI() {
view.backgroundColor = .systemPink
let button = UIButton(type: .system).then {
$0.setTitle("Button", for: .normal)
$0.addTarget(self, action: #selector(didTapButton), for: .touchUpInside)
}
[
button,
].forEach { view.addSubview($0) }
button.snp.makeConstraints {
$0.center.equalToSuperview()
$0.width.equalTo(120)
$0.height.equalTo(44)
}
}
@objc private func didTapButton() {
viewModel.didTapNext()
}
}
MainCoordinator
에 다음 화면으로 전환하는 로직을 추가함:
extension MainCoordinator {
func navigateToDetail() {
let detailViewModel = DetailViewModel()
let detailViewController = DetailViewController(viewModel: detailViewModel)
navigationController.pushViewController(detailViewController, animated: true)
}
}
앱의 진입점을 AppCoordinator
로 설정함:
import UIKit
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
var appCoordinator: AppCoordinator?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
let window = UIWindow(windowScene: windowScene)
self.window = window
let appCoordinator = AppCoordinator(window: window)
self.appCoordinator = appCoordinator
appCoordinator.start()
}
}
프로토콜 사용
MainCoordinator
의 역할을 나타내는 MainCoordinatorProtocol
프로토콜을 정의한다:
(MainViewModel
은 이 프로토콜에 의존하여 MainCoordinator
와 통신함)
protocol MainCoordinatorProtocol: AnyObject {
func navigateToDetail()
}
MainViewModel은 MainCoordinatorProtocol
에만 의존하므로, 실제 MainCoordinator가 무엇인지 알 필요가 없다.
MainCoordinator
에 위에서 정의한 프로토콜을 채택시키고 구현한다:
extension MainCoordinator: MainCoordinatorProtocol {
internal func navigateToDetail() {
let detailViewModel = DetailViewModel(coordinator: DetailCoordinator())
let detailViewController = DetailViewController(viewModel: detailViewModel)
navigationController.pushViewController(detailViewController, animated: true)
}
}
MainViewModel
은 Coordinator의 구체적인 타입을 참조하지 않고, Protocol에 의존하여 동작한다:
class MainViewModel {
private weak var coordinator: MainCoordinatorProtocol?
init(coordinator: MainCoordinatorProtocol) {
self.coordinator = coordinator
}
func didTapNavigate() {
coordinator?.navigateToDetail()
}
}
MainViewModel
은 Coordinator를 직접 생성하지 않고, 생성자에서 MainCoordinatorProtocol
을 주입받음MainViewController
는 MainViewModel
에만 의존하며, MainViewModel
이 MainCoordinator
와의 통신을 처리한다:
import UIKit
import SnapKit
import Then
class MainViewController: UIViewController {
private let viewModel: MainViewModel
init(viewModel: MainViewModel) {
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
configureUI()
}
// MARK: - UI Configuration
}
MainCoordinator
는 MainViewModel
에 자신을 주입하고, MainViewController
를 초기화한다:
func start() {
let viewModel = MainViewModel(coordinator: self) // 의존성 주입
let viewController = MainViewController(viewModel: viewModel)
navigationController.pushViewController(viewController, animated: true)
}
Delegate 패턴 (프로토콜) 사용
MainViewModel.swift
protocol MainViewModelDelegate: AnyObject {
//
}
class MainViewModel {
private weak var coordinator: MainCoordinatorProtocol?
weak var delegate: MainViewModelDelegate?
init(coordinator: MainCoordinatorProtocol) {
self.coordinator = coordinator
}
func didTapNavigate() {
coordinator?.navigateToDetail()
}
}
MainViewController.swift
import UIKit
import SnapKit
import Then
class MainViewController: UIViewController, MainViewModelDelegate {
private let viewModel: MainViewModel
init(viewModel: MainViewModel) {
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
configureUI()
viewModel.delegate = self
}
// MARK: - UI Configuration
}
프로토콜 & 간략화한 Factory 패턴 활용
Protocol
기반 의존성 주입DetailViewModel
도 DetailCoordinatorProtocol
에 의존하도록 수정하고, DetailCoordinator
는 생성자가 아닌 주입 방식으로 연결한다:
DetailCoordinator.swift
protocol DetailCoordinatorProtocol: AnyObject {
func saveDetailData()
}
class DetailCoordinator: Coordinator {
var childCoordinators: [Coordinator] = []
func start() {
//
}
}
extension DetailCoordinator: DetailCoordinatorProtocol {
func saveDetailData() {
//
}
}
DetailViewModel.swift
protocol DetailViewModelDelegate: AnyObject {
//
}
class DetailViewModel {
private weak var coordinator: DetailCoordinatorProtocol?
weak var delegate: DetailViewModelDelegate?
init(coordinator: DetailCoordinatorProtocol) {
self.coordinator = coordinator
}
func saveData() {
coordinator?.saveDetailData()
}
}
MainCoordinator
에서 구체 타입 생성 제거MainCoordinator
에서 DetailCoordinator
를 직접 생성하지 않고, 의존성을 외부에서 주입받도록 수정한다:
MainCoordinator.swift
protocol MainCoordinatorProtocol: AnyObject {
func navigateToDetail()
}
class MainCoordinator: Coordinator {
var childCoordinators: [Coordinator] = []
private let navigationController: UINavigationController
private let detailCoordinatorFactory: () -> DetailCoordinatorProtocol
init(
navigationController: UINavigationController,
detailCoordinatorFactory: @escaping () -> DetailCoordinatorProtocol
) {
self.navigationController = navigationController
self.detailCoordinatorFactory = detailCoordinatorFactory
}
internal func start() {
let viewModel = MainViewModel(coordinator: self)
let viewController = MainViewController(viewModel: viewModel)
navigationController.pushViewController(viewController, animated: true)
}
}
extension MainCoordinator: MainCoordinatorProtocol {
internal func navigateToDetail() {
let detailCoordinator = detailCoordinatorFactory()
let detailViewModel = DetailViewModel(coordinator: detailCoordinator)
let detailViewController = DetailViewController(viewModel: detailViewModel)
navigationController.pushViewController(detailViewController, animated: true)
}
}
Coordinator 간의 의존성을 조정하는 책임을 AppCoordinator
에게 위임한다:
AppCoordinator.swift
class AppCoordinator: Coordinator {
var childCoordinators: [Coordinator] = []
private let navigationController: UINavigationController
private let window: UIWindow
init(window: UIWindow, navigationController: UINavigationController) {
self.window = window
self.navigationController = navigationController
}
func start() {
let mainCoordinator = MainCoordinator(
navigationController: navigationController,
detailCoordinatorFactory: { DetailCoordinator() }
)
childCoordinators.append(mainCoordinator)
mainCoordinator.start()
setupWindow()
}
private func setupWindow() {
window.rootViewController = navigationController
window.makeKeyAndVisible()
}
}
MVVM-C(Model-View-ViewModel-Coordinator) 아키텍처는 iOS 개발에서 흔히 사용되는 디자인 패턴으로, MVVM(Model-View-ViewModel) 패턴에 Coordinator를 추가하여 모듈 간의 네비게이션과 의존성을 효율적으로 관리하는 구조이다.
ViewModel
에서 전달받은 데이터를 렌더링하며, 사용자 입력 이벤트를 전달함View
와 Model
사이의 중간 관리자 역할View
에 전달ViewController
나 ViewModel
이 네비게이션 관련 코드를 가지지 않도록 함모듈화
테스트 가능성
유지보수성
의존성 역전 원칙(DIP) 준수
사용자 입력 → View → ViewModel
ViewModel → Model
Model → ViewModel
Coordinator
로그인 플로우를 MVVM-C로 구현한다고 가정해보자.
import UIKit
class LoginCoordinator: Coordinator {
var navigationController: UINavigationController
init(navigationController: UINavigationController) {
self.navigationController = navigationController
}
func start() {
let viewModel = LoginViewModel()
let loginViewController = LoginViewController(viewModel: viewModel)
viewModel.coordinator = self // ViewModel이 Coordinator를 참조
navigationController.pushViewController(loginViewController, animated: true)
}
func navigateToDashboard() {
let dashboardCoordinator = DashboardCoordinator(navigationController: navigationController)
dashboardCoordinator.start()
}
}
import Foundation
class LoginViewModel {
weak var coordinator: LoginCoordinator? // 약한 참조로 순환 참조 방지
func login(username: String, password: String) {
// 로그인 로직 (예: 네트워크 호출)
if username == "test" && password == "password" {
coordinator?.navigateToDashboard() // 성공 시 대시보드로 이동
}
}
}
import UIKit
class LoginViewController: UIViewController {
private let viewModel: LoginViewModel
init(viewModel: LoginViewModel) {
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
// UI 구성 및 버튼 액션 추가
}
@objc func loginButtonTapped() {
viewModel.login(username: "test", password: "password")
}
}
이처럼 MVVM-C 아키텍처를 사용하면, 각 컴포넌트의 역할이 명확히 분리되어 복잡한 프로젝트에서도 코드의 유지보수가 용이해진다. 필요에 따라 Combine이나 RxSwift를 도입하여 바인딩 작업을 개선할 수도 있다.
Factory 패턴(Factory Method Pattern)은 객체 생성 로직을 캡슐화하여, 객체를 직접 생성하지 않고, 이를 생성하는 메서드나 클래스를 통해 생성하는 디자인 패턴이다. 이 패턴은 객체 생성의 책임을 분리하고, 코드의 유연성과 확장성을 높이는 데 유용하다.
객체 생성 캡슐화
확장성 제공
단일 책임 원칙(SRP)
객체 생성 로직이 복잡할 때
유연성이 필요한 경우
클라이언트 코드와 객체 생성 로직을 분리하고 싶을 때
Creator (Factory)
factoryMethod
)를 정의하는 추상 클래스 또는 인터페이스ConcreteCreator
Creator
를 구현한 구체적인 클래스Product
ConcreteProduct
import UIKit
// 기본 UIButton은 Product 역할
protocol ButtonFactory {
func createButton() -> UIButton
}
class PrimaryButtonFactory: ButtonFactory {
func createButton() -> UIButton {
let button = UIButton(type: .system)
button.setTitle("Primary", for: .normal)
button.backgroundColor = .systemBlue
button.setTitleColor(.white, for: .normal)
button.layer.cornerRadius = 8
return button
}
}
class SecondaryButtonFactory: ButtonFactory {
func createButton() -> UIButton {
let button = UIButton(type: .system)
button.setTitle("Secondary", for: .normal)
button.backgroundColor = .lightGray
button.setTitleColor(.black, for: .normal)
button.layer.cornerRadius = 8
return button
}
}
func setupUI(buttonFactory: ButtonFactory) {
let button = buttonFactory.createButton()
button.frame = CGRect(x: 50, y: 100, width: 200, height: 50)
view.addSubview(button)
}
let primaryFactory = PrimaryButtonFactory()
setupUI(buttonFactory: primaryFactory)
let secondaryFactory = SecondaryButtonFactory()
setupUI(buttonFactory: secondaryFactory)
protocol ComponentFactory {
func createButton() -> UIButton
func createLabel() -> UILabel
}
class LightThemeFactory: ComponentFactory {
func createButton() -> UIButton {
let button = UIButton(type: .system)
button.backgroundColor = .white
button.setTitleColor(.black, for: .normal)
return button
}
func createLabel() -> UILabel {
let label = UILabel()
label.textColor = .black
return label
}
}
class DarkThemeFactory: ComponentFactory {
func createButton() -> UIButton {
let button = UIButton(type: .system)
button.backgroundColor = .black
button.setTitleColor(.white, for: .normal)
return button
}
func createLabel() -> UILabel {
let label = UILabel()
label.textColor = .white
return label
}
}
func setupUI(factory: ComponentFactory) {
let button = factory.createButton()
let label = factory.createLabel()
button.frame = CGRect(x: 50, y: 100, width: 200, height: 50)
label.frame = CGRect(x: 50, y: 200, width: 200, height: 20)
view.addSubview(button)
view.addSubview(label)
}
let themeFactory: ComponentFactory = DarkThemeFactory()
setupUI(factory: themeFactory)
Factory 패턴은 객체 생성 로직이 복잡하거나 객체 생성 방식이 자주 변경될 가능성이 있을 때 매우 유용하다.
다만, 단순한 프로젝트에서는 사용하지 않아도 충분하므로 필요할 때만 도입하는 것이 좋다.
클로저를 사용한 경량화된 Factory 구현은 간단한 객체 생성 로직을 처리하기 위해 Factory 패턴의 개념을 최소한으로 적용하는 방식이다. 별도의 Factory 클래스를 만들지 않고, 클로저를 사용하여 객체 생성 책임을 전달하고 캡슐화한다.
객체 생성 로직 캡슐화:
구체 클래스와의 결합 제거:
간단하고 효율적:
private let someFactory: () -> SomeProtocol
someFactory
: 객체 생성 클로저로, SomeProtocol
타입을 반환간단함:
유연성:
테스트 가능성:
의존성 주입과 잘 어울림:
protocol DetailCoordinatorProtocol {
func start()
}
class DetailCoordinator: DetailCoordinatorProtocol {
func start() {
print("DetailCoordinator started")
}
}
class MainCoordinator {
private let detailCoordinatorFactory: () -> DetailCoordinatorProtocol
init(detailCoordinatorFactory: @escaping () -> DetailCoordinatorProtocol) {
self.detailCoordinatorFactory = detailCoordinatorFactory
}
func navigateToDetail() {
let detailCoordinator = detailCoordinatorFactory()
detailCoordinator.start()
}
}
let mainCoordinator = MainCoordinator(
detailCoordinatorFactory: { DetailCoordinator() }
)
mainCoordinator.navigateToDetail()
클로저를 사용하면 Mock 객체를 쉽게 주입할 수 있다:
class MockDetailCoordinator: DetailCoordinatorProtocol {
var didStart = false
func start() {
didStart = true
}
}
func testNavigateToDetail() {
let mockCoordinator = MockDetailCoordinator()
let mainCoordinator = MainCoordinator(
detailCoordinatorFactory: { mockCoordinator }
)
mainCoordinator.navigateToDetail()
XCTAssertTrue(mockCoordinator.didStart) // 테스트 성공 여부 확인
}
단순한 객체 생성 로직:
생성 책임 분리:
테스트와 유연성 요구:
클로저를 활용한 경량화된 Factory 구현은 작고 단순한 요구사항에 적합한 방법이다.
이 방식은 테스트 가능성과 코드의 간결함을 동시에 확보하는 실용적인 방법이다!