일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
Tags
- 디자인패턴
- 부트캠프
- 네이버 부캠
- rxswift
- notion
- 부스트캠프
- 후기
- WWDC
- 커스텀 뷰
- Design Pattern
- Opensource
- 개발
- Algorithm
- 코코아 인터널스
- Hello
- Swift
- World
- OS
- SwiftUI
- IOS
- boostcamp
- development
- 알고리즘
- Cocoa Internals
- 단위 테스트
- Tistory
Archives
- Today
- Total
꿈돌이랜드
swift-dependencies: Dependency lifetimes 본문
반응형
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 }
- TaskLocal 변수는 withValue 메서드 Scope 내에서만 값을 변경 가능합니다.
- 결론적으로, 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 |
---|