Ver2.0 iOS 디자인 패턴의 끝판왕: Coordinator 패턴 완벽 마스터하기 🚀

iOS 디자인 패턴의 끝판왕: Coordinator 패턴 완벽 마스터하기 🚀
안녕하세요 개발자 여러분! 오늘은 2025년 3월, iOS 개발계에서 여전히 핫한 Coordinator 패턴에 대해 완전 꿀팁 대방출할게요! 😎 복잡한 화면 전환 로직에 머리 아프셨던 분들, 이제 그 고통에서 벗어날 시간이에요~
앱 개발할 때 화면 이동 로직이 뷰컨트롤러에 다 들어있어서 "이게 뭔 스파게티야..." 하고 현타 온 적 있으시죠? ㅋㅋㅋ 오늘은 그 해결책인 Coordinator 패턴을 쉽고 재밌게 알아볼게요! 재능넷에서 iOS 개발 재능을 공유하는 분들에게도 완전 꿀팁이 될 거예요! 🍯
📚 목차
- Coordinator 패턴이 뭐길래? 🤔
- 왜 Coordinator 패턴을 써야 할까요? 💡
- Coordinator 패턴 기본 구조 설계하기 🏗️
- 실전! Coordinator 패턴 구현하기 👨💻
- 심화: 다양한 Coordinator 활용 패턴 🔥
- Coordinator와 SwiftUI의 만남 🌈
- 마무리 및 추천 자료 📝
1. Coordinator 패턴이 뭐길래? 🤔
Coordinator 패턴은 앱의 화면 흐름(flow)을 관리하는 디자인 패턴이에요. 2015년에 Soroush Khanlou라는 개발자가 처음 소개했는데, 지금은 iOS 개발자들 사이에서 완전 메이저한 패턴이 됐어요!
쉽게 말하면, Coordinator는 "화면 이동의 교통정리 담당자"라고 생각하면 돼요. 뷰컨트롤러들이 서로 어떻게 연결되는지 알 필요 없이, Coordinator가 모든 화면 전환을 책임지는 구조예요. 완전 깔끔하지 않나요? ✨
진짜 간단히 말하면 이런 느낌이에요:
// 기존 방식 (뷰컨트롤러가 다른 뷰컨트롤러를 직접 생성하고 보여줌)
class ProfileViewController: UIViewController {
func showSettings() {
let settingsVC = SettingsViewController()
navigationController?.pushViewController(settingsVC, animated: true)
}
}
// Coordinator 패턴 (화면 전환은 Coordinator가 담당)
class ProfileViewController: UIViewController {
weak var coordinator: ProfileCoordinator?
func settingsButtonTapped() {
coordinator?.showSettings()
}
}
어때요? 뷰컨트롤러는 이제 자기 할 일(UI 표시)만 하고, 화면 이동은 Coordinator에게 위임하는 거죠! 완전 깔끔~
2. 왜 Coordinator 패턴을 써야 할까요? 💡
진짜 솔직히 말해서, Coordinator 패턴 없이도 앱은 잘 돌아가요. 근데 왜 이렇게 많은 개발자들이 이 패턴을 쓸까요? 그 이유는... 👇
- 뷰컨트롤러 다이어트 성공! - 뷰컨트롤러에서 화면 전환 로직을 빼니까 코드가 훨씬 날씬해져요. 뷰컨트롤러는 이제 화면 표시에만 집중할 수 있어요.
- 재사용성 UP! - 뷰컨트롤러가 특정 내비게이션 로직에 의존하지 않으니 다른 플로우에서도 쉽게 재사용할 수 있어요.
- 테스트 용이성 MAX! - 화면 전환 로직을 분리했기 때문에 각 컴포넌트를 독립적으로 테스트하기 훨씬 쉬워져요.
- 앱 플로우 한눈에 파악! - 앱의 모든 화면 흐름이 Coordinator에 정의되어 있어서 전체 구조를 쉽게 이해할 수 있어요.
- 의존성 주입 간편! - Coordinator가 뷰컨트롤러를 생성할 때 필요한 의존성을 쉽게 주입할 수 있어요.
2025년 현재, 많은 iOS 개발자들이 MVVM+C(MVVM + Coordinator) 패턴을 사용하고 있어요. 재능넷에서도 iOS 개발 재능을 공유하는 분들 대부분이 이 패턴을 활용하고 있죠! 😉
🔥 Before vs After Coordinator 패턴 적용 🔥
왼쪽: 각 VC가 서로 의존하는 복잡한 구조 vs 오른쪽: Coordinator가 모든 화면 전환을 관리하는 깔끔한 구조
3. Coordinator 패턴 기본 구조 설계하기 🏗️
자, 이제 Coordinator 패턴의 기본 구조를 설계해볼게요! 먼저 모든 Coordinator가 따라야 할 프로토콜부터 정의해볼까요?
protocol Coordinator: AnyObject {
var childCoordinators: [Coordinator] { get set }
var navigationController: UINavigationController { get set }
func start()
func childDidFinish(_ child: Coordinator?)
}
이 프로토콜의 각 요소를 살펴볼게요:
childCoordinators: 하위 Coordinator들을 관리하는 배열이에요. 메모리 관리를 위해 꼭 필요해요!navigationController: Coordinator가 화면을 표시할 때 사용할 네비게이션 컨트롤러예요.start(): Coordinator의 시작점! 여기서 첫 화면을 표시해요.childDidFinish(_:): 하위 Coordinator가 작업을 마치면 메모리에서 해제하기 위한 메서드예요.
이제 기본 Coordinator 구현체를 만들어볼게요:
class BaseCoordinator: Coordinator {
var childCoordinators: [Coordinator] = []
var navigationController: UINavigationController
init(navigationController: UINavigationController) {
self.navigationController = navigationController
}
func start() {
// 하위 클래스에서 구현
}
func childDidFinish(_ child: Coordinator?) {
if let child = child {
childCoordinators.removeAll { $0 === child }
}
}
func addChildCoordinator(_ coordinator: Coordinator) {
childCoordinators.append(coordinator)
}
}
이제 앱의 메인 Coordinator를 만들어볼게요:
class AppCoordinator: BaseCoordinator {
override func start() {
// 앱 시작 시 로그인 상태에 따라 다른 화면 보여주기
if UserDefaults.standard.bool(forKey: "isLoggedIn") {
showMainFlow()
} else {
showLoginFlow()
}
}
func showLoginFlow() {
let loginCoordinator = LoginCoordinator(navigationController: navigationController)
addChildCoordinator(loginCoordinator)
loginCoordinator.delegate = self
loginCoordinator.start()
}
func showMainFlow() {
let mainCoordinator = MainCoordinator(navigationController: navigationController)
addChildCoordinator(mainCoordinator)
mainCoordinator.start()
}
}
extension AppCoordinator: LoginCoordinatorDelegate {
func loginCoordinatorDidFinishLogin(_ coordinator: LoginCoordinator) {
childDidFinish(coordinator)
showMainFlow()
}
}
Coordinator 패턴의 핵심은 계층 구조예요. AppCoordinator가 최상위에 있고, 그 아래 LoginCoordinator, MainCoordinator 등이 있는 형태죠. 각 Coordinator는 자신의 영역에서만 화면 전환을 담당해요.
이렇게 계층 구조로 만들면 앱의 각 부분을 독립적으로 관리할 수 있어서 정말 편해요! 👍
4. 실전! Coordinator 패턴 구현하기 👨💻
이제 실제로 로그인 플로우를 구현해볼게요! 먼저 LoginCoordinator부터 만들어볼까요?
protocol LoginCoordinatorDelegate: AnyObject {
func loginCoordinatorDidFinishLogin(_ coordinator: LoginCoordinator)
}
class LoginCoordinator: BaseCoordinator {
weak var delegate: LoginCoordinatorDelegate?
override func start() {
let loginVC = LoginViewController()
loginVC.coordinator = self
navigationController.setViewControllers([loginVC], animated: true)
}
func showSignUp() {
let signUpVC = SignUpViewController()
signUpVC.coordinator = self
navigationController.pushViewController(signUpVC, animated: true)
}
func showForgotPassword() {
let forgotPasswordVC = ForgotPasswordViewController()
forgotPasswordVC.coordinator = self
navigationController.pushViewController(forgotPasswordVC, animated: true)
}
func didFinishLogin() {
delegate?.loginCoordinatorDidFinishLogin(self)
}
}
이제 LoginViewController는 어떻게 구현할까요?
class LoginViewController: UIViewController {
weak var coordinator: LoginCoordinator?
private let emailTextField = UITextField()
private let passwordTextField = UITextField()
private let loginButton = UIButton()
private let signUpButton = UIButton()
private let forgotPasswordButton = UIButton()
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
}
private func setupUI() {
// UI 설정 코드...
loginButton.addTarget(self, action: #selector(loginTapped), for: .touchUpInside)
signUpButton.addTarget(self, action: #selector(signUpTapped), for: .touchUpInside)
forgotPasswordButton.addTarget(self, action: #selector(forgotPasswordTapped), for: .touchUpInside)
}
@objc private func loginTapped() {
// 로그인 로직...
UserDefaults.standard.set(true, forKey: "isLoggedIn")
coordinator?.didFinishLogin()
}
@objc private func signUpTapped() {
coordinator?.showSignUp()
}
@objc private func forgotPasswordTapped() {
coordinator?.showForgotPassword()
}
}
보이시나요? 뷰컨트롤러는 이제 화면 전환에 대해 전혀 신경쓰지 않아요! 그냥 coordinator에게 "이런 이벤트가 발생했어요~" 하고 알려주기만 하면 돼요. 완전 깔끔하죠? ㅎㅎ
이제 MainCoordinator를 구현해볼게요:
class MainCoordinator: BaseCoordinator {
override func start() {
let tabBarController = UITabBarController()
// 홈 탭
let homeNavigationController = UINavigationController()
let homeCoordinator = HomeCoordinator(navigationController: homeNavigationController)
addChildCoordinator(homeCoordinator)
homeCoordinator.start()
// 프로필 탭
let profileNavigationController = UINavigationController()
let profileCoordinator = ProfileCoordinator(navigationController: profileNavigationController)
addChildCoordinator(profileCoordinator)
profileCoordinator.start()
// 설정 탭
let settingsNavigationController = UINavigationController()
let settingsCoordinator = SettingsCoordinator(navigationController: settingsNavigationController)
addChildCoordinator(settingsCoordinator)
settingsCoordinator.start()
// 탭바 설정
tabBarController.viewControllers = [
homeNavigationController,
profileNavigationController,
settingsNavigationController
]
navigationController.setViewControllers([tabBarController], animated: true)
}
}
와우! 이제 탭바 기반 앱의 구조도 깔끔하게 정리됐어요. 각 탭은 자신만의 Coordinator를 가지고 있어서 완전 모듈화된 구조가 됐죠! 😎
💡 꿀팁: Coordinator 메모리 관리
Coordinator 패턴을 사용할 때 가장 흔한 실수는 메모리 누수예요! childCoordinators 배열에 추가한 coordinator를 제거하지 않으면 메모리 누수가 발생해요.
항상 coordinator의 작업이 끝나면 childDidFinish(_:) 메서드를 호출해서 배열에서 제거해주세요!
// 예시: 모달로 표시한 coordinator 작업 완료 시
func dismissModal() {
navigationController.dismiss(animated: true) { [weak self] in
guard let self = self else { return }
self.childDidFinish(self.someChildCoordinator)
self.someChildCoordinator = nil
}
}
5. 심화: 다양한 Coordinator 활용 패턴 🔥
기본적인 Coordinator 패턴을 이해했다면, 이제 좀 더 심화된 내용을 알아볼까요? 2025년 현재 iOS 개발자들이 많이 사용하는 Coordinator 활용 패턴들이에요!
5.1 Factory 패턴과 Coordinator 조합하기
Coordinator에 Factory 패턴을 적용하면 뷰컨트롤러 생성 로직을 더 깔끔하게 분리할 수 있어요:
protocol ViewControllerFactory {
func makeHomeViewController() -> HomeViewController
func makeProfileViewController() -> ProfileViewController
func makeSettingsViewController() -> SettingsViewController
}
class AppViewControllerFactory: ViewControllerFactory {
let networkService: NetworkService
let userManager: UserManager
init(networkService: NetworkService, userManager: UserManager) {
self.networkService = networkService
self.userManager = userManager
}
func makeHomeViewController() -> HomeViewController {
let viewModel = HomeViewModel(networkService: networkService)
let viewController = HomeViewController(viewModel: viewModel)
return viewController
}
func makeProfileViewController() -> ProfileViewController {
let viewModel = ProfileViewModel(userManager: userManager)
let viewController = ProfileViewController(viewModel: viewModel)
return viewController
}
func makeSettingsViewController() -> SettingsViewController {
let viewModel = SettingsViewModel(userManager: userManager)
let viewController = SettingsViewController(viewModel: viewModel)
return viewController
}
}
class HomeCoordinator: BaseCoordinator {
private let factory: ViewControllerFactory
init(navigationController: UINavigationController, factory: ViewControllerFactory) {
self.factory = factory
super.init(navigationController: navigationController)
}
override func start() {
let viewController = factory.makeHomeViewController()
viewController.coordinator = self
navigationController.setViewControllers([viewController], animated: false)
}
}
이렇게 하면 의존성 주입이 훨씬 쉬워지고, 테스트도 더 용이해져요! 재능넷에서 iOS 개발 재능을 공유하는 개발자들도 이런 패턴을 많이 활용하더라구요! 😉
5.2 Deep Linking 처리하기
앱에서 딥링크를 처리해야 할 때 Coordinator 패턴이 정말 빛을 발해요:
enum DeepLink {
case product(id: String)
case profile(username: String)
case settings(section: SettingsSection)
}
extension AppCoordinator {
func handleDeepLink(_ deepLink: DeepLink) {
switch deepLink {
case .product(let id):
navigateToProduct(withID: id)
case .profile(let username):
navigateToProfile(withUsername: username)
case .settings(let section):
navigateToSettings(section: section)
}
}
private func navigateToProduct(withID id: String) {
// 메인 화면으로 이동 후 제품 상세 화면으로 이동
showMainFlow()
if let mainCoordinator = childCoordinators.first(where: { $0 is MainCoordinator }) as? MainCoordinator {
mainCoordinator.showProduct(withID: id)
}
}
// 다른 딥링크 처리 메서드들...
}
이렇게 하면 앱의 어느 상태에서든 특정 화면으로 깔끔하게 이동할 수 있어요! 👍
5.3 Coordinator와 MVVM 패턴 조합하기
MVVM과 Coordinator 패턴을 함께 사용하면 정말 강력한 아키텍처가 완성돼요:
class ProfileViewModel {
weak var coordinator: ProfileCoordinator?
private let userManager: UserManager
// 바인딩을 위한 프로퍼티
var username = Observable<String>("")
var bio = Observable<String>("")
init(userManager: UserManager) {
self.userManager = userManager
}
func loadUserData() {
let user = userManager.getCurrentUser()
username.value = user.username
bio.value = user.bio
}
func editProfileTapped() {
coordinator?.showEditProfile()
}
func logoutTapped() {
userManager.logout()
coordinator?.didLogout()
}
}
class ProfileViewController: UIViewController {
private let viewModel: ProfileViewModel
weak var coordinator: ProfileCoordinator? {
didSet {
viewModel.coordinator = coordinator
}
}
init(viewModel: ProfileViewModel) {
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()
setupUI()
bindViewModel()
viewModel.loadUserData()
}
private func bindViewModel() {
viewModel.username.bind { [weak self] username in
self?.usernameLabel.text = username
}
viewModel.bio.bind { [weak self] bio in
self?.bioLabel.text = bio
}
}
// UI 설정 및 이벤트 처리 코드...
}
MVVM+C 패턴에서는 ViewModel이 Coordinator를 참조해서 화면 전환을 요청할 수 있어요. 이렇게 하면 ViewController는 정말 UI에만 집중할 수 있게 되죠!
6. Coordinator와 SwiftUI의 만남 🌈
2025년 현재, SwiftUI가 많이 성숙해졌지만 여전히 UIKit과 함께 사용하는 경우가 많아요. SwiftUI에서도 Coordinator 패턴을 적용할 수 있을까요? 당연하죠! 😎
SwiftUI에서는 NavigationStack과 함께 Coordinator 패턴을 구현할 수 있어요:
class SwiftUIAppCoordinator: ObservableObject {
@Published var path = NavigationPath()
@Published var sheet: Sheet?
@Published var fullScreenCover: FullScreenCover?
enum Sheet: Identifiable {
case settings
case profile(userId: String)
var id: String {
switch self {
case .settings: return "settings"
case .profile(let userId): return "profile_\(userId)"
}
}
}
enum FullScreenCover: Identifiable {
case onboarding
case login
var id: String {
switch self {
case .onboarding: return "onboarding"
case .login: return "login"
}
}
}
enum Destination: Hashable {
case detail(item: Item)
case settings
case favorites
}
func showDetail(item: Item) {
path.append(Destination.detail(item: item))
}
func showSettings() {
path.append(Destination.settings)
}
func showFavorites() {
path.append(Destination.favorites)
}
func showSettingsSheet() {
sheet = .settings
}
func showProfileSheet(userId: String) {
sheet = .profile(userId: userId)
}
func showOnboardingFullScreen() {
fullScreenCover = .onboarding
}
func showLoginFullScreen() {
fullScreenCover = .login
}
func dismissSheet() {
sheet = nil
}
func dismissFullScreenCover() {
fullScreenCover = nil
}
func popToRoot() {
path.removeLast(path.count)
}
func goBack() {
path.removeLast()
}
}
이제 이 Coordinator를 SwiftUI 뷰에서 사용해볼게요:
struct ContentView: View {
@StateObject private var coordinator = SwiftUIAppCoordinator()
var body: some View {
NavigationStack(path: $coordinator.path) {
HomeView()
.environmentObject(coordinator)
.navigationDestination(for: SwiftUIAppCoordinator.Destination.self) { destination in
switch destination {
case .detail(let item):
DetailView(item: item)
.environmentObject(coordinator)
case .settings:
SettingsView()
.environmentObject(coordinator)
case .favorites:
FavoritesView()
.environmentObject(coordinator)
}
}
.sheet(item: $coordinator.sheet) { sheet in
switch sheet {
case .settings:
SettingsSheetView()
.environmentObject(coordinator)
case .profile(let userId):
ProfileSheetView(userId: userId)
.environmentObject(coordinator)
}
}
.fullScreenCover(item: $coordinator.fullScreenCover) { cover in
switch cover {
case .onboarding:
OnboardingView()
.environmentObject(coordinator)
case .login:
LoginView()
.environmentObject(coordinator)
}
}
}
}
}
struct HomeView: View {
@EnvironmentObject var coordinator: SwiftUIAppCoordinator
var body: some View {
VStack {
Text("Home Screen")
.font(.largeTitle)
Button("Show Detail") {
coordinator.showDetail(item: Item(id: "1", name: "Sample Item"))
}
Button("Show Settings") {
coordinator.showSettings()
}
Button("Show Settings Sheet") {
coordinator.showSettingsSheet()
}
Button("Show Login") {
coordinator.showLoginFullScreen()
}
}
}
}
SwiftUI에서도 Coordinator 패턴의 핵심 아이디어인 '화면 전환 로직 분리'를 구현할 수 있어요! 이렇게 하면 SwiftUI 뷰도 화면 전환 로직에서 자유로워져요.
2025년에는 SwiftUI와 UIKit을 혼합해서 사용하는 경우도 많은데, 이럴 때도 Coordinator 패턴이 큰 도움이 돼요:
class MixedCoordinator: BaseCoordinator {
override func start() {
let homeVC = UIHostingController(rootView: HomeView().environmentObject(SwiftUIAppCoordinator()))
navigationController.setViewControllers([homeVC], animated: false)
}
func showUIKitScreen() {
let detailVC = DetailViewController()
detailVC.coordinator = self
navigationController.pushViewController(detailVC, animated: true)
}
func showSwiftUIScreen() {
let swiftUIView = ProfileView(userId: "123")
let hostingController = UIHostingController(rootView: swiftUIView)
navigationController.pushViewController(hostingController, animated: true)
}
}
이렇게 하면 UIKit과 SwiftUI를 오가는 화면 전환도 깔끔하게 처리할 수 있어요! 👍
7. 마무리 및 추천 자료 📝
지금까지 Coordinator 패턴에 대해 알아봤는데요, 어떠셨나요? 처음에는 좀 복잡해 보일 수 있지만, 한번 익숙해지면 앱 구조가 얼마나 깔끔해지는지 느낄 수 있을 거예요! 😊
🌟 Coordinator 패턴 핵심 정리 🌟
- 화면 전환 로직을 뷰컨트롤러에서 분리하여 단일 책임 원칙을 지켜요.
- 계층적 구조로 앱의 다양한 플로우를 관리해요.
- 의존성 주입이 용이해져 테스트하기 쉬운 코드가 돼요.
- 딥링크 처리가 훨씬 간단해져요.
- MVVM과 결합하면 더욱 강력한 아키텍처가 완성돼요.
- SwiftUI에서도 적용 가능한 유연한 패턴이에요.
Coordinator 패턴은 2025년 현재 iOS 개발에서 정말 중요한 패턴 중 하나예요. 재능넷에서 iOS 개발 재능을 찾고 계신다면, Coordinator 패턴을 잘 이해하고 구현할 수 있는 개발자를 만나보세요! 🚀
📚 추천 자료
- "Advanced iOS App Architecture" - Raywenderlich 팀의 책 (2025 개정판)
- "Coordinators Essential Tutorial" - Hacking with Swift
- "MVVM-C in Practice" - Swift by Sundell
- "Coordinator Pattern in SwiftUI" - SwiftUI Lab
- "Testing Coordinators" - Swift Testing Tips
여러분의 iOS 개발 여정에 Coordinator 패턴이 큰 도움이 되길 바랍니다! 질문이나 의견이 있으시면 언제든지 댓글로 남겨주세요. 함께 성장해요! 💪
🌈 iOS 개발 재능이 필요하신가요?
재능넷에서 Coordinator 패턴을 능숙하게 다루는 iOS 개발자를 만나보세요! 복잡한 앱 구조도 깔끔하게 설계해드립니다.
재능넷 - 당신의 재능을 나누고, 필요한 재능을 찾는 곳
관련 키워드
댓글 1
지식인의 숲 - 지적 재산권 보호 고지
지적 재산권 보호 고지
- 저작권 및 소유권: 본 컨텐츠는 재능넷의 독점 AI 기술로 생성되었으며, 대한민국 저작권법 및 국제 저작권 협약에 의해 보호됩니다.
- AI 생성 컨텐츠의 법적 지위: 본 AI 생성 컨텐츠는 재능넷의 지적 창작물로 인정되며, 관련 법규에 따라 저작권 보호를 받습니다.
- 사용 제한: 재능넷의 명시적 서면 동의 없이 본 컨텐츠를 복제, 수정, 배포, 또는 상업적으로 활용하는 행위는 엄격히 금지됩니다.
- 데이터 수집 금지: 본 컨텐츠에 대한 무단 스크래핑, 크롤링, 및 자동화된 데이터 수집은 법적 제재의 대상이 됩니다.
- AI 학습 제한: 재능넷의 AI 생성 컨텐츠를 타 AI 모델 학습에 무단 사용하는 행위는 금지되며, 이는 지적 재산권 침해로 간주됩니다.
댓글 작성
이 글에 대한 여러분의 생각을 들려주세요
로그인이 필요합니다
댓글을 작성하려면 먼저 로그인해주세요.