꿈돌이랜드

swift-dependencies: Dependency lifetimes 본문

Programming/SwiftUI

swift-dependencies: Dependency lifetimes

loinsir 2024. 6. 13. 01:31
반응형

Dependency lifetimes

How task locals work

  • Dependency 프로퍼티 래퍼가 초기화되면, 그 순간 dependency의 현재 상태를 캡처합니다.
  • @TaskLocal 변수가 새로운 비동기 task들로부터 상속되는 것과 비슷합니다.
    • TaskLocal 변수는 withValue 메서드 Scope 내에서만 값을 변경 가능합니다.
      • 이는 TaskLocal 변수가 동시성 환경에서 Thread-safe하게 만듦니다.
    • 단, 상속된 Task의 Scope 내에서는 부모 Task의 TaskLocal 값을 상속받습니다.
    • 하지만, 일반적으로 task local은 escaping closure 범위를 넘어설 때마다 오버라이드를 잃습니다.
    • 아래 예시 코드 처럼 withValue로 오버라이드한 값이 asyncAfter 클로저 내에서 다시 1로 돌아가는 것을 볼 수 있습니다.
      print(Locals.value) // 1
      Locals.$value.withValue(42) {
      	print(Locals.value) // 42
      	DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
      	  print(Locals.value) // 1
      	}
      	print(Locals.value) // 42
      }
      
  • 결론적으로, Swift는 보편적이진 않지만, task local 변수들을 특정 escaping, 비구조적 컨텍스트로 전파시키기 위해서는 추가적인 작업을 해야합니다.

How @Dependency lifetimes work

  • 이제 task local 변수들이 어떻게 작동하는지 알았으니, @Dependency의 생명주기를 이해할 수 있습니다.
  • dependencies는 @TaskLocal로서 유지되고, 많은 task locals의 규칙 또한 dependencies에 적용됩니다.
  • 예를 들어 dependencies들은 tasks에서 상속되지만, 일반적으로 escaping 경계를 넘지는 않습니다.
  • 하지만 몇가지 주의할 사항이 있습니다.
  • task local과 마찬가지로, dependency의 값은 withDependencies의 trailing, non-escaping 클로저내에서 변경될 수 있습니다.
  • 하지만 라이브러리는 잘 정의된 방식으로 변경을 연장할 수 있는 몇가지 방법을 제공합니다.
  • 예를 들어, 사용자 정보를 가져오기 위한 API 클라이언트에 액세스한다고 가정해봅시다.
    class FeatureModel: ObservableObject {
      @Dependency(\.apiClient) var apiClient
    
    
      func onAppear() async {
        do {
          self.user = try await self.apiClient.fetchUser()
        } catch {}
      }
    }
  • 때로는 apiClient의 다른 구현을 사용하는 통제된 환경에서 이 모델을 구성하고 싶을 수 있습니다.
  • 아마 테스트가 대부분 이러한 예시일 것입니다.
  • 테스트에서, 우리는 외부 세계의 모호한 상황에 노출되기 때문에 라이브 네트워크 요청을 만들고 싶지 않습니다.
  • 대신 우리는 데이터가 어떻게 우리의 기능 로직에 흐르는지 테스트하고 싶기에, 동기적이고, 즉각적으로 데이터를 리턴시키도록 구현을 제공하고 싶습니다.
  • 라이브러리에서 이를 수행하기 위한 helper가 제공되며 이를WithDependencies(_:operation:) 라 합니다.
  • 이는 두가지 클로저를 취하는데, 첫번째는 원하는 dependencies를 오버라이드 가능하게 하는 것이고, 두번째는 그 변환된 dependencies가 적용된 스코프에서 기능 로직이 실행되도록 하는 것입니다.
    func testOnAppear() async {
      await withDependencies {
        $0.apiClient.fetchUser = { _ in User(id: 42, name: "Blob") }
      } operation: {
        let model = FeatureModel()
        XCTAssertEqual(model.user, nil)
        await model.onAppear()
        XCTAssertEqual(model.user, User(id: 42, name: "Blob"))
      }
    }
  • 그래서 위의 예시에서 모든 operation 클로저는 실제 네트워크 요청 없이 기능 코드 실행이 가능케 합니다.
  • 한단계 더 나아가, operation 후행 클로저 범위에서 전체 테스트를 실행할 필요가 없습니다.
  • 해당 스코프에서 모델을 구성하기만 하면 되고, 모든 dependencies들이 FeatureModel 내의 인스턴스 변수로 선언되어있는 한, 모델과의 모든 상호 작용은 클로저 외부에서도 제어된 종속성을 사용합니다.

 

중략…

 

  • 그러나, 자식 모델을 부모 모델로부터 생성할 때는 주의해야 합니다.
  • 부모의 의존성으로부터 자식의 의존성이 상속되기 때문에, 자식 모델을 생성할 때 반드시 withDependencies(from:operation:file:line:) 을 사용해야 합니다.
    let onboardingModel = withDependencies(from: self) {
      $0.apiClient = .mock
    } operation: {
      FeatureModel()
    }

 

  • 일반적으로, 만약 앱의 매 기능 계층마다 적절하게 의존성들이 상속되는 것을 원한다면, 어떠한 ObservableObject 모델들은 withDependencies(from:operation:file:line) 내에서 생성해야 합니다.
  • Dependencies는 이미 previewValue 라는 개념을 지원하고 있기에, 이렇게 된다면 또한 매우 특정한 환경에서 프리뷰를 실행할 수 있게됩니다.

… 중략…

  • 때떄로 매우 특정한 state에서 기능이 어떻게 동작하는지 보기 위해 의존성을 커스터마이징하고 싶을 수 있습니다. 예를 들어 만약, fetchUser 엔드포인트가 에러를 내뱉을 때, 어떻게되는지 보기 위해 프리뷰를 다음과 같이 업데이트 할 수 있습니다:
    struct Feature_Previews: PreviewProvider {
      static var previews: some View {
        FeatureView(
          model: withDependencies {
            $0.apiClient.fetchUser = { _ in
              struct SomeError: Error {}
              throw SomeError()
            }
          } operation: {
            FeatureModel()
          }
        )
      }
    }

Accessing a @Dependency from pre-structured concurrency

  • 의존성들은 task local 내에서 점유되기에, 오직 자동적으로 structured concurrency와 Task 내에 자동적으로 전파됩니다.
  • escaping 클로저 너머 의존성들에 접근하기 위해서 (예를 들어 콜백이나 Combine 연산자) closure 안으로 전파될 수 있도록 추가적인 escape 작업을 의존성들에 해주어야 합니다.
  • 예를 들어 특정 작업을 딜레이 시키기 위해 DispatchQueue.main.asyncAfter 를 사용한다고 가정하고, 해당 로직이 의존성을 사용한다고 했을 때. 해당 의존성이 의존성이 escaping 클로저내에서 올바른 값으로 반영될 수 있도록, withEscapedDependencies(_:) 를 사용해야 합니다.
    withEscapedDependencies { dependencies in
      DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
        dependencies.yield {
          // All code in here will use dependencies at the time of calling withEscapedDependencies.
        }
      }
    }

 

출처: https://pointfreeco.github.io/swift-dependencies/main/documentation/dependencies/lifetimes/
반응형

'Programming > SwiftUI' 카테고리의 다른 글

[SwiftUI] SwiftUI와 UIKit와의 호환  (0) 2023.09.11