Dependency?= 의존성
네트워크 통신, 유저디펄트, 키체인 등 의존성에 대한 관리 -> app 개발 전반에 영향을 미침을 우리는 알고 있다.
스유의 프리뷰를 사용해보면 알겠지만 프리뷰에도 의존성을 주입해주어야 프리뷰를 사용할 수 있다.
TCA Dependency는 개발에서 더 쉽게 관리할수 있도록 도와주는 라이브러리가 있다.
먼저 요즘껏들을 알기 위해서는 역사를 알야아한다.
ReducerProtocol 이전
Environment라는 구조체에서 의존성 관리를 하였다. 우리가 아는 흔한 하나의 구조체에 사용해야되는 기능들을 다 넣어놓고 의존해주는 형식.
문제점: 어떤 뷰에서는 사용하지 않고 어떤 하위뷰에서 사용하더라도 여기 안에 넣어놔야한다. ㅏㅗ 귀차나.
새로운 의존성을 추가할때도 선언부터 초기화까지 수정하고 상위에서 주입받을경우도 상위에서도 선언하고 추가해야한다. ㅏㅗ 기차나
그래서 TCA는 ReducerProtocol 도입과 함께 Dependency 라이브러리가 도입되었다.
@Dependency
public struct Dependency<Value>: @unchecked Sendable, _HasInitialValues {
// 앱내에 필요한 의존성이 저장되어있는 DependencyValues
let initialValues: DependencyValues
private let keyPath: KeyPath<DependencyValues, Value>
private let file: StaticString
private let fileID: StaticString
private let line: UInt
}
이렇게 프로퍼티 래퍼가 정의되어있다. 이중 DependencyValues에 저장된 특정 의존성에 키패스를 통해 접근할 수 있도록 정의되어 있다.
그러면 DepndencyValues가 몬데? 그 전에 DependencyKey 프로토콜을 알아야한다. 왜냐? DependencyValues에 특정 의존성을 추가 등록하기 위해
public protocol DependencyKey: TestDependencyKey {
/// 실제 앱 동작에 사용될 값
static var liveValue: Value { get }
associatedtype Value = Self
/// 프리뷰를 위한 값
static var previewValue: Value { get }
/// Test를 위해 사용될 mock 값
static var testValue: Value { get }
}
- liveValue는 앱이 실제 기기에서 동작하거나, 시뮬레이터 통해 동작할때 사용되는 dependency value. 반드시 리턴을 해야하는 defaultValue라고 한다.
- struct MyDependencyKey: DependencyKey { static let liveValue = "Default value" } 요렇게 사용
DependencyValue
: DependencyKey를 통해 의존성을 반환하며 관리하는 역할.
public struct DependencyValues: Sendable {
@TaskLocal public static var _current = Self()
#if DEBUG
@TaskLocal static var isSetting = false
#endif
// 현재 의존성
@TaskLocal static var currentDependency = CurrentDependency()
fileprivate var cachedValues = CachedValues()
// DepedencyKey를 통해, 앱에서 사용될 Dependency를 관리하는 storage
private var storage: [ObjectIdentifier: AnySendable] = [:]
public init() {
#if canImport(XCTest)
_ = setUpTestObservers
#endif
}
/* DependencyValue를 DependencyKey를 통해, 접근할 수 있도록 구현된 subscript */
public subscript<Key: TestDependencyKey>(
key: Key.Type,
file: StaticString = #file,
function: StaticString = #function,
line: UInt = #line
) -> Key.Value where Key.Value: Sendable {
/*
(1) 커스텀하게 의존성을 등록하고 사용하기위해, 앞서 배운 DependencyKey 정의
private struct MyDependencyKey: DependencyKey {
static let testValue = "Default value"
}
(2) 정의된 DependencyKey값을 통해 아래와 같이, computed-property를 통해 의존성 등록 및 접근
extension DependencyValues {
var myCustomValue: String {
get { self[MyDependencyKey.self] }
set { self[MyDependencyKey.self] = newValue }
}
*/
get {
guard let base = self.storage[ObjectIdentifier(key)]?.base,
let dependency = base as? Key.Value
else {
let context =
self.storage[ObjectIdentifier(DependencyContextKey.self)]?.base as? DependencyContext
?? defaultContext
switch context {
case .live, .preview:
return self.cachedValues.value(
for: Key.self,
context: context,
file: file,
function: function,
line: line
)
case .test:
var currentDependency = Self.currentDependency
currentDependency.name = function
return Self.$currentDependency.withValue(currentDependency) {
self.cachedValues.value(
for: Key.self,
context: context,
file: file,
function: function,
line: line
)
}
}
}
return dependency
}
set {
self.storage[ObjectIdentifier(key)] = AnySendable(newValue)
}
}
public static var live: Self {
var values = Self()
values.context = .live
return values
}
/// A collection of "preview" dependencies.
public static var preview: Self {
var values = Self()
values.context = .preview
return values
}
/// A collection of "test" dependencies.
public static var test: Self {
var values = Self()
values.context = .test
return values
}
func merging(_ other: Self) -> Self {
var values = self
values.storage.merge(other.storage, uniquingKeysWith: { $1 })
return values
}
}
흠 내부 코드다..길다 친절히 써있지만 한번더 정리하자면
currentDependency: 현재 DependencyKey를 통해 사용하고 있는 의존성
subscript: stoarge에 저장된 dependency 탐색 및 접근
stoarge: DependencyKey를 통해 의존성 저장 장소
그런데 보면 TaskLocal이 겁나게 많다 이게 뭔데?
- 앱의 모든 곳에 값을 전달하도록 만들어줌. (에 ? 그러면 전역변수인가?)
- 전역변수보다는 더 안전. How? 동시 컨텍스트 사용시 안정성 보장(여러작업이 경쟁 조건 X, 로컬에서 동일한 작업 액세스)하여 Race Condition방지한다.
- 특정 범위에서만 변경이 가능하다
- 기존 Task에서 생성된 Task에 의해 상속.
enum Locals {
@TaskLocal static var value = 1
}
print(Locals.value) // 1
Locals.$value.withValue(42) {
print(Locals.value) // 42
}
print(Locals.value) // 1
직접 수정하는 것이 아니라 withValue클로저 내에서만 값의 변경이 적용됨. get-only 프로퍼티임.
그 이유?안전하고 값을 추론 쉽도록
작업 로컬 상속: non escaping closure범위 밖에서도 변경된 값을 유지할 수 있도록 하고, 결과값도 예측이 쉽도록 하기 위해 swift concurrrency이용.
enum Locals {
@TaskLocal static var value = 1
}
print(Locals.value) // 1
Locals.$value.withValue(42) {
print(Locals.value) // 42
Task {
try await Task.sleep(for: .seconds(1))
print(Locals.value) // 42
}
print(Locals.value) // 42
}
보면 1초후에 task에서 액세스할때도 재정의된 상태로 유지되는 것을 볼 수 있다. 왜오애ㅗ애ㅙ? @TaskLocal이 Task에 상속되기 때문에!
여기서 LifeTime관련된 얘기도 나오는데 Task이외의 다른걸로 비동기처리하면 바로 풀림...ex)DispatchQueue
Dependency Overriding
- mock 데이터를 넣어 테스트 할때 우리는 의존성을 런타임 시점에 변경해서 사용하여야 한다. 예시코드는
func testRandomData() async {
let dummyData = Data(count: 0)
let store = TestStore(initialState: RealReducer.State()) {
RealReducer()
} withDependencies: {
$0.data.getRandomData = { dummyData }
}
await store.send(.getButtonTapped) {
$0.isRequestingData = true
}
await store.receive(.dataResponse(dummyData)) {
$0.data = dummyData
$0.isRequestingData = false
}
}
이런식으로 임의의 더미데이터를 활용하고 withDependencies를 이용하여 오버라이딩해서 테스트 할 수 이씀!
TCA로의 구현이 아닌 ObservableObject에서는 어캐 사용할까?
class AppModel: ObservableObject {
@Published var todos: TODOSModel?
func buttonTapped() {
self.todos = withDependencies(from: self) {
$0.apiClient = .mock}
operation: {
TODOSModel()
}}}
새로운 종속성이 하위모델로 전파되도록 의존성 재정의해야할때는 주의!!왜? 생성된 하위 모델이 withDependencies 호출 내에서 수행되어야 하위에서 상위 모델에서 사용하는 종속성 선택할 수 있다.
-> 종속성이 재정의 되었을때 하위까지 전파되려면 하위에서도 withDependencies를 이용하자
https://axiomatic-fuschia-666.notion.site/Chapter-5-Dependency-de90da4e19554625af3ffc005ab13ed9
'SWIFTUI' 카테고리의 다른 글
TCA- TestingCode (1) | 2024.10.26 |
---|---|
TCA-Dependency...DI,DIP를 곁들인 (2) | 2024.09.20 |
TCA(2)-Store, ViewStore& Binding (4) | 2024.07.16 |
TCA(1)- (mvvm...-> NEXT?) (0) | 2024.07.04 |
go back to basic - @main 플젝만들면 항상생기는 파일 이건 몰까? (0) | 2024.03.01 |