반응형

SwiftUI Study – EnvironmentValues와 커스텀 EnvironmentKey로 의존성을 깔끔하게 주입하는 방법

 

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

SwiftUI에서 의존성을 주입하는 방법으로 가장 자주 떠올리는 것은 다음과 같습니다.

  • 이니셜라이저 파라미터로 직접 전달
  • @EnvironmentObject
  • 싱글톤 (MyService.shared)

하지만 앱 규모가 커질수록 이런 방식은 다음과 같은 문제를 만듭니다.

  • View 계층 깊은 곳까지 파라미터가 줄줄이 전달됨
  • EnvironmentObject가 점점 늘어나면서 전역 상태가 오염
  • 싱글톤에 강하게 결합되어 테스트가 어려움
  • 프리뷰/테스트 환경과 실제 앱 환경을 다르게 구성하기 어려움

여기서 유용하게 쓸 수 있는 도구가 바로 EnvironmentValues + 커스텀 EnvironmentKey 입니다.

“EnvironmentValues는 뷰 트리 전체에 전파되는 읽기 전용 설정/서비스 공간 이다.”

이를 활용하면 다음을 구현할 수 있습니다.

  • 네트워크 클라이언트, 로깅, 트래킹 등 서비스 주입
  • A/B 테스트, 플래그, 환경별 설정 값
  • 테스트/미리보기에서만 다른 구현체를 쉽게 교체

 

2. 잘못된 패턴 예시

❌ 예시 1: 어디서든 싱글톤 직접 참조

struct ProfileView: View {
    var body: some View {
        VStack {
            Text("프로필")
        }
        .onAppear {
            Analytics.shared.log(event: "profile_appear")   // ❌ 강한 결합
        }
    }
}

문제점

  • 모든 View가 Analytics.shared 에 의존
  • 테스트 시 다른 구현으로 교체하기 어려움
  • 프리뷰에서 호출되면 원치 않는 네트워크/로깅 실행 가능

❌ 예시 2: EnvironmentObject로 서비스까지 전달

final class ServiceContainer: ObservableObject {
    let api: APIClient
    let analytics: Analytics
}

@EnvironmentObject var services: ServiceContainer   // ❌ 서비스까지 EnvironmentObject로

문제점

  • 서비스와 상태가 뒤섞인 거대한 컨테이너 탄생
  • 어디서든 services.api 로 접근 가능 → 사실상 글로벌
  • View 구조가 서비스 컨테이너에 종속되어 버림

 

3. 올바른 패턴 예시

✅ 예시 1: AnalyticsService를 EnvironmentKey로 주입

먼저 프로토콜 정의:

protocol AnalyticsService {
    func log(event: String)
}

struct DefaultAnalyticsService: AnalyticsService {
    func log(event: String) {
        print("LOG:", event)
    }
}

EnvironmentKey 정의:

private struct AnalyticsKey: EnvironmentKey {
    static let defaultValue: AnalyticsService = DefaultAnalyticsService()
}

EnvironmentValues 확장:

extension EnvironmentValues {
    var analytics: AnalyticsService {
        get { self[AnalyticsKey.self] }
        set { self[AnalyticsKey.self] = newValue }
    }
}

View에서 사용:

struct ProfileView: View {
    @Environment(\.analytics) private var analytics

    var body: some View {
        VStack {
            Text("프로필")
        }
        .onAppear {
            analytics.log(event: "profile_appear")
        }
    }
}

루트 쪽에서 서비스 주입:

@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            ProfileView()
                .environment(\.analytics, DefaultAnalyticsService())
        }
    }
}

장점

  • View는 구체 타입이 아니라 AnalyticsService 프로토콜에만 의존
  • 테스트/프리뷰에서 다른 구현체를 쉽게 넣을 수 있음
  • 싱글톤 의존성이 제거되고, 구조적으로 훨씬 깔끔해짐

✅ 예시 2: 프리뷰에서 테스트용/더미 서비스 주입

struct MockAnalyticsService: AnalyticsService {
    func log(event: String) {
        print("PREVIEW LOG:", event)
    }
}

struct ProfileView_Previews: PreviewProvider {
    static var previews: some View {
        ProfileView()
            .environment(\.analytics, MockAnalyticsService())
    }
}

장점

  • 프리뷰에서는 실제 로깅/네트워크 호출 없이 동작
  • 로그를 통해 화면 단위 이벤트 흐름을 눈으로 확인 가능
  • 실제 앱 환경과 프리뷰 환경을 분리하기 쉬움

✅ 예시 3: 다국어/지역별 설정, Feature Flag 도입

struct FeatureFlags {
    var isNewPayFlowEnabled: Bool
}

private struct FeatureFlagsKey: EnvironmentKey {
    static let defaultValue = FeatureFlags(isNewPayFlowEnabled: false)
}

extension EnvironmentValues {
    var featureFlags: FeatureFlags {
        get { self[FeatureFlagsKey.self] }
        set { self[FeatureFlagsKey.self] = newValue }
    }
}

사용:

struct PaymentRootView: View {
    @Environment(\.featureFlags) private var flags

    var body: some View {
        Group {
            if flags.isNewPayFlowEnabled {
                NewPaymentView()
            } else {
                LegacyPaymentView()
            }
        }
    }
}

앱 루트에서:

WindowGroup {
    PaymentRootView()
        .environment(\.featureFlags, FeatureFlags(isNewPayFlowEnabled: true))
}

장점

  • 런타임 구성(플래그, A/B 테스트 등)을 Environment로 깔끔하게 제어
  • View는 단순히 flags 값에 따라 분기만 하면 됨

 

4. 실전 적용 팁

✔ 팁 1 – “전역 서비스 + 설정 값”은 EnvironmentKey를 우선 고려

Analytics, Logger, Theme, FeatureFlags 등은 Environment에 딱 맞는 대상.

✔ 팁 2 – EnvironmentObject는 “상태 공유”에, Environment는 “정책/서비스”에

두 개념의 역할을 분리하면 설계가 명확해진다.

✔ 팁 3 – 프로토콜 기반 설계로 테스트 가능성 확보

View는 항상 추상 타입(프로토콜)에 의존하도록 만들어야 한다.

✔ 팁 4 – 프리뷰에서 다른 구현체를 넣어보며 확인

Mock 서비스, Stub API를 넣어 동작을 쉽게 검증할 수 있다.

✔ 팁 5 – 싱글톤 참조 코드를 점진적으로 Environment 기반으로 치환

기존 UIKit + 싱글톤 구조에서도 SwiftUI 전환 시 자연스럽게 적용 가능하다.

 

5. 정리

  • EnvironmentValues + 커스텀 EnvironmentKey를 사용하면
    전역 서비스·설정·플래그를 뷰 트리 전체에 깔끔하게 주입할 수 있다.
  • View는 구체 구현 대신 프로토콜과 Environment에 의존하게 되어
    테스트·프리뷰·환경 별 분기가 훨씬 쉬워진다.
  • EnvironmentObject는 “변하는 상태”, Environment는 “정책/서비스” 라는 역할 분리를 명확히 하면
    SwiftUI 아키텍처가 훨씬 단순하고 예측 가능해진다.
반응형
Posted by 까칠코더
,