반응형

SwiftUI Study – @State / @Binding / @ObservedObject / @StateObject / @EnvironmentObject 정확한 역할 이해하기


 

1. 왜 중요한가 (문제 배경)

SwiftUI의 핵심은 상태(State)입니다.

상태를 어디에 두느냐는 앱의 성능, UI 일관성, 렌더링 빈도, 유지보수성에 큰 영향을 줍니다.

많은 개발자가 다음과 같은 혼란을 겪습니다.

  • @State와 @ObservedObject 차이를 모름
  • ViewModel을 @ObservedObject로 선언해 매번 초기화됨
  • @Binding 남용으로 부모·자식 간 상태가 꼬임
  • @EnvironmentObject를 전역 변수처럼 사용해 의존성 증가

이 팁은 SwiftUI 상태 관리의 가장 중요한 원칙을 정리합니다.


 

2. 잘못된 패턴 예시

❌ 예시 1: ViewModel을 @ObservedObject로 선언해 매번 재생성됨

struct WrongView: View {
    @ObservedObject var viewModel = UserViewModel()   // ❌ 문제 발생

    var body: some View {
        Text(viewModel.name)
    }
}

문제:

  • @ObservedObject는 외부에서 주입될 때 적합한 방식
  • 직접 생성하면 View가 다시 그려질 때마다 ViewModel이 재생성됨
  • API 요청, 초기화 로직이 반복 실행됨 → 성능 저하 + 의도치 않은 사이드이펙트

❌ 예시 2: UI 상태와 도메인 상태를 섞음

struct WrongLoginView: View {
    @State private var email = ""
    @State private var password = ""
    @State private var isLoading = false
    @State private var isLoggedIn = false    // ❌ 도메인 상태까지 UI에서 관리

    var body: some View {
        // 전체 로직이 View 내부 존재 → 테스트 불가능 구조
    }
}

문제:

  • UI 상태와 비즈니스 상태가 뒤섞임
  • 다른 화면에서 재사용하기 어려움
  • 유지보수 비용 증가

❌ 예시 3: 무분별한 Binding 전달로 상태 구조가 꼬임

struct ParentView: View {
    @State private var isOn = false

    var body: some View {
        ChildSwitch(isOn: $isOn)   // ❌ 항상 좋은 구조는 아님
    }
}

문제:

  • 자식이 부모 상태를 자유롭게 변경
  • 상태 동기화 경로가 여러 군데로 확산
  • 디버깅 어려움

❌ 예시 4: EnvironmentObject 남용

class AppState: ObservableObject {
    @Published var user: User?
}

struct SomeView: View {
    @EnvironmentObject var appState: AppState   // ❌ 전역 변수처럼 사용
}

문제:

  • 의존성이 숨겨져 있어 추적 어려움
  • preview에서 제공 안 하면 크래시
  • 상태 공유 범위가 과도해짐

 

3. 올바른 패턴 예시

✅ 예시 1: ViewModel은 @StateObject로 선언

struct UserProfileView: View {
    @StateObject private var viewModel = UserViewModel()  // 올바른 사용

    var body: some View {
        Text(viewModel.name)
    }
}

장점:

  • ViewModel이 한 번만 생성
  • API 요청, Timer 등이 안정적으로 운용됨

✅ 예시 2: 도메인 상태는 ViewModel에서, UI 상태는 View에서 관리

struct LoginView: View {
    @State private var email = ""
    @State private var password = ""

    @StateObject private var viewModel = LoginViewModel()

    var body: some View {
        VStack {
            TextField("Email", text: $email)
            SecureField("Password", text: $password)

            Button("로그인") {
                viewModel.login(email: email, password: password)
            }

            if viewModel.isLoading {
                ProgressView()
            }
        }
    }
}

✅ 예시 3: Binding 대신 단방향 데이터 흐름 유지

Child(
    value: isOn,
    onToggle: { isOn = $0 }
)

장점:

  • 상태가 한 방향으로 흐름
  • 예상 가능한 구조

✅ 예시 4: EnvironmentObject는 전역 상태일 때만 최소한으로

적절한 사용:

  • UserSession
  • AppSettings
  • App-wide Permission State

부적절한 사용:

  • 특정 화면에서만 필요한 ViewModel
  • 단순 UI 상태

 

4. 실전 적용 팁

✔ 상태 소유권(Single Source of Truth)을 먼저 결정

SwiftUI 상태 설계에서 가장 중요한 질문:

“이 상태의 주인은 누구인가?”

  • UI 자체: @State
  • 부모 → 자식 변경 필요: @Binding
  • ViewModel: @StateObject
  • 외부에서 주입되는 모델: @ObservedObject
  • 앱 전체 공유: @EnvironmentObject

✔ 단방향 데이터 흐름을 유지하라

SwiftUI는 React와 동일하게 단방향 데이터 흐름이 핵심입니다.

State → View → User Action → State 변경

Binding 남용은 이 구조를 혼란스럽게 만듦.


✔ ViewModel의 수명 주기는 @StateObject로 보장

Navigation 이동 시 ViewModel이 살아 있어야 한다면 반드시 @StateObject.


✔ 속성 래퍼 선택 기준표

상황 올바른 선택
View 내부 전용 UI 상태 @State
부모 상태를 자식이 수정 @Binding
외부에서 주입된 ObservableObject @ObservedObject
ViewModel처럼 View가 소유해야 할 객체 @StateObject
앱 전체에서 공유되는 전역 상태 @EnvironmentObject

 

5. 정리

  • SwiftUI 상태 래퍼의 선택은 앱 구조의 절반 이상을 결정한다
  • @StateObject와 @ObservedObject 혼동은 가장 흔한 실수
  • Binding 남용은 단방향 상태 흐름을 깨뜨린다
  • EnvironmentObject는 전역 상태일 때만 최소한으로
  • UI 상태와 도메인 상태는 반드시 분리

상태만 올바르게 설계해도 SwiftUI 앱의 안정성은 크게 향상됩니다.

 

반응형
Posted by 까칠코더
,