SwiftUI에서 글로벌 AppState 아키텍처
Source: Dev.to
번역하려는 전체 텍스트를 제공해 주시면, 요청하신 대로 마크다운 형식과 코드 블록, URL은 그대로 유지하면서 한국어로 번역해 드리겠습니다.
Introduction
SwiftUI 앱이 커짐에 따라 흔히 떠오르는 질문이 있습니다: 이 상태는 어디에 존재하나요?
일반적인 전역 관심사에는 다음이 포함됩니다:
- 인증 상태
- 선택된 탭
- 딥 링크
- 온보딩 흐름
- 전역 오류
- 앱 라이프사이클 이벤트
- 기능 조정
이 상태를 뷰와 뷰 모델에 흩어 놓으면 예측할 수 없는 네비게이션, 중복된 로직, 디버깅이 어려운 버그, 그리고 잘못된 시점에 상태가 리셋되는 문제가 발생합니다.
해결책은 Global AppState입니다 – 앱을 거대한 신 객체로 만들지 않으면서도 깔끔하고 확장 가능한 아키텍처를 제공합니다.
AppState에 포함되는 내용
AppState는 전역적인, 기능 간 상태를 나타내며 다음과 같은 특성을 가집니다:
- 개별 화면보다 오래 지속됨
- 여러 기능을 조정함
- 라이프사이클 변화에 반응함
- 네비게이션을 주도함
- 뷰 재생성 시에도 유지됨
전형적인 예시:
- 사용자 세션 / 인증 상태
- 선택된 탭
- 전역 네비게이션 라우트
- 딥링크 처리
- 앱 단계 (포그라운드/백그라운드)
- 전역 로딩 / 동기화 플래그
- 전역 오류 표시
일반적인 규칙
- 여러 기능이 해당 상태에 관심이 있다면 → AppState
- 한 화면만 해당 상태에 관심이 있다면 → ViewModel
AppState에 포함되지 말아야 할 항목
- 텍스트 필드 값
- 스크롤 위치
- 로컬 애니메이션
- 기능‑별 데이터 리스트
- 임시 UI 토글
@Observable 로 AppState 정의
@Observable
class AppState {
var session: UserSession?
var selectedTab: Tab = .home
var route: AppRoute?
var isSyncing = false
var pendingDeepLink: DeepLink?
}
이 객체는 앱이 실행되는 전체 기간 동안 존재하고, 한 번 주입된 뒤 고수준 동작을 주도합니다.
앱 상태 주입
@main
struct MyApp: App {
@State private var appState = AppState()
var body: some Scene {
WindowGroup {
RootView()
.environment(appState)
}
}
}
뷰에서 AppState에 접근하기
@Environment(AppState.self) var appState
싱글톤이 필요하지 않습니다.
Source:
AppState를 통한 네비게이션
enum AppRoute: Hashable {
case login
case home
case profile(id: String)
}
루트 뷰에서:
.navigationDestination(for: AppRoute.self) { route in
switch route {
case .login:
LoginView()
case .home:
HomeView()
case .profile(let id):
ProfileView(userID: id)
}
}
상태를 업데이트하여 네비게이션 트리거:
appState.route = .profile(id: user.id)
장점
- 예측 가능함
- 테스트 용이함
- 딥링크 친화적
중앙 집중식 딥‑링크 처리
func handle(_ link: DeepLink) {
switch link {
case .profile(let id):
route = .profile(id: id)
case .home:
selectedTab = .home
}
}
뷰는 더 이상 URL을 파싱하지 않으며; AppState가 외부 인텐트를 → 내부 상태로 변환합니다.
라이프사이클 변화에 대응하기
@Environment(\.scenePhase) var phase
.onChange(of: phase) { newPhase in
switch newPhase {
case .background:
saveState()
case .active:
refreshIfNeeded()
default:
break
}
}
이렇게 하면 뷰와 뷰 모델에서 라이프사이클 로직을 분리할 수 있습니다.
AppState 과부하 방지
Bad example
class AppState {
var feedPosts: [Post]
var profileViewModel: ProfileViewModel
var searchText: String
}
Good approach
AppState는 조정 역할을 합니다.- 기능별 뷰 모델이 데이터를 소유합니다.
- 서비스가 부수 효과를 처리합니다.
하위 상태를 사용한 AppState 확장
@Observable
class AppState {
var sessionState = SessionState()
var navigationState = NavigationState()
var syncState = SyncState()
}
각 하위 상태는 명확한 책임과 제한된 범위를 가지고 있어, 대규모 앱에서도 아름답게 확장됩니다.
AppState 테스트
func test_navigation_changes_route() {
let appState = AppState()
appState.route = .login
XCTAssertEqual(appState.route, .login)
}
func test_profile_deep_link() {
let appState = AppState()
appState.handle(.profile(id: "123"))
XCTAssertEqual(appState.route, .profile(id: "123"))
}
UI가 포함되지 않으며, 테스트는 순수 데이터에 초점을 맞춥니다.
상태 흐름
User Action
↓
ViewModel
↓
AppState (if global)
↓
View reacts
잘 설계된 글로벌 AppState의 장점
- 탐색을 단순화한다
- 수명 주기 동작을 안정화한다
- 딥 링크를 간단하게 만든다
- 전역 해킹을 제거한다
- 테스트 가능성을 향상시킨다
- 팀 간 확장성을 제공한다
절제해서 사용할 때, Global AppState는 SwiftUI에서 가장 강력한 아키텍처 도구 중 하나이다.