2026년에 SwiftUI 프로젝트를 구조화하는 방법
Source: Dev.to
문제
Xcode는 ContentView.swift만 제공하고 그게 전부입니다. 앱이 커지면서 다음과 같은 상황이 발생합니다:
- 한 폴더에 50개의 파일
- ViewModel이 View와 섞여 있음
- 명확한 관심사의 분리 없음
- 무언가를 찾아야 할 때마다 고통
해결책: 기능 기반 아키텍처
MyApp/
├── App/
│ ├── MyAppApp.swift
│ ├── AppDelegate.swift (if needed)
│ └── AppConfiguration.swift
├── Features/
│ ├── Auth/
│ │ ├── Views/
│ │ │ ├── LoginView.swift
│ │ │ └── RegisterView.swift
│ │ ├── ViewModels/
│ │ │ └── AuthViewModel.swift
│ │ └── Models/
│ │ └── User.swift
│ ├── Home/
│ │ ├── Views/
│ │ ├── ViewModels/
│ │ └── Models/
│ └── Profile/
│ ├── Views/
│ ├── ViewModels/
│ └── Models/
├── Core/
│ ├── Network/
│ │ ├── NetworkManager.swift
│ │ ├── APIEndpoint.swift
│ │ └── APIError.swift
│ ├── Storage/
│ │ ├── StorageManager.swift
│ │ └── KeychainManager.swift
│ └── Extensions/
│ ├── View+Extensions.swift
│ └── String+Extensions.swift
├── UI/
│ ├── Components/
│ │ ├── PrimaryButton.swift
│ │ ├── LoadingView.swift
│ │ └── ErrorView.swift
│ └── Theme/
│ ├── Colors.swift
│ ├── Fonts.swift
│ └── Spacing.swift
└── Resources/
├── Assets.xcassets
└── Localizable.strings
왜 이렇게 작동하는가
1. 기능은 자체 포함형
각 기능은 필요한 모든 것을 가지고 있습니다:
- Views (UI)
- ViewModels (로직)
- Models (데이터)
새로운 기능을 추가하나요? 새 폴더를 만들면 됩니다. 기능을 제거하나요? 폴더를 삭제하면 됩니다.
2. Core는 공유 로직
기능들 사이에서 사용되는 것들:
- 네트워킹
- 스토리지
- 확장(Extensions)
한 곳에서 관리하고, 어디서든 사용합니다.
3. UI는 재사용 가능
모든 기능이 사용하는 컴포넌트와 테마:
// UI/Theme/Colors.swift
import SwiftUI
enum AppColors {
static let primary = Color("Primary")
static let secondary = Color("Secondary")
static let background = Color("Background")
static let text = Color("Text")
}
// UI/Components/PrimaryButton.swift
import SwiftUI
struct PrimaryButton: View {
let title: String
let action: () -> Void
var body: some View {
Button(action: action) {
Text(title)
.font(.headline)
.foregroundColor(.white)
.frame(maxWidth: .infinity)
.padding()
.background(AppColors.primary)
.cornerRadius(12)
}
}
}
ViewModel 패턴
모든 기능의 ViewModel은 동일한 패턴을 따릅니다:
@MainActor
final class HomeViewModel: ObservableObject {
// MARK: - Published State
@Published private(set) var items: [Item] = []
@Published private(set) var isLoading = false
@Published private(set) var error: Error?
// MARK: - Dependencies
private let networkManager: NetworkManager
init(networkManager: NetworkManager = .shared) {
self.networkManager = networkManager
}
// MARK: - Public Methods
func loadItems() async {
isLoading = true
error = nil
do {
items = try await networkManager.fetch(ItemsEndpoint())
} catch {
self.error = error
}
isLoading = false
}
}
핵심 포인트
@MainActor는 UI 업데이트가 메인 스레드에서 이루어지도록 보장합니다.private(set)은 외부에서의 변경을 방지합니다.- 의존성 주입을 통해 테스트가 가능해집니다.
탐색
간단한 앱 – 네이티브 SwiftUI 탐색
NavigationStack {
HomeView()
.navigationDestination(for: Item.self) { item in
DetailView(item: item)
}
}
복잡한 앱 – 코디네이터 패턴
@MainActor
final class AppCoordinator: ObservableObject {
@Published var path = NavigationPath()
enum Destination: Hashable {
case detail(Item)
case settings
case profile
}
func navigate(to destination: Destination) {
path.append(destination)
}
func pop() {
path.removeLast()
}
func popToRoot() {
path.removeLast(path.count)
}
}
빠른 설정 스크립트
구조를 자동으로 생성합니다:
#!/bin/bash
mkdir -p App
mkdir -p Features/{Auth,Home,Profile}/{Views,ViewModels,Models}
mkdir -p Core/{Network,Storage,Extensions}
mkdir -p UI/{Components,Theme}
mkdir -p Resources
내 전체 템플릿
이 모든 것(실제 코드 포함)을 시작 템플릿으로 패키징했습니다. 이 템플릿에는 다음이 포함됩니다:
- 5개의 준비된 화면
- 20개 이상의 UI 구성 요소
NetworkManager와 async/await 지원- 다크 모드 지원
- MVVM 아키텍처
설정을 건너뛰고 싶다면 여기에서 확인하세요.
TL;DR
- Feature‑based structure – 기능별로 그룹화하고 파일 유형별이 아니라.
- Core – 공유 유틸리티.
- UI – 재사용 가능한 컴포넌트와 테마.
- MVVM –
@MainActor가 적용된 ViewModel. - Be consistent – 모든 곳에 동일한 패턴을 적용하세요.