The Composable Architecture(TCA)๋ ์ผ๊ด๋๊ณ ์ดํดํ ์ ์๋ ๋ฐฉ์์ผ๋ก ์ดํ๋ฆฌ์ผ์ด์ ์ ๋ง๋ค๊ธฐ ์ํด ํ์ํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ๋๋ค. ํฉ์ฑ(Composition), ํ ์คํ (Testing) ๊ทธ๋ฆฌ๊ณ ์ธ์ฒด ๊ณตํ(Ergonomics)์ ์ผ๋์ ๋ TCA๋ SwiftUI, UIKit์ ์ง์ํ๋ฉฐ ๋ชจ๋ ์ ํ ํ๋ซํผ(iOS, macOS, tvOS, watchOS)์์ ์ฌ์ฉ ๊ฐ๋ฅํฉ๋๋ค.
์ด ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ ๋ค์ํ ๋ชฉ์ ๊ณผ ๋ณต์ก๋๋ฅผ ๊ฐ์ง ์ดํ๋ฆฌ์ผ์ด์ ์ ๋ง๋ค๊ธฐ ์ํด ํ์ํ ํต์ฌ ๋๊ตฌ๋ฅผ ์ ๊ณตํฉ๋๋ค. ๊ฐ ๋๊ตฌ๊ฐ ์ ๊ณตํ๋ ํฅ๋ฏธ๋ก์ด ์คํ ๋ฆฌ๋ ์ฐ๋ฆฌ๊ฐ ๋งค์ผ ๋ง๋๋ ์๋ง์ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ ๋ฐฉ๋ฒ์ ์๋ ค์ค ๊ฒ๋๋ค.
-
์ํ(State) ๊ด๋ฆฌ
๊ฐ๋จํ ๊ฐ ํ์ ๋ค๋ก ์ดํ๋ฆฌ์ผ์ด์ ์ ์ํ๋ฅผ ๊ด๋ฆฌํ๋ ๋ฐฉ๋ฒ, ์ํ๋ฅผ ๊ณต์ ๋ฅผ ํตํด ํ๋ฉด์์ ์ผ์ด๋๋ ๋ณํ(Mutation)๋ฅผ ๋ค๋ฅธ ํ๋ฉด์์ ์ฆ์ ๊ด์ธก(Observe)ํ๋ ๋ฐฉ๋ฒ์ ์ ๊ณตํฉ๋๋ค. -
ํฉ์ฑ(Composition)
๊ธฐ๋ฅ์ ์ฌ๋ฌ ๊ฐ์ ๋ ๋ฆฝ๋ ๋ชจ๋๋ก ์ถ์ถํ๋ ๋ฐฉ๋ฒ, ์ด ๋ชจ๋์ ๋ค์ ํฉ์ณ์ ๊ฑฐ๋ํ ๊ธฐ๋ฅ์ ์์ ์ปดํฌ๋ํธ์ ์งํฉ์ผ๋ก ๊ตฌ์ฑํ๋ ๋ฐฉ๋ฒ์ ์ ๊ณตํฉ๋๋ค. -
์ฌ์ด๋ ์ดํํธ(Side Effects)
์ดํ๋ฆฌ์ผ์ด์ ๋ฐ๊นฅ์ธ์๊ณผ ์ ์ดํ๋ ์์ ์ ํ ์คํธํ ์ ์๊ณ ์ดํดํ๊ธฐ ์ฝ๊ฒ ์์ฑํ๋ ๋ฐฉ๋ฒ์ ์ ๊ณตํฉ๋๋ค. -
ํ ์คํ (Testing)
์ํคํ ์ฒ ๋ด๋ถ์ ๊ธฐ๋ฅ์ ํ ์คํธํ๋ ๋ฐฉ๋ฒ๋ฟ๋ง ์๋๋ผ ์ฌ๋ฌ ํํธ๋ก ๊ตฌ์ฑ๋ ๊ธฐ๋ฅ์ ํตํฉ ํ ์คํธ๋ฅผ ์์ฑํ๋ ๋ฐฉ๋ฒ, ์ฌ์ด๋ ์ดํํธ๊ฐ ์ดํ๋ฆฌ์ผ์ด์ ์ ๋ผ์น๋ ์ํฅ์ ๋ํด ์ ์ฒด ํ ์คํธ๋ฅผ ์์ฑํ๋ ๋ฐฉ๋ฒ์ ์ ๊ณตํฉ๋๋ค. ์ด ํ ์คํธ ๋ฐฉ์์ ์ฌ๋ฌ๋ถ์ ๋น์ฆ๋์ค ๋ก์ง์ด ์์๋๋ก ์ ์๋ํ๋์ง์ ๋ํ ๊ฐํ ๋ณด์ฆ๋ ์ ๊ณตํฉ๋๋ค. -
์ธ์ฒด ๊ณตํ(Ergonomics)
์์ ๋ด์ฉ์ ๊ฐ๋ฅํ ํ ์ ์ ๊ฐ๋ ์ ๊ฐ๋จํ API๋ก ์ด๋ฃจ๋ ๋ฐฉ๋ฒ์ ์ ๊ณตํฉ๋๋ค.
The Composable Architecture๋ Point-Free์ Brandon Williams์ Stephen Celis๊ฐ ๋ง๋ Swift์ ํจ์ํ ํ๋ก๊ทธ๋๋ฐ์ ๋ํด ์์๋ณด๋ ์ํผ์๋๋ฅผ ํตํด ํ์ํ์ต๋๋ค.
์ ์ฒด ์ํผ์๋๋ ์ฌ๊ธฐ์์ ํ์ธํ์ค ์ ์์ต๋๋ค. ์ด์ธ์๋, ์ด ๋ค ํํธ๋ก ๊ตฌ์ฑ๋ ์์๋ ์์ต๋๋ค. (part 1, part 2, part 3 ๊ทธ๋ฆฌ๊ณ part 4)
์ด ์ ์ฅ์์ ๊ธฐ๋ณธ์ ์ธ ๋ฌธ์ ๋ถํฐ ๋ณต์กํ ๋ฌธ์ ๊น์ง TCA๋ฅผ ํตํด ํด๊ฒฐํ ์ ์๋ค๋ ๊ฒ์ ์ฆ๋ช ํ๊ธฐ ์ํ ๋ง์ ์์ ๊ฐ ์์ต๋๋ค. ์ฌ๊ธฐ์ ํ์ธํ์ค ์ ์์ผ๋ฉฐ, ๋ด์ฉ์ ์๋์ ๊ฐ์ต๋๋ค.
- Case Studies
- Getting started
- Effects
- Navigation
- Higher-order reducers
- Reusable components
- Location manager
- Motion manager
- Search
- Speech Recognition
- Tic-Tac-Toe
- Todos
- Voice memos
TCA๋ฅผ ํตํด ๊ธฐ๋ฅ์ ๋ง๋ค๊ธฐ ์ํด์ ์ฌ๋ฌ๋ถ์ ๋๋ฉ์ธ์ ๊ตฌ์ฑํ๋ ๋ช ๊ฐ์ง ํ์ ์ ์ ์ํด์ผ ํฉ๋๋ค.
- ์ํ(State): ๋น์ฆ๋์ค ๋ก์ง์ ์ํํ๊ฑฐ๋ UI๋ฅผ ๊ทธ๋ฆด ๋ ํ์ํ ๋ฐ์ดํฐ์ ๋ํ ์ค๋ช ์ ๋ํ๋ด๋ ํ์ ์ ๋๋ค.
- ํ๋(Action): ์ฌ์ฉ์๊ฐ ํ๋ ํ๋์ด๋ ๋ ธํฐํผ์ผ์ด์ ๋ฑ ์ดํ๋ฆฌ์ผ์ด์ ์์ ์๊ธธ ์ ์๋ ๋ชจ๋ ํ๋์ ๋ํ๋ด๋ ํ์ ์ ๋๋ค.
- ํ๊ฒฝ(Environment): API ํด๋ผ์ด์ธํธ๋ ์ ๋๋ฆฌํฑ์ค ํด๋ผ์ด์ธํธ์ ๊ฐ์ด ์ดํ๋ฆฌ์ผ์ด์ ์ด ํ์๋ก ํ๋ ์์กด์ฑ(Dependency)์ ๊ฐ์ง๊ณ ์๋ ํ์ ์ ๋๋ค.
- ๋ฆฌ๋์(Reducer): ์ด๋ค ํ๋(Action)์ด ์ฃผ์ด์ก์ ๋ ์ง๊ธ ์ํ(State)๋ฅผ ๋ค์ ์ํ๋ก ๋ณํ์ํค๋ ๋ฐฉ๋ฒ์ ๊ฐ์ง๊ณ ์๋ ํจ์์
๋๋ค. ๋ํ ๋ฆฌ๋์๋ ์คํํ ์ ์๋ ์ดํํธ(Effect, ์์: API ๋ฆฌํ์คํธ)๋ฅผ ๋ฐํํด์ผ ํ๋ฉฐ, ๋ณดํต์
Effect
๊ฐ์ ๋ฐํํฉ๋๋ค. - ์คํ ์ด(Store): ์ค์ ๋ก ๊ธฐ๋ฅ์ด ์๋ํ๋ ๊ณต๊ฐ์ ๋๋ค. ์ฐ๋ฆฌ๋ ์ฌ์ฉ์ ํ๋(Action)์ ๋ณด๋ด์ ์คํ ์ด(Store)๋ ๋ฆฌ๋์(Reducer)์ ์ดํํธ(Effect)๋ฅผ ์คํํ ์ ์๊ณ , ์คํ ์ด(Store)์์ ์ผ์ด๋๋ ์ํ(State) ๋ณํ๋ฅผ ๊ด์ธก(observe)ํด์ UI๋ฅผ ์ ๋ฐ์ดํธํ ์๋ ์์ต๋๋ค.
์์ ํ์ ์ ์ ์ํ๋ ๊ฒ์ ์ด์ ์ ์ฆ์ ์ฌ๋ฌ๋ถ์ ๊ธฐ๋ฅ์ ํ ์คํธ ๊ฐ๋ฅ์ฑ์ ๋ถ์ฌํ ์ ์๋ค๋ ๊ฒ์ด๊ณ , ๊ฒ๋ค๊ฐ ํฌ๊ณ ๋ณต์กํ ๊ธฐ๋ฅ์ ์๋ก ๊ฒฐํฉ ๊ฐ๋ฅํ ์๊ณ ๋ ๋ฆฝ๋ ๋ชจ๋๋ก ์ชผ๊ฐค ์๋ ์์ต๋๋ค.
๊ฐ๋จํ ์์๋ก ์ค๋ช ๋๋ฆฌ๊ฒ ์ต๋๋ค. ํ๋ฉด์ ์ซ์์ ์ด ์ซ์๋ฅผ ์ฆ๊ฐํ ์ ์๋ + ๋ฒํผ, ๊ฐ์ํ ์ ์๋ - ๋ฒํผ์ด ์๋ค๊ณ ํด๋ณด๊ฒ ์ต๋๋ค. ๋ ๋ค์ํ ํ๋์ ์ํด ํญ ํ๋ฉด API ํธ์ถ์ ํด์ ์ซ์์ ๊ดํ ๋ฌด์์ ์ฌ์ค์ ์๋ฆผ์ฐฝ์ผ๋ก ๋ณด์ฌ์ฃผ๋ ๋ฒํผ๋ ์ถ๊ฐํฉ๋๋ค.
๊ทธ๋ฌ๋ฉด ํ๋ฉด์ ์ํ(State)๋ ๋ฌด์์ด ์์๊น์?
๋จผ์ ํ๋ฉด์ ์ซ์๋ฅผ ์ ์๋ก ๊ฐ์ง๊ณ ์์ ๊ฒ์ด๊ณ , ์๋ฆผ์ฐฝ์ ๋ณด์ฌ์ค ๋ ํ์ํ ์ซ์์ ๊ดํ ์ฌ์ค๋ ์์ ๊ฒ์
๋๋ค. (์๋ฆผ์ฐฝ์ด ๋ฐ ํ์๊ฐ ์๋ ์ํฉ์์ nil
๊ฐ์ ๋ฃ์ด์ผ ํ๋ ์ต์
๋๋ก ๊ฐ๊ฒ ์ต๋๋ค.)
struct AppState: Equatable {
var count = 0
var numberFactAlert: String?
}
ํ๋(Action)์๋ ๋ฌด์์ด ์์๊น์? ์ฆ๊ฐ ๋ฒํผ์ด๋ ๊ฐ์ ๋ฒํผ์ ๋๋ฅด๋ ํ๋์ ๋๊ตฌ๋ ์๊ฐํ ์ ์์ ๋งํผ ๋ช ํํ ํ๋๋ ์๊ณ , ๋ฐ๋๋ก ์๋ฆผ์ฐฝ์ ๋ซ๊ฑฐ๋ ๋ฌด์์ ์ฌ์ค API ๋ฆฌํ์คํธ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ์์ ๋ ๋ฐ์ํ๋ ํ๋๊ฐ์ด ์ฝ๊ฐ์ ์๊ฐํ๊ธฐ ์ด๋ ค์ด ํ๋๋ ์์ ๊ฒ์ ๋๋ค.
enum AppAction: Equatable {
case factAlertDismissed
case decrementButtonTapped
case incrementButtonTapped
case numberFactButtonTapped
case numberFactResponse(Result<String, ApiError>)
}
struct ApiError: Error, Equatable {}
๋ค์์ ํ๋ฉด์ด ์ ๋๋ก ์๋ํ๊ธฐ ์ํด ํ์ํ ์์กด์ฑ(Dependency)์ ๊ด๋ฆฌํ๋ ํ๊ฒฝ(Environment) ์ฐจ๋ก์
๋๋ค.
์ซ์์ ๊ดํ ์ฌ์ค์ ๊ฐ์ ธ์ค๋ ๊ฒฝ์ฐ ๋คํธ์ํฌ ๋ฆฌํ์คํธ๋ฅผ ์์ฝํด์ Effect
๊ฐ์ผ๋ก ๋ง๋๋ ์์
์ด ์๊ฒ ๋ค์. ์ด ์์
์ ์์กด์ฑ์ Int
๋ฅผ ๋ฐ์์ Effect<String, ApiError>
๋ฅผ ๋ฐํํ๋ ํจ์๊ฐ ๋๊ฒ ์ต๋๋ค. ์ฌ๊ธฐ์ String์ ๋ฆฌํ์คํธ์ ๋ฆฌ์คํฐ์ค๋ฅผ ์์ฝํ ๊ฐ์
๋๋ค.
์ดํํธ๋ ํต์์ ์ผ๋ก ๋ฐฑ๊ทธ๋ผ์ด๋ ์ค๋ ๋์์ ์์
์ ์ฒ๋ฆฌํ๊ฒ ๋ ๊ฒ์
๋๋ค(URLSession
์ด ํ๋ ๊ฒ์ฒ๋ผ์). ์ ํฌ๋ ์ดํํธ์ ๊ฐ์ ๋ฉ์ธ ํ์์ ๋ฐ์ ๋ฐฉ๋ฒ์ด ํ์ํฉ๋๋ค. ๋ฉ์ธ ํ ์ค์ผ์ค๋ฌ๋ฅผ ์ฌ์ฉํด์ผ ํ
์คํธ๋ฅผ ์์ฑํ ์ ์์ต๋๋ค. AnyScheduler
๋ฅผ ์ฌ์ฉํด์ ํ๋ก๋์
์์ DispatchQueue
๋ฅผ ์ฌ์ฉํ๊ณ ํ
์คํธ ์์ ํ
์คํธ ์ค์ผ์ค๋ฌ๋ฅผ ์ฌ์ฉํด๋ด
์๋ค.
struct AppEnvironment {
var mainQueue: AnySchedulerOf<DispatchQueue>
var numberFact: (Int) -> Effect<String, ApiError>
}
์ด์ ๋ฆฌ๋์(Reducer)๋ฅผ ๊ตฌํํด๋ด
์๋ค. ๊ทธ๋ฌ๋ ค๋ฉด ํ์ฌ ์ํ(State)๋ฅผ ๋ณํ์์ผ์ ๋ค์ ์ํ๋ก ๋ง๋๋ ๋ฐฉ๋ฒ์ ๋ํ ์ค๋ช
๊ณผ ์ด๋ค ์ดํํธ(Effect)๊ฐ ์คํ๋ผ์ผํ๋์ง์ ๋ํ ์ค๋ช
์ด ํ์ํฉ๋๋ค. ๋ง์ฝ ์ด๋ ํ ์ดํํธ๋ ์คํ์ด ํ์ํ์ง ์์ ๊ฒฝ์ฐ์ .none
์ ๋ฐํํ๋ฉด ๋ฉ๋๋ค.
let appReducer = Reducer<AppState, AppAction, AppEnvironment> { state, action, environment in
switch action {
case .factAlertDismissed:
state.numberFactAlert = nil
return .none
case .decrementButtonTapped:
state.count -= 1
return .none
case .incrementButtonTapped:
state.count += 1
return .none
case .numberFactButtonTapped:
return environment.numberFact(state.count)
.receive(on: environment.mainQueue)
.catchToEffect()
.map(AppAction.numberFactResponse)
case let .numberFactResponse(.success(fact)):
state.numberFactAlert = fact
return .none
case .numberFactResponse(.failure):
state.numberFactAlert = "Could not load a number fact :("
return .none
}
}
๋ง์ง๋ง์ผ๋ก ์ด ๊ธฐ๋ฅ์ด ์๋๋ ๋ทฐ๋ฅผ ์ ์ํฉ๋๋ค. Store<AppState, AppAction>
๊ฐ ์์ผ๋ฉด ๋ชจ๋ ์ํ ๋ณํ๋ฅผ ๊ด์ธกํ๊ณ UI๋ฅผ ๋ค์ ๊ทธ๋ฆด ์ ์์ผ๋ฉฐ, ์ฌ์ฉ์ ํ๋์ ๋ณด๋ด์ ์ํ๋ฅผ ๋ณํํ ์๋ ์์ต๋๋ค. .alert
View Modifier๊ฐ ์๊ตฌํ๋ ๋๋ก ์ซ์์ ๊ดํ ์ฌ์ค์ ๊ตฌ์กฐ์ฒด๋ก ํ ๋ฒ ๊ฐ์ธ์ Identifiable
์ ๋ฐ๋ฅด๊ฒ ๋ง๋ค๊ฒ ์ต๋๋ค.
struct AppView: View {
let store: Store<AppState, AppAction>
var body: some View {
WithViewStore(self.store) { viewStore in
VStack {
HStack {
Button("โ") { viewStore.send(.decrementButtonTapped) }
Text("\(viewStore.count)")
Button("+") { viewStore.send(.incrementButtonTapped) }
}
Button("Number fact") { viewStore.send(.numberFactButtonTapped) }
}
.alert(
item: viewStore.binding(
get: { $0.numberFactAlert.map(FactAlert.init(title:)) },
send: .factAlertDismissed
),
content: { Alert(title: Text($0.title)) }
)
}
}
}
struct FactAlert: Identifiable {
var title: String
var id: String { self.title }
}
ํ ๊ฐ์ง ์ค์ํ ์ฌ์ค์ ์ด ๋ชจ๋ ๊ธฐ๋ฅ์ ์ค์ ์ดํํธ ์์ด ๊ตฌํํ ์ ์๋ค๋ ๊ฒ์ ๋๋ค. ์ด๋ ๊ธฐ๋ฅ ์์ฒด๋ฅผ ๋ ๋ฆฝ๋ ํ๊ฒฝ์์ ๋ํ๋์ ์์ด ๋ง๋ค ์ ์๋ค๋ ๊ฒ์ ์ฆ๋ช ํ๋ ๊ฒ์ด๋ฉฐ ์ปดํ์ผ ์๊ฐ ๋จ์ถ์ผ๋ก ์ง๊ฒฐ๋๊ธฐ๋ ํฉ๋๋ค.
์ด ๋ง์ธ์ฆ์จ, ๋์ผํ ์คํ ์ด์ UIKit์ ๋ถ์ด๋ ๊ฒ๋ ๊ฐ๋ฅํ๋ค๋ ์๋ฏธ์
๋๋ค. UI ์
๋ฐ์ดํธ๋ ์๋ฆผ์ฐฝ์ ๋ณด์ฌ์ฃผ๋ ์์
์ ์ํด viewDidLoad
์์ ์คํ ์ด๋ก ๊ตฌ๋
(Subscribe)ํ๋ฉด ๋ฉ๋๋ค. ์ฝ๋ ์์ฒด๋ SwiftUI ๋ฒ์ ๋ณด๋ค ์กฐ๊ธ ๋ ๊น๋๋ค.
ํผ์ณ๋ณด๊ธฐ
class AppViewController: UIViewController {
let viewStore: ViewStore<AppState, AppAction>
var cancellables: Set<AnyCancellable> = []
init(store: Store<AppState, AppAction>) {
self.viewStore = ViewStore(store)
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
let countLabel = UILabel()
let incrementButton = UIButton()
let decrementButton = UIButton()
let factButton = UIButton()
// addSubview๋ constraint ์ค์ ํ๋ ์ฝ๋๋ ์๋ตํ์ต๋๋ค
self.viewStore.publisher
.map { "\($0.count)" }
.assign(to: \.text, on: countLabel)
.store(in: &self.cancellables)
self.viewStore.publisher.numberFactAlert
.sink { [weak self] numberFactAlert in
let alertController = UIAlertController(
title: numberFactAlert, message: nil, preferredStyle: .alert
)
alertController.addAction(
UIAlertAction(
title: "Ok",
style: .default,
handler: { _ in self?.viewStore.send(.factAlertDismissed) }
)
)
self?.present(alertController, animated: true, completion: nil)
}
.store(in: &self.cancellables)
}
@objc private func incrementButtonTapped() {
self.viewStore.send(.incrementButtonTapped)
}
@objc private func decrementButtonTapped() {
self.viewStore.send(.decrementButtonTapped)
}
@objc private func factButtonTapped() {
self.viewStore.send(.numberFactButtonTapped)
}
}
์ด์ ๋ทฐ๋ ์ค๋น๋์์ผ๋ ์๋์ ์ํ ์คํ ์ด๋ฅผ ๋ง๋ค์ด๋ด ์๋ค. ์ฌ๊ธฐ์ ๋ํ๋์๋ฅผ ์ ๊ณตํ๋ฉด ๋ฉ๋๋ค. ๊ทธ๋ฆฌ๊ณ API ๋ฆฌํ์คํธ๋ฅผ ์๋ตํ๊ธฐ ์ํด ๋ฌธ์์ด์ mock ํด์ ๋ฐ๋ก ๋ฐํํ๋ ์ดํํธ๋ฅผ ์ฃผ์ ํฉ๋๋ค.
let appView = AppView(
store: Store(
initialState: AppState(),
reducer: appReducer,
environment: AppEnvironment(
mainQueue: DispatchQueue.main.eraseToAnyScheduler(),
numberFact: { number in Effect(value: "\(number) is a good number Brent") }
)
)
)
๋๋์ด ํ๋ฉด์ ๋ณด์ฌ์ฃผ๊ธฐ ์ํ ์์ ์ด ๋ชจ๋ ๋๋ฌ์ต๋๋ค. ์ด๋ ๊ฒ ์ฌ๋ฌ ๋จ๊ณ๋ฅผ ํตํด ๊ธฐ๋ฅ์ ๋ง๋๋ ๊ฒ์ ์์ํ๊ฒ SwiftUI๋ก ๋ง๋๋ ๊ฒ๋ณด๋จ ํ์คํ ๋ช ๋จ๊ณ ๋ ์๊ธด ํฉ๋๋ค. ํ์ง๋ง ๊ทธ๋งํผ ๋ ์ด์ ์ด ์์ต๋๋ค. ์ด๋ฌํ ๋จ๊ณ๋ ๋จ์ํ ๋ก์ง์ ๊ด์ธก ๊ฐ๋ฅํ ๊ฐ์ฒด๋ ๋ค์ํ UI ์ปดํฌ๋ํธ์ ํด๋ก์ ์ ํฉ๋ฟ๋ฆฌ๋ ๊ฒ๋ณด๋ค, ์ํ ๋ณ๊ฒฝ์ ์ ์ฉํ๋ ๊ฒ์ ์ผ๊ด๋ ํ๋๋ฅผ ๊ฐ์ง๋๋ก ํด์ค๋๋ค. ๋ํ ์ฌ์ด๋ ์ดํํธ๋ฅผ ๊ฐ๊ฒฐํ๊ฒ ํํํ๋ ๋ฐฉ๋ฒ๋ ์ ๊ณตํฉ๋๋ค. ๊ทธ๋ฆฌ๊ณ ์ถ๊ฐ์ ์ธ ์์ ์์ด ์ดํํธ๊ฐ ํฌํจ๋ ๋ก์ง์ ๋ฐ๋ก ํ ์คํธํ ์๋ ์์ต๋๋ค.
ํ
์คํธ๋ฅผ ํ๊ธฐ ์ํด์ TestStore
๋ฅผ ์์ฑํด์ผ ํฉ๋๋ค. TestStore
๋ ์์์ ๋ง๋ ์คํ ์ด์ ๊ฐ์ ๋ด์ฉ์ ๋ํ๋์๋ก ๋ง๋ค์ด๋ ๋์ง๋ง, ์ด๋ฒ์ ์กฐ๊ธ ๋ ํ
์คํธํ๊ธฐ ์ข์ ๋ํ๋์๋ฅผ ์ฃผ์
ํ๊ฒ ์ต๋๋ค. ํนํ, ๋ผ์ด๋ธ DispatchQueue.main
๋์ ํ
์คํธ ์ค์ผ์ค๋ฌ๋ฅผ ์ฌ์ฉํด์ ์ด๋ค ์์
์ด ์งํ๋๋ ๊ฒ์ ์ ์ดํ ์ ์์ด์ ํ๋ฅผ ๊ตณ์ด ๊ธฐ๋ค๋ฆด ํ์๊ฐ ์๊ฒ ๋ง๋ค๊ฒ ์ต๋๋ค.
let scheduler = DispatchQueue.testScheduler
let store = TestStore(
initialState: AppState(),
reducer: appReducer,
environment: AppEnvironment(
mainQueue: scheduler.eraseToAnyScheduler(),
numberFact: { number in Effect(value: "\(number) is a good number Brent") }
)
)
์์ฑ๋ TestStore
๋ ์ ์ฒด ๋จ๊ณ๋ณ ์ฌ์ฉ์ ํ๋ก์ฐ๋ฅผ ๋ฃ์ ์ ์๋๋ฐ์, ์ฌ๊ธฐ์ ์ ์ฒด ๋จ๊ณ๋ ์ํ์ ๋ณ๊ฒฝ์ด ์ฐ๋ฆฌ๊ฐ ์์ํ๋๋ก ์ ์๋ํ๋์ง ์ฆ๋ช
์ ํ๊ธฐ ์ํ ๋ชจ๋ ๋จ๊ณ๋ผ๊ณ ์๊ฐํ์๋ฉด ๋ฉ๋๋ค. ๊ฒ๋ค๊ฐ ์คํ ์ด์ ๋ฐ์ดํฐ๋ฅผ ๋ฐ๊พธ๋ ์ดํํธ๊ฐ ์คํ๋๋ ๋จ๊ณ์ ๊ทธ ์์
๊น์ง ์์ธํ ๋ฃ์ด์ค์ผ ํฉ๋๋ค.
์๋์ ํ ์คํธ๋ ์ฌ์ฉ์๊ฐ ์ฆ๊ฐ ๋ฒํผ์ ๋๋ฅด๊ณ ๊ฐ์ ๋ฒํผ์ ๋๋ฅธ ํ ์ซ์์ ๊ดํ ์ฌ์ค์ ์์ฒญํ ๋ค ์ดํํธ์ ๋ฆฌ์คํฐ์ค๊ฐ ์๋ฆผ์ฐฝ์ ๋์ฐ๊ฒ ๋ง๋ค๊ณ ๋ง์ง๋ง์ผ๋ก ์๋ฆผ์ฐฝ์ ๋ซ๋ ๊ฒ๊น์ง์ ๋ด์ฉ์ ๋ด๊ณ ์์ต๋๋ค.
store.assert(
// ์ฆ๊ฐ/๊ฐ์ ๋ฒํผ์ ๋๋ ์ ๊ฒฝ์ฐ ์นด์ดํธ๋ฅผ ๋ฐ๊พธ๋ ๊ฒ์ ๋ํ ํ
์คํธ
.send(.incrementButtonTapped) {
$0.count = 1
},
.send(.decrementButtonTapped) {
$0.count = 0
},
// ์ซ์์ ๊ดํ ์ฌ๋ฐ๋ ์ฌ์ค ๋ฒํผ์ ๋๋ฅด๊ณ ์ดํํธ์์ ๋ฆฌ์คํฐ์ค๋ฅผ ๋ฐ๋ ๊ฒ์ ๋ํ ํ
์คํธ
// reducer์์ `.receive(on:)`์ ์ฌ์ฉํ์ผ๋ ์ค์ผ์ค๋ฌ๋ฅผ `advance()`ํด์ค์ผ ํฉ๋๋ค.
.send(.numberFactButtonTapped),
.do { scheduler.advance() },
.receive(.numberFactResponse(.success("0 is a good number Brent"))) {
$0.numberFactAlert = "0 is a good number Brent"
},
// ์๋ฆผ์ฐฝ ๋ซ๊ธฐ
.send(.factAlertDismissed) {
$0.numberFactAlert = nil
}
)
์ฌ๊ธฐ๊น์ง The Composable Architecture์์ ๊ธฐ๋ฅ์ ๋ง๋ค๊ณ ํ ์คํธํ๋ ๊ฒ์ ๋ํ ๊ธฐ๋ณธ์ด์์ต๋๋ค. ์์ผ๋ก ํฉ์ฑ(Composition), ๋ชจ๋ํ(Modularity), ์ ์์ฑ(Adaptability), ๋ณต์กํ ์ดํํธ๋ฅผ ๋ค๋ฃจ๋ ๋ฒ๊ณผ ๊ฐ์ด ์ฌ๋ฌ๋ถ์ ๊ธฐ๋ค๋ฆฌ๋ ๊ฐ๋ ๋ค์ด ์ ๋ง ๋ง์ต๋๋ค. ์์ ๋๋ ํ ๋ฆฌ์์ ๋ ์์ธํ ์ฌ์ฉ๋ฒ์ด ์๊ฐ๋ผ ์์ผ๋ ์ดํด๋ณด์๊ธฐ ๋ฐ๋๋๋ค.
The Composable Architecture๋ ์ฌ๋ฌ ๋๋ฒ๊น ๋๊ตฌ๋ฅผ ๊ธฐ๋ณธ์ ์ผ๋ก ์ ๊ณตํฉ๋๋ค.
-
reducer.debug()
๋ ๋ฆฌ๋์๊ฐ ๋ฐ๋ ๋ชจ๋ ํ๋๊ณผ ์ํ ๋ณ๊ฒฝ์ ๋ํ ์ค๋ช ์ ๋๋ฒ๊ทธ ์ฐฝ์ ์ถ๋ ฅํฉ๋๋ค.received action: AppAction.todoCheckboxTapped( index: 0 ) โ AppState( โ todos: [ โ Todo( - isComplete: false, + isComplete: true, โ description: "Milk", โ id: 5834811A-83B4-4E5E-BCD3-8A38F6BDCA90 โ ), โ Todo( โ isComplete: false, โ description: "Eggs", โ id: AB3C7921-8262-4412-AA93-9DC5575C1107 โ ), โ Todo( โ isComplete: true, โ description: "Hand Soap", โ id: 06E94D88-D726-42EF-BA8B-7B4478179D19 โ ), โ ] โ )
-
reducer.signpost()
๋ ํ๋์ด ์คํ๋๋ ๋ฐ์ ๊ฑธ๋ฆฐ ์๊ฐ๊ณผ ์ธ์ ์คํ๋๋์ง ๋ฑ์ ๋ํ ์ ๋ณด๋ฅผ ์ป์ ์ ์๋๋ก Instrument์ ํ์๋ฅผ ์์ฑํด์ค๋๋ค.
Composable Architecture์ ๊ฐ์ฅ ์ค์ํ ์์น ์ค ํ๋๋ ์ฌ์ด๋ ์ดํํธ๋ ์ ๋ ์ง์ ์ ์ผ๋ก ์คํ๋์ง ์๊ณ , ๋์ Effect
ํ์
์ ๊ฐ์ผ ํ์ ๋ฆฌ๋์์์ ๋ฐํ๋๊ณ ๋์ค์ ์คํ ์ด์์ ์คํ๋๋ค๋ ๊ฒ์
๋๋ค. ์ด๋ ์ดํ๋ฆฌ์ผ์ด์
์ ๋ฐ์ดํฐ ํ๋ก์ฐ๋ฅผ ๊ฐ๊ฒฐํํ๋ ๋ฐ ์์ด์ ๊ฐ์ฅ ์ค์ํ ๋ด์ฉ์ผ๋ก ์ด ์์น์ ๋ฐ๋ผ์ผ ์ฌ์ฉ์์ ํ๋๊ณผ ์ดํํธ ์คํ ์ฌ์ด์ ์ฌ์ดํด์ ๋ํ ํ
์คํธ ๊ฐ๋ฅ์ฑ์ ๋ณด์ฅ๋ฐ์ ์ ์์ต๋๋ค.
ํ์ง๋ง ์ด ๋ง์ ์ฐ๋ฆฌ๊ฐ ๋งค์ผ ๋ง๋๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ SDK๊ฐ Composable Architecture ์คํ์ผ๋ก ๋ฐ๋์ด์ผ ํ๋ค๋ ๊ฒ์ ์๋ฏธํ๊ธฐ๋ ํฉ๋๋ค. ์ ํฌ๋ ์ด ๊ณ ํต์ ์กฐ๊ธ์ด๋๋ง ๋์ด๋๋ฆฌ๊ธฐ ์ํด ์ ํ์ ์ ๋ช ํ ํ๋ ์์ํฌ๋ฅผ Composable Architecture์์ ์ฌ์ฉํ์ ๋ ์ ์๋ํ๋๋ก ๋ํผ(wrapper) ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ ๊ณตํ๊ณ ์์ต๋๋ค. ์ง์ํ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ๋ชฉ๋ก์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
ComposableCoreLocation
:CLLocationManager
์ ๋ํผ๋ก, ๋ฆฌ๋์์์ ์ฌ์ฉํ๊ธฐ ์ฝ๊ณCLLocationManager
์ ๊ธฐ๋ฅ์ ๋ก์ง์์ ์ฌ์ฉํ๋ ๊ฒฝ์ฐ๋ฅผ ํ ์คํธํ๊ธฐ์๋ ์ฉ์ดํ๊ฒ ๋ง๋ค์์ต๋๋ค.ComposableCoreMotion
:CMMotionManager
์ ๋ํผ๋ก, ๋ฆฌ๋์์์ ์ฌ์ฉํ๊ธฐ ์ฝ๊ณCMMotionManager
์ ๊ธฐ๋ฅ์ ๋ก์ง์์ ์ฌ์ฉํ๋ ๊ฒฝ์ฐ๋ฅผ ํ ์คํธํ๊ธฐ์๋ ์ฉ์ดํ๊ฒ ๋ง๋ค์์ต๋๋ค.
์ ํฌ๊ฐ ์์ง ์์ ํ์ง ๋ชปํ ํ๋ ์์ํฌ๋ฅผ ์ํ ๋ํผ๋ฅผ ๋ง๋ค๊ณ ์ถ์ผ์๋ฉด ์ธ์ ๋ ์ด์๋ฅผ ๋ง๋ค์ด์ฃผ์ธ์! ๋์๊ฐ ๋ฐฉํฅ์ ๋ํด ํจ๊ป ํ ๋ก ํ๊ณ ์ถ์ต๋๋ค.
-
Composable Architecture๊ฐ Elm์ด๋ Redux๊ฐ์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ๋ค๋ฅธ ์ ์ ๋ฌด์์ธ๊ฐ์?
ํผ์ณ์ ๋ต๋ณ๋ณด๊ธฐ
the Composable Architecture(TCA)๋ the Elm Architecture(TEA)์ Redux๊ฐ ๋์คํํ ์์ด๋์ด์ ๊ธฐ๋ฐํ๊ณ ์์ง๋ง ์ ํ์ ํ๋ซํผ์์ Swift ์ธ์ด์ ๋ง๊ฒ ๋ง๋ค์ด์ก์ต๋๋ค.TCA์ ๋ช๋ช ๋ถ๋ถ์ ๋ค๋ฅธ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ค์ ๋นํด ์ข ๋ ๊ณ ์ง์ด ์๋ ํธ์ ๋๋ค. ์๋ฅผ ๋ค์ด, Redux๋ ์ฌ์ด๋ ์ดํํธ๋ฅผ ๋ฐ์ํ๋ ๊ฒ์ ๋ํ ๊ท์น์ด ์๋ ๋ฐ๋ฉด, TCA๋ ๋ชจ๋ ์ฌ์ด๋ ์ดํํธ๋ฅผ
Effect
ํ์ ์ผ๋ก ๋ชจ๋ธ๋งํ๊ณ ๋ฆฌ๋์๊ฐ ๋ฐํํด์ผํ๋ ๊ฒ์ด ํ์์ ๋๋ค.์ด๋ค ๋ถ๋ถ์์ TCA๋ ๋ค๋ฅธ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ค์ ๋นํด ๋์จํ ํธ์ด๊ธฐ๋ ํฉ๋๋ค. ์๋ฅผ ๋ค์ด, Elm์์
Cmd
ํ์ ์ผ๋ก ๋ง๋ค ์ ์๋ effect์ ์ข ๋ฅ๋ฅผ ์ปจํธ๋กคํ๋ ๋ฐ๋ฉด์ TCA๋Effect
๊ฐ Combine์Publisher
ํ๋กํ ์ฝ์ ๋ฐ๋ฅด๊ธฐ ๋๋ฌธ์ ์ด๋ค ์ข ๋ฅ์ ์ดํํธ๋ ๋ง๋ค ์ ์์ต๋๋ค.๋, TCA๋ Redux๋ Elm ๋ฑ ๋ค๋ฅธ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๊ฐ ์ ๊ฒฝ์ฐ์ง ์๋ ๋ถ๋ถ์ ๋์ ์ฐ์ ์์๋ฅผ ์ฃผ๊ธฐ๋ ํฉ๋๋ค. ์๋ฅผ ๋ค์ด, ๊ฑฐ๋ํ ๊ธฐ๋ฅ์ ์์ ๋จ์์ ์ชผ๊ฐ๊ณ ๋ค์ ๊ฒฐํฉํ ์ ์๊ฒ ๋ง๋ค์ด์ฃผ๋ ํฉ์ฑ(Composition)์ TCA์์ ์์ฃผ ์ค์ํ ์ธก๋ฉด ์ค ํ๋์ ๋๋ค. ํฉ์ฑ์ ๋ฆฌ๋์์
pullback
๊ณผcombine
์ฐ์ฐ์ ๋๋ถ์ ์์ฑํ ์ ์์๊ณ , ๊ฒฐ๊ณผ์ ์ผ๋ก ๋ณต์กํ ๊ธฐ๋ฅ์ ๋ชจ๋ํํด์ ๋ ๋ ๋ฆฝ๋ ์ฝ๋๋ก ๋ง๋ค๊ณ ๋ ๋์ ์ปดํ์ผ ์๊ฐ์ ์ ๊ณตํ ์ ์๊ฒ ๋์์ต๋๋ค. -
Store
๊ฐ thread-safe ํ ์ด์ ๋ ๋ฌด์์ธ๊ฐ์?
์send
๋ ํ์ ์์ด์ง ์๋์?
์send
๋ ๋ฉ์ธ ์ค๋ ๋์์ ์คํ๋์ง ์๋์?ํผ์ณ์ ๋ต๋ณ๋ณด๊ธฐ
action์ด `Store`๋ก ๋ณด๋ด์ง๋ฉด ๋ฆฌ๋์๋ ์ง๊ธ ์ํ์์ ์คํ๋๊ณ , ์ด ์์ ์์ฒด๋ ์ฌ๋ฌ ์ค๋ ๋์์ ์คํ๋ ์ ์์ต๋๋ค. `send`์ ๊ตฌํ๋ถ์์ ํ๋ฅผ ์ฌ์ฉํ๋ ๋ฐฉ๋ฒ๋ ์๊ฒ ์ง๋ง, ์ด๋ ์๋ก์ด ๋ฌธ์ ๋ฅผ ๋ง๋ญ๋๋ค.-
๊ฐํธํ๊ฒ
DispatchQueue.main.async
๋ฅผ ์ฌ์ฉํ๋ค๋ฉด ๋ฉ์ธ ์ค๋ ๋์์ ์ค๋ ๋๋ฅผ ๋ฐ์ด๋์ผ๋ ค๋ ์ผ์ด ์ผ์ด๋ ๊ฒ์ ๋๋ค. ๋๋ก๋ ์ ๋๋ฉ์ด์ ๋ธ๋ฝ์ฒ๋ผ ๋๊ธฐ์ ์ผ๋ก ์ผ์ด๋์ผ ํ๋ ์์ ์ด ์์ํ ๋ฐ, ์ด๋ด ๊ฒฝ์ฐ UIKit๊ณผ SwiftUI์ ์์์น ๋ชปํ ๋ฌธ์ ๊ฐ ๋ฐ์ํ ๊ฒ์ ๋๋ค. -
DispatchQueue.main.async
๋ฅผ ์ฌ์ฉํ๊ณ ์์ธ ์์ ์ ๋ฐ๋ก ์คํํ๋ ์ค์ผ์ค๋ฌ๋ฅผ ๋ง๋ค ์๋ ์์ ๊ฒ์ ๋๋ค. (์์: ReactiveSwift์UIScheduler
) ์ด๋ ์คํ๋ ค ์ํฉ์ ๋ ๋ณต์กํ๊ฒ ๋ง๋ค๊ธฐ ๋๋ฌธ์ ์์ฒญ ๊ด์ฐฎ์ ์ด์ ๊ฐ ์๋ค๋ฉด ์๋ง ์ฑํ๋์ง ์์ ๊ฒ์ ๋๋ค.
๊ฒฐ๊ตญ ์ ํฌ๋
Store
๊ฐ ์ ํ์ API์ฒ๋ผ ์ํธ์์ฉํ๋๋ก ๋ง๋ค์์ต๋๋ค.URLSession
์ด ๋ฐฑ๊ทธ๋ผ์ด๋ ์ค๋ ๋๋ก ๊ฒฐ๊ณผ๋ฅผ ์ ๋ฌํ๊ณ ๋ฉ์ธ ์ค๋ ๋๋ก ๋๊ธฐ๋ ์์ ์ ์ฐ๋ฆฌ์๊ฒ ๋งก๊ธฐ๋ ๊ฒ์ฒ๋ผ TCA๋ ๋ฉ์ธ ์ค๋ ๋์์ ํ๋์ ๋ณด๋ด๋ ๊ฒ์ ์ฌ์ฉ์์๊ฒ ๋งก๊น๋๋ค. ๋ง์ฝ ์ฌ๋ฌ๋ถ์ด ์ถ๋ ฅ์ ๋ฉ์ธ ์ค๋ ๋๊ฐ ์๋ ๊ณณ์ผ๋ก ์ ๋ฌํ๋ ์ดํํธ๋ฅผ ์ฌ์ฉํ์ ๋ค๋ฉด.receive(on:)
์ ์ด์ฉํด์ ๋ฉ์ธ ์ค๋ ๋๋ก ๋๊ธฐ๋๋ก ๋ง๋์ ์ผ ํฉ๋๋ค.์ด ์ ๊ทผ๋ฒ์ ์ดํํธ๊ฐ ์์ฑ๋๊ณ ๋ณํ๋๋ ๋ฐฉ๋ฒ์ ๋ํ ๊ฐ์ค์ ์๋ฅผ ์ต์ํํด์คฌ์ผ๋ฉฐ, ๋ถํ์ํ ์ค๋ ๋ ๋ฐ์ด๋๊ธฐ ๊ฐ์ ๋ฌธ์ ๋ฅผ ๋ง์์ฃผ์์ต๋๋ค. ์ดํํธ์ ์ค์ผ์ฅด๋ง์ ๋ํ ์ฑ ์์ด ์๋ค๋ฉด ์ดํํธ์ ๋ํ ํ ์คํธ๊ฐ ์ฆ์ ๋๊ธฐ์ ์ผ๋ก ์งํ๋ ๊ฒ์ ๋๋ค. ๊ทธ๋ ๊ฒ ๋๋ฉด ์คํ๋๊ณ ์๋ ์ดํํธ๊ฐ ์ด๋ป๊ฒ ๋ค์์ผ๋ก ๋์ด๊ฐ๋์ง ํน์ ์ดํ๋ฆฌ์ผ์ด์ ์ ์ํ์ ์ด๋ป๊ฒ ์ํฅ์ ๋ผ์น๋์ง์ ๋ํ ์ํฉ์ ์ ํ ์ ์ ์์ ๊ฒ์ ๋๋ค. ํ์ง๋ง ์ํ๋ค๋ฉด
Store
์์ ์ดํํธ์ ์ด๋ฌํ ์ธก๋ฉด์ ํ ์คํธํ๊ฑฐ๋ ๋ฌด์ํ ์ ์๋ ์ ์ฐ์ฑ์ ๋จ๊ฒจ๋์์ต๋๋ค.์ ํฌ๊ฐ ์ ํํ ๋ฐฉ์์ด ๋ง์์ ๋ค์ง ์์ผ์๋์? ๊ฑฑ์ ๋ง์ธ์! The Composable Architecture๋ ์ด ๋ถ๋ถ์ ์ฌ๋ฌ๋ถ์ด ์ํ๋๋๋ก ๋ฐ๊ฟ ์ ์๋๋ก ์ ์ฐํ๊ฒ ๋ง๋ค์ด์ก์ต๋๋ค. ์๋์ ๊ฐ์ด ๋ชจ๋ ์ดํํธ์ ๋ํด์ ๊ฒฐ๊ณผ๋ฅผ ๋ฉ์ธ ์ค๋ ๋์ ์ ๋ฌํ๊ฒ ๋ง๋๋ ๊ณ ๊ณ ๋ฆฌ๋์๋ฅผ ๋ง๋ค์ด์ ์ฃผ์ ํ๋ฉด ์ค๋ ๋์ ๋ํ ์ฑ ์์ ๊ฑฑ์ ํ์ง ์์ผ์ ๋ ๋ฉ๋๋ค.
extension Reducer { func receive<S: Scheduler>(on scheduler: S) -> Self { Self { state, action, environment in self(&state, action, environment) .receive(on: scheduler) .eraseToEffect() } } }
๊ทธ๋๋ ์ฌ์ ํ ๋ถํ์ํ ์ค๋ ๋ ๊ฑด๋๋ฐ๊ธฐ๊ฐ ์๊ธฐ์ง ์๋๋ก ํด์ฃผ๋
UIScheduler
๋ ๋์ฆ๊ฐ ์๊ฒ ๋ค์. -
The Composable Architecture๋ Combine ํ๋ ์์ํฌ๋ฅผ ๋ํ๋์๋ก ๊ฐ๊ณ ์๊ธฐ ๋๋ฌธ์ ์ต์ ๊ฐ๋ฐ ํ๊ฒ์ด iOS 13, macOS 10.15, Mac Catalyst 13, tvOS 13 ๊ทธ๋ฆฌ๊ณ watchOS 6 ์ด์์ด์ด์ผ ํฉ๋๋ค. ๋ง์ฝ ๋ ์ด์ OS๋ฅผ ์ง์ํด์ผ ํ๋ ๊ฒฝ์ฐ๋ ReactiveSwift ๋ฒ์ ์ด๋ RxSwift๋ฒ์ ์ ์ฌ์ฉํ์๋ฉด ๋ฉ๋๋ค!
Composable Architecture๋ Xcode ํ๋ก์ ํธ์ ํจํค์ง ๋ํ๋์๋ก ์ถ๊ฐํ ์ ์์ต๋๋ค.
- File ๋ฉ๋ด์์ Swift Packages โบ Add Package Dependency๋ฅผ ์ ํํ์ธ์.
- ์ ์ฅ์ URL ํ ์คํธ ํ๋์ "https://github.com/pointfreeco/swift-composable-architecture"๋ฅผ ์ ๋ ฅํ์ธ์.
- ์ฌ๋ฌ๋ถ์ ํ๋ก์ ํธ๊ฐ ์ด๋ป๊ฒ ๊ตฌ์ฑ๋ผ์๋๊ฐ์ ๋ฐ๋ผ ๋ค์๊ณผ ๊ฐ์ด ์์
ํ์๋ฉด ๋ฉ๋๋ค.
- ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ์ ๊ทผํด์ผ ํ๋ ์ดํ๋ฆฌ์ผ์ด์ ํ๊ฒ์ด ํ๋์ผ ๊ฒฝ์ฐ, ComposableArchitecture๋ฅผ ์ดํ๋ฆฌ์ผ์ด์ ์ ๋ฐ๋ก ์ถ๊ฐํ์๋ฉด ๋ฉ๋๋ค.
- ์ดํ๋ฆฌ์ผ์ด์ ํ๊ฒ์ด ์ฌ๋ฌ ๊ฐ์ผ ๊ฒฝ์ฐ, ๊ณต์ ํ๋ ์์ํฌ๋ฅผ ๋ง๋ค์ด์ ComposableArchitecture๋ฅผ ๋ํ๋์๋ก ๊ฐ์ง๊ฒ ๋ง๋ ํ ๊ทธ ๊ณต์ ํ๋ ์์ํฌ๋ฅผ ๊ฐ ํ๊ฒ์์ ๋ํ๋์๋ก ๊ฐ์ง๋ฉด ๋ฉ๋๋ค. ๋ฐ๋ชจ ์ดํ๋ฆฌ์ผ์ด์ ์ธ Tic-Tac-Toe์์ ๊ธฐ๋ฅ์ ์ฌ๋ฌ ๋ชจ๋๋ก ์ชผ๊ฐ ํ ์ ์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ธ TicTacToeCommon ํ๋ ์์ํฌ๋ฅผ ์ฌ์ฉํ๊ณ ์์ผ๋ ์ด ํ๋ก์ ํธ์์ ์ฌ์ฉ ๋ฐฉ๋ฒ์ ์์ธํ ํ์ธํ์ค ์ ์์ ๊ฒ๋๋ค.
Composable Architecture์ ๊ฐ์ฅ ์ต์ ๋ฒ์ ๊ฐ๋ฐ ๋ฌธ์๋ ์ฌ๊ธฐ์ ํ์ธํ์ค ์ ์์ต๋๋ค.
Composable Architecture์ ๋ํด ๊ถ๊ธํ ์ ์ด ์๊ฑฐ๋ ์ ํฌ์ ํ ๋ก ์ ํ๊ณ ์ถ์ผ์ ๊ฒฝ์ฐ์ discussions ํญ์์ ํ ํฝ์ ๋ง๋์๋ฉด ๋ฉ๋๋ค. ํน์ Swift ํฌ๋ผ์์๋ ๊ฐ๋ฅํฉ๋๋ค.
์๋๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ด๊ธฐ ๊ฐ๋ฐ ๋จ๊ณ๋ถํฐ ํผ๋๋ฐฑ์ ์ฃผ์๊ณ ์ง๊ธ์ Composable Architecture๊ฐ ์์ ์ ์๊ฒ ๋์์ฃผ์ ๊ณ ๋ง์ด ๋ถ๋ค์ ๋ชฉ๋ก์ ๋๋ค.
Paul Colton, Kaan Dedeoglu, Matt Diephouse, Josef Doleลพal, Eimantas, Matthew Johnson, George Kaimakas, Nikita Leonov, Christopher Liscio, Jeffrey Macko, Alejandro Martinez, Shai Mishali, Willis Plummer, Simon-Pierre Roy, Justin Price, Sven A. Schmidt, Kyle Sherman, Petr ล รญma, Jasdev Singh, Maxim Smirnov, Ryan Stone, Daniel Hollis Tavares, ๊ทธ๋ฆฌ๊ณ ๋ชจ๋ Point-Free ๊ตฌ๋ ์๋ถ๋ค๊น์ง ๋ชจ๋ ๊ฐ์ฌ๋๋ฆฝ๋๋ค. ๐
ํนํ, SwiftUI์ ๊ธฐ์ดํ ๋ฌธ์ ๋ค์ ํด๊ฒฐํ๊ณ ์ต์ข API๋ฅผ ๊ฐ์ ํ๋ ๋ฐ ๋์์ ์ค Chris Liscio๊ป๋ ํน๋ณํ ๊ฐ์ฌ๋ฅผ ๋๋ฆฌ๊ณ ์ถ์ต๋๋ค.
๊ทธ๋ฆฌ๊ณ Shai Mishali์ CombineCommunity ํ๋ก์ ํธ์ Publishers.Create
๊ฐ ์์๊ธฐ ๋๋ฌธ์ Effect
์์ ๋ธ๋ฆฌ๊ฒ์ดํธ์ ์ฝ๋ฐฑ ๊ธฐ๋ฐ API๋ฅผ ์ฐ๊ฒฐํ์ฌ ํ์ฌ ํ๋ ์์ํฌ์ ํต์ ํ ๋ ๋ ๋์ ์ธํฐํ์ด์ค๋ฅผ ๋ง๋ค ์ ์์์ต๋๋ค. ๊ฐ์ฌ๋๋ฆฝ๋๋ค.
Composable Architecture๋ Elm์ด๋ Redux๊ฐ์ ๋ค๋ฅธ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ค์ ์์ด๋์ด์ ๊ธฐ๋ฐํด์ ๋ง๋ค์ด์ก์ต๋๋ค.
iOS ์ปค๋ฎค๋ํฐ์๋ Composable Architecture ์ด์ธ์๋ ๋ค๋ฅธ ์ํคํ ์ฒ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๊ฐ ์์ต๋๋ค. ๊ฐ์ ๋ค๋ฅธ ํน์ง์ ๊ฐ์ง๊ณ ์์ผ๋ ์ดํด๋ณด์๋ ๊ฒ๋ ์ถ์ฒํฉ๋๋ค.
-
ํผ์ณ๋ณด๊ธฐ
์ด ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ MIT ๋ผ์ด์ผ์ค๋ฅผ ๋ฐ๋ฆ ๋๋ค. ์์ธํ ์ฌํญ์ LICENSE๋ฅผ ์ฐธ๊ณ ํด์ฃผ์ธ์.