SwiftUI에서 글로벌 AppState 아키텍처

발행: (2025년 12월 18일 오전 06:24 GMT+9)
6 min read
원문: Dev.to

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에서 가장 강력한 아키텍처 도구 중 하나이다.

Back to Blog

관련 글

더 보기 »