일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- 1일1알골
- SwiftUI
- uikit
- 리액트
- Swift
- 알고리즘
- Protocol
- 스위프트
- 위젯킷
- TCA
- 스유
- 멋쟁이사자처럼
- 영남대
- widgetkit
- 네트워크
- 문법
- 웹
- swift concurrency
- 대외활동
- composable architecture
- spritekit
- cs
- 멋사
- 컴퓨터그래픽스
- widget
- 운영체제
- c++
- 후기
- dispatchqueue
- 백준
- Today
- Total
맛동산이
SwiftUI) TCA에서의 의존성 주입, dependency 본문
Dependency란
dependency란 네트워크 통신, 파일 엑세스, 타이머, 스레드 등 타입이나, 함수로 이루어진 컨트롤할수 없는, 시스템 외부에서 일어나는 상호작용을 의미한다.
이러한 디펜던시를 제대로 컨트롤하지 못하면 예상치 못한 사이드이펙트가 발생할수 있다.
TCA는 https://github.com/pointfreeco/swift-dependencies 라는 라이브러리를 사용하고 있으며, 해당 라이브러리의 장점은 다음과 같다.
- 읽기쉽고, 테스트 코드짜기 쉬움
- swiftui 프리뷰와 동작을 잘하며, simulator를 통해서도 동작을 잘함
- 3rd party 라이브러리, (파이어베이스 같은) 것을 연동할때 컴파일 타임이 적게 든다.
TCA 에서 사용할수 있는 dependency Value값은 다음 페이지에서 확인할수 있다.
https://pointfreeco.github.io/swift-dependencies/1.0.0/documentation/dependencies/dependencyvalues/
사용하기
1. dependency 설정하기
struct State {
var time: Date?
var uuid: String
}
enum Action: Equatable {
case tappedUUIDButton
case setNewUUID
}
@Dependency(\\.date.now) var now
@Dependency(\\.uuid) var uuid
@Dependency(\\.continuousClock) var clock
func reduce() {
switch action {
case .tappedUUIDButton:
//디펜던시로 uuid를 만듬
state.uuid = uuid().uuidString
return .run {
try await clock.sleep(for: .second(1))
await send(.setNewUUID)
}
case .setNewUUID:
state.uuid = uuid().uuidString
return .none
}
}
위의 예시 코드를 통해서 어떻게 사용하는지 작성이 되어있다.
- 먼저 디펜던시를 정의
- 사용하는곳에서 dependency의 메소드 사용
이면 끝이다.
위의 내용을 정의하자면, uuid버튼을 누르면 즉시 uuidString이 생성되고
1초 후에 다시 uuidString이 생성되는 코드이다.
Overriding dependency
API정의를 내부에서 할수도 있지만, 이를 Dependency를 통해서 만든다면, 위에서 언급했던 것처럼 네트워크 환경의 사이드이펙트를 받지 않고 만들수 있기 때문에 내부적으로 만드는것이 아닌 외부에서 주입받는 방식으로 처리할수 있다.
struct APIManager {
var search: @Sendable(string) async throws -> ResultModel
}
extension DependencyValue {
var apiManager: APIManager {
get { self(APIManager.self) }
set { self(APIManager.self) = newValue }
}
extension APIManager: DependencyKey {
static let liveValue = APIManager(
search: { stringValue in
여기에서 api 호출 메소드 구현
}
)
}
DependencyValue???
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는 실제로 사용하는값.
해당값은 무조건 존재해야하며 이후 previewValue와 testValue와 같이 해당 값을 이용해서 네트워크에 대한 사이드 이펙트 없이 테스터블한 코드를 짤수 있다.
디팬던시 value 정의하기
정의된 디팬던시의 타입에 extension을 통해서 해당 디팬던시 키 프로퍼티를 정의해준다.
extension Store: DependencyKey {
static let liveValue = Self {
let (data, _) = try await URLSession.shared.data(from: URL(string: "<https://api.escuelajs.co/api/v1/categories>")!)
return try JSONDecoder().decode(Categories.self, from: data)
}
static var previewValue = CategoryClient {
let categories = [
Category(id: 1, name: "category 1", image: "", creationAt: "", updatedAt: ""),
Category(id: 2, name: "category 2", image: "", creationAt: "", updatedAt: ""),
Category(id: 3, name: "category 3", image: "", creationAt: "", updatedAt: ""),
Category(id: 4, name: "category 4", image: "", creationAt: "", updatedAt: ""),
]
return categories
}
}
liveValue는 실제로 우리가 실제로 사용할 리모트 api,
previewValue에는 해당 모델이 가질 데이터를 넣어주면 프리뷰에서 디팬던시를 넣어줬을때 해당 로딩을 통해서 주어진 값을 보여주게 된다.(mock 데이터)
사용부
이제 디팬던시를 정의 했기 때문에 사용하는 부분에서는 다음과 같다. 먼저 뷰모델에서 액션이 들어오고 해당 액션에서 api콜링을 하게 된다.
struct AllCategoryListFeature: Reducer {
struct State: Equatable {
}
enum Action: Equatable {
}
@Dependency(\\.정의된 디팬던시) var dependency// <- here
// 생략
}
먼저 디팬던시를 정의해주고
case .action:
return .run { send in
try await send(.categoriesResponse(client.fetchAllCategory()))
}
액션에서는 .run을 통해서 비동기적으로 처리하는 데이터를 받아서 사이드이팩트를 발생시킨다.
이렇게 만들어진 사이드이팩트의 값을 받아서
다시 액션으로 send를 통해서 ui가 변화할수 있도록 state값을 바꿔준다.
이게 가능한 이유는 send가 내부적으로
let send: @MainActor @Sendable (Action) -> Void
public init(send: @escaping @MainActor @Sendable (Action) -> Void) {
self.send = send
}
이렇게 MainActor클로저를 실행시키 때문이다.
TCA에서 사용한 예시
TCA 공식예시에서는 위의 나처럼 Result타입을 그냥 뿌린게 아니라.
받는 쪽에서 Result타입으로 받았다.
예를들어
enum Action {
case searchResponse(Result<GeocodingSearch, Error>)
}
이렇게 받기 때문에 실제로 Reducer 내부에서는
case .searchResponse(.failure):
state.results = []
return .none
case let .searchResponse(.success(response)):
state.results = response.results
return .none
이렇게 두개의 타입으로 받아서 처리해준다.
내용이 조금 두서없는것 같은데 정의하자면,
dependency는 sideEffect를 발생시킬수 있는 것을 의미하며, 다양한 api. 스케줄러, 타이머 등을 정의해서 사용할수 있다. 사용하는 방법은
- 디팬던시 키패스 정의
- 디팬던시 키를 채택하는 클래스 정의, 프로토콜 생성
- liveValue, 와 previewValue등 api 정의
- 사용 하는곳에서 .run을 통해서 비동기 사이드이팩트 발생
이 되겠다.
참고자료