J
[SwiftUI] SwiftUI + TCA : Redux ํจํด ์ํฅ? ๋จ๋ฐฉํฅ? ๋ณธ๋ฌธ
๐ง ์์ํ๊ธฐ ์ ์…
TCA๋ฅผ ์์ํ๊ธฐ ์ ์ ์์ฃผ ์ธ๊ธ๋๋ ๊ฐ๋ ๋ค์ด ์์ต๋๋ค.
Redux ํจํด์ ์ํฅ์ ๋ฐ์๋ค, ๋จ๋ฐฉํฅ ์ํคํ ์ฒ๋ฅผ ๋ฐ๋ฅธ๋ค ๋ฑ์ธ๋ฐ, ์ฒ์ ์ ํ๋ฉด ์ด๋ฐ ์ฉ์ด๋ค์ด ์คํ๋ ค ์ดํด๋ฅผ ๋ฐฉํดํ ์๋ ์์ต๋๋ค.
๊ทธ๋์ ๋จผ์ ์ด ๊ฐ๋ ๋ค์ ๊ฐ๋จํ ์ ๋ฆฌํ ํ, ๋ณธ๊ฒฉ์ ์ผ๋ก TCA์ ๋ํด ์์๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค!
๐ TCA๊ฐ Redux ํจํด์์ ์ํฅ์ ๋ฐ์ ํต์ฌ ์์น
TCA๋ Redux ํจํด์์ ์ํฅ์ ๋ฐ์ "๋จ์ผ ์ํ(Single State)์ ์์ ํจ์ ๊ธฐ๋ฐ์ ์ก์ ์ฒ๋ฆฌ(Pure Function Action Handling)" ๋ฅผ ๋ฐ๋ฆ ๋๋ค.
์ง๊ธ๋ถํฐ ์ด๊ฒ ๋ฌด์จ ์๋ฏธ์ธ์ง ํ๋์ฉ ์ฝ๊ฒ ํ์ด์ ์ค๋ช ํด๋ณผ๊ฒ์.
1๏ธโฃ ๋จ์ผ ์ํ(Single State)๋?
๐ ์ฑ์ ๋ชจ๋ ์ํ(State)๋ฅผ ํ๋์ struct ์์์ ๊ด๋ฆฌํ๋ค.
์ด ๋ง์ ์ฑ ์ ์ฒด์ ๋ฐ์ดํฐ(์ํ)๊ฐ ๋จ ํ๋์ ๊ตฌ์กฐ์ฒด๋ก ํํ๋๋ค๋ ๊ฒ ์ ์๋ฏธํฉ๋๋ค.
์ฆ, "์ฌ๋ฌ ๊ฐ์ ๋ทฐ(View)๋ ์ปดํฌ๋ํธ๊ฐ ๊ฐ์ ์ํ๋ฅผ ๊ฐ์ง์ง ์๊ณ , ํ๋์ ์ค์ ์ํ์์ ๊ด๋ฆฌ๋๋ค" ๋ผ๋ ๊ฐ๋ ์ด์์.
๐น ๊ธฐ์กด ๋ฐฉ์ (SwiftUI @State)
๊ธฐ์กด SwiftUI์์๋ ๋ทฐ๋ง๋ค @State ๋ณ์๋ฅผ ์ ์ธํ์ฌ ์ํ๋ฅผ ๊ด๋ฆฌํฉ๋๋ค.
struct CounterView: View {
@State private var count: Int = 0
var body: some View {
VStack {
Text("\\(count)")
Button("์ฆ๊ฐ") { count += 1 }
}
}
}
- CounterView ๋ด๋ถ์์ count ์ํ๋ฅผ ๊ด๋ฆฌํจ.
- ํ์ง๋ง, ๋ง์ฝ ๋ค๋ฅธ ํ๋ฉด์์๋ count๋ฅผ ๊ณต์ ํด์ผ ํ ๊ฒฝ์ฐ, @State๋ฅผ @StateObject๋ @EnvironmentObject๋ก ๋ฐ๊ฟ์ผ ํ๋ ๋ถํธํจ.
๐น TCA ๋ฐฉ์ (Single State)
TCA์์๋ ์ฑ์ ๋ชจ๋ ์ํ๋ฅผ State ๋ผ๋ ๋จ์ผ ๊ตฌ์กฐ์ฒด์์ ๊ด๋ฆฌํฉ๋๋ค.
struct CounterFeature: Reducer {
struct State: Equatable {
var count: Int = 0
}
}
- count ์ํ๋ ๋ ์ด์ View ์์ ์กด์ฌํ์ง ์๊ณ , State ๊ตฌ์กฐ์ฒด ์์์ ๊ด๋ฆฌ๋จ.
- ์ฆ, ์ฑ ์ ์ฒด๊ฐ ๋จ ํ๋์ ์ํ(State)๋ก ํํ๋จ.
- ๋๋ถ์ ์ฌ๋ฌ ํ๋ฉด์์ ๊ฐ์ ์ํ๋ฅผ ์ฝ๊ฒ ๊ณต์ ํ๊ณ ๊ด๋ฆฌํ ์ ์์.
โก "์ฑ์ ๋ชจ๋ ๋ฐ์ดํฐ๋ ํ ๊ณณ์์ ๊ด๋ฆฌ๋๋ค!"
โก "์ด๋์๋ ๊ฐ์ ๋ฐ์ดํฐ๋ฅผ ์ฝ๊ฒ ์ฐธ์กฐํ๊ณ ๋ณ๊ฒฝํ ์ ์๋ค!"
2๏ธโฃ ์์ ํจ์(Pure Function) ๊ธฐ๋ฐ์ ์ก์ ์ฒ๋ฆฌ
๐ ๋ชจ๋ ์ํ(State) ๋ณ๊ฒฝ์ ์์ ํจ์(Pure Function)์ธ Reducer์์๋ง ์ฒ๋ฆฌํ๋ค.
๐ ์ฆ, Reducer ๋ด๋ถ์์๋ง State๋ฅผ ๋ณ๊ฒฝํ ์ ์๊ณ , ๋ค๋ฅธ ๊ณณ์์๋ ์ํ๋ฅผ ์ง์ ์์ ํ ์ ์๋ค.
๐ก ์์ ํจ์๋?
๊ฐ์ ์ ๋ ฅ(State + Action)์ด ๋ค์ด์ค๋ฉด ํญ์ ๊ฐ์ ์ถ๋ ฅ(State)์ด ๋์ค๋ ํจ์๋ถ์ํจ๊ณผ(Side Effect)๊ฐ ์์
⇒ ๋คํธ์ํฌ ์์ฒญ, DB ์ ์ฅ ๊ฐ์ ์์ ์ ํ์ง ์์
๐น SwiftUI์์ ์ง์ ์ํ๋ฅผ ๋ณ๊ฒฝํ๋ ๋ฐฉ์
๊ธฐ๋ณธ SwiftUI์์๋ ์ํ๋ฅผ ์ง์ ๋ณ๊ฒฝํ ์ ์์ต๋๋ค.
struct CounterView: View {
@State private var count: Int = 0
var body: some View {
VStack {
Text("\\(count)")
Button("์ฆ๊ฐ") {
count += 1 // ์ง์ ์ํ๋ฅผ ๋ณ๊ฒฝ
}
}
}
}
- Button์ ๋๋ฅด๋ฉด count += 1 ์ด ์คํ๋๋ฉด์ ์ํ๊ฐ ์ฆ์ ๋ณ๊ฒฝ๋จ.
- ํ์ง๋ง ์ํ๊ฐ ์ฌ๋ฌ ๊ณณ์์ ์์ ๋ ๊ฒฝ์ฐ ์์ธกํ๊ธฐ ์ด๋ ค์์ง๋ฉฐ, ๋๋ฒ๊น ์ด ์ด๋ ต๊ณ , ์ํ ๋ณ๊ฒฝ ๋ก์ง์ด ๋ถ์ฐ๋จ.
๐น TCA ๋ฐฉ์ (Reducer์์๋ง ์ํ๋ฅผ ๋ณ๊ฒฝ)
TCA์์๋ ์ํ๋ฅผ ์ง์ ๋ณ๊ฒฝํ์ง ์๊ณ , Reducer๋ฅผ ํตํด์๋ง ๋ณ๊ฒฝ ํด์ผ ํฉ๋๋ค.
struct CounterFeature: Reducer {
struct State: Equatable {
var count: Int = 0
}
enum Action: Equatable {
case increment
}
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .increment:
state.count += 1 // ์ํ ๋ณ๊ฒฝ
return .none
}
}
}
}
๐ก Reducer์ ์์น
- ์
๋ ฅ์ด ๊ฐ์ผ๋ฉด ํญ์ ๊ฐ์ ๊ฒฐ๊ณผ๋ฅผ ๋ฐํํด์ผ ํ๋ค.
- state.count += 1 ์ ๊ฐ์ Action.increment ๊ฐ ๋ค์ด์ค๋ฉด ํญ์ ๊ฐ์ ๋์์ ์ํํจ.
- Reducer ๋ด๋ถ์์๋ง State๋ฅผ ๋ณ๊ฒฝํ ์ ์๋ค.
- View์์๋ state.count += 1 ๊ฐ์ ์ฝ๋๋ฅผ ์ง์ ์คํํ ์ ์์.
- Side Effect(๋ถ์ํจ๊ณผ) ์์ด ์ํ ๋ณ๊ฒฝ๋ง ๋ด๋นํ๋ค.
- API ์์ฒญ ๊ฐ์ ์์ ์ Effect๋ฅผ ํตํด ๋ถ๋ฆฌํด์ผ ํจ.
โก "Reducer์์๋ง ์ํ๋ฅผ ๋ณ๊ฒฝํ ์ ์๋ค!"
โก "Reducer๋ ๊ฐ์ ์ ๋ ฅ(Action + State)์ ๋ํด ํญ์ ๊ฐ์ ์ถ๋ ฅ์ ๋ด์ผ ํ๋ค!"
3๏ธโฃ ์ก์ ๊ธฐ๋ฐ ์ํ ๋ณ๊ฒฝ (Action-Driven State Management)
TCA์์๋ Action์ ํตํด ์ํ ๋ณ๊ฒฝ์ด ์ด๋ฃจ์ด์ง๋๋ค.
์ด๋ ๊ฒ ํ๋ฉด ์ํ ๋ณ๊ฒฝ์ด ๋ช ํํ๊ฒ ์ถ์ ๊ฐ๋ฅ ํด์ง๊ณ , ์ด๋ค ์ด๋ฒคํธ๊ฐ ๋ฐ์ํ๋์ง๋ฅผ ์ฝ๊ฒ ์ ์ ์์ต๋๋ค.
enum Action: Equatable {
case increment
}
- ์ฌ์ฉ์์ ์ ๋ ฅ์ Action์ผ๋ก ์ ์ํ๊ณ ,
- Reducer์์ Action์ ๋ฐ์ State๋ฅผ ๋ณ๊ฒฝํฉ๋๋ค.
โก "๋ชจ๋ ์ํ ๋ณ๊ฒฝ์ ๋ช ํํ Action์ ํตํด ์ด๋ฃจ์ด์ง๋ค!"
๐ ์ ๋ฆฌ: TCA๊ฐ Redux ํจํด์์ ์ํฅ์ ๋ฐ์ ์ด์
โ ํต์ฌ ๊ฐ๋ | โ ์ค๋ช |
๋จ์ผ ์ํ (Single State) | ์ฑ์ ๋ชจ๋ ๋ฐ์ดํฐ๋ฅผ ํ๋์ State ๊ตฌ์กฐ์ฒด์์ ๊ด๋ฆฌ |
์์ ํจ์ ๊ธฐ๋ฐ ์ํ ๋ณ๊ฒฝ (Pure Function Reducer) | ์ํ ๋ณ๊ฒฝ์ ๋ฐ๋์ Reducer์์๋ง ์ฒ๋ฆฌ (View์์ ์ง์ ๋ณ๊ฒฝ ๋ถ๊ฐ) |
์ก์ ๊ธฐ๋ฐ ์ํ ๋ณ๊ฒฝ (Action-Driven State Management) | Action์ ํตํด ๋ช ํํ๊ฒ ์ํ๋ฅผ ๋ณ๊ฒฝ |
๐ TCA๋ ๋จ๋ฐฉํฅ(Unidirectional) ์ํคํ ์ฒ์ด๋ค.
TCA(The Composable Architecture)๋ Redux ํจํด์์ ์ํฅ์ ๋ฐ์ ๋จ๋ฐฉํฅ ๋ฐ์ดํฐ ํ๋ฆ(Unidirectional Data Flow, UDF)์ ๋ฐ๋ฅด๋ ๊ตฌ์กฐ์ด๋ค.
์ฆ, ๋ฐ์ดํฐ๊ฐ ํ ๋ฐฉํฅ์ผ๋ก๋ง ํ๋ฅด๋ฉฐ, ์ํ ๋ณ๊ฒฝ์ด ์์ธก ๊ฐ๋ฅํ๊ฒ ๊ด๋ฆฌ๋๋ค.
1๏ธโฃ ๋จ๋ฐฉํฅ ๋ฐ์ดํฐ ํ๋ฆ์ด๋?
๋จ๋ฐฉํฅ ๋ฐ์ดํฐ ํ๋ฆ์ด๋ ์ฌ์ฉ์์ ์ ๋ ฅ(์ก์ )์ด ์ํ(State)๋ฅผ ๋ณ๊ฒฝํ๋ ์ ์ผํ ๊ฒฝ๋ก๊ฐ ์ ํด์ ธ ์๋ ๊ตฌ์กฐ ๋ฅผ ์๋ฏธํ๋ค.
โ ๋จ๋ฐฉํฅ ๋ฐ์ดํฐ ํ๋ฆ (TCA์์์ ํ๋ฆ)
- ์ฌ์ฉ์๊ฐ ๋ฒํผ์ ํด๋ฆญํ๋ฉด(Action)
- Reducer๊ฐ Action์ ๋ฐ์ State๋ฅผ ๋ณ๊ฒฝํ๊ณ
- View๊ฐ ๋ณ๊ฒฝ๋ State๋ฅผ ๋ค์ ๋ ๋๋งํ๋ค.
๐ ๋จ ํ๋์ ๋ฐฉํฅ๋ง ์กด์ฌํ๋ค: [์ฌ์ฉ์ ์ ๋ ฅ] → [Action ๋ฐ์] → [Reducer] → [State ๋ณ๊ฒฝ] → [View ์ ๋ฐ์ดํธ]
2๏ธโฃ TCA์ ๋ฐ์ดํฐ ํ๋ฆ (Unidirectional Flow)
TCA์์ ๋ฐ์ดํฐ๋ ์๋์ ๊ฒฝ๋ก๋ก๋ง ์ด๋ํ๋ค.
(1) View → Store (Action ์ ์ก)
- ์ฌ์ฉ์์ ์ ๋ ฅ(๋ฒํผ ํด๋ฆญ ๋ฑ)์ด ๋ฐ์ํ๋ฉด Action์ Store.send(action)์ ํตํด ์ ๋ฌํ๋ค.
struct CounterView: View {
let store: StoreOf<CounterFeature>
var body: some View {
WithViewStore(store, observe: { $0 }) { viewStore in
VStack {
Text("\\(viewStore.count)")
Button("+") {
viewStore.send(.increment) // Action ๋ฐ์
}
}
}
}
}
- ์ฌ์ฉ์๊ฐ ๋ฒํผ์ ๋๋ฅด๋ฉด Action.increment๊ฐ Reducer๋ก ์ ๋ฌ๋จ.
(2) Store → Reducer (์ํ ๋ณ๊ฒฝ)
- Reducer์์ Action์ ๋ฐ์ State๋ฅผ ๋ณ๊ฒฝํ๋ค.
struct CounterFeature: Reducer {
struct State: Equatable {
var count: Int = 0
}
enum Action: Equatable {
case increment
}
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .increment:
state.count += 1 // ์ํ ๋ณ๊ฒฝ
return .none
}
}
}
}
- Reducer๋ ์์ ํจ์(Pure Function)๋ก, Action์ ์ฒ๋ฆฌํ๊ณ ์๋ก์ด State๋ฅผ ๋ฐํํจ.
(3) Store → View (์ํ ์ ๋ฌ)
- Store๊ฐ ๋ณ๊ฒฝ๋ State๋ฅผ View์ ์ ๋ฌํ๋ฉด, SwiftUI๊ฐ ๋ค์ ๋ ๋๋งํ๋ค.
WithViewStore(store, observe: { $0 }) { viewStore in
Text("\\(viewStore.count)")
}
- WithViewStore๋ฅผ ํตํด State๋ฅผ ๊ตฌ๋ ํ๊ณ ์๋ค๊ฐ count ๊ฐ์ด ๋ณ๊ฒฝ๋๋ฉด ํ๋ฉด์ ๋ค์ ๊ทธ๋ฆผ.
3๏ธโฃ ๋จ๋ฐฉํฅ ๋ฐ์ดํฐ ํ๋ฆ์ ์ฅ์
โ ์์ธก ๊ฐ๋ฅํจ → ์ํ(State)๊ฐ ๋ณ๊ฒฝ๋๋ ์ ์ผํ ๊ฒฝ๋ก๊ฐ ์กด์ฌํ๋ฏ๋ก, ๋๋ฒ๊น ์ด ์ฌ์.
โ ํ ์คํธ๊ฐ ์ฉ์ดํจ → Reducer๊ฐ ์์ ํจ์์ด๋ฏ๋ก, ๊ฐ์ ์ ๋ ฅ(Action)์ด ๋ค์ด์ค๋ฉด ๊ฐ์ ๊ฒฐ๊ณผ(State)๊ฐ ๋์์ ๋จ์ ํ ์คํธ๊ฐ ๊ฐ๋ฅ.
โ ์ ์ง๋ณด์์ฑ์ด ๋์ → ์ํ(State), ๋ก์ง(Reducer), UI(View)๊ฐ ๋ถ๋ฆฌ๋์ด ์ฝ๋๊ฐ ๋ช ํํจ.
4๏ธโฃ ์๋ฐฉํฅ ๋ฐ์ดํฐ ํ๋ฆ๊ณผ์ ๋น๊ต
โ ์๋ฐฉํฅ(Bidirectional) ๋ฐ์ดํฐ ํ๋ฆ์ด๋?
- ์ํ(State)๋ฅผ ์ฌ๋ฌ ๊ณณ์์ ์ง์ ์์ ํ๊ฑฐ๋,
- ๋ฐ์ดํฐ๊ฐ ์์ ์ปดํฌ๋ํธ์์ ํ์ ์ปดํฌ๋ํธ๋ก ๋ด๋ ค๊ฐ ์๋ ์๊ณ , ๋ฐ๋๋ก ์ฌ๋ผ๊ฐ ์๋ ์๋ ๊ตฌ์กฐ.
๐ก ์์: SwiftUI์ @Binding์ ์ฌ์ฉํ ์๋ฐฉํฅ ๋ฐ์ดํฐ ํ๋ฆ
struct ParentView: View {
@State private var count = 0
var body: some View {
ChildView(count: $count) // ์๋ฐฉํฅ ๋ฐ์ธ๋ฉ
}
}
struct ChildView: View {
@Binding var count: Int // ์ํ๋ฅผ ์ง์ ์์ ๊ฐ๋ฅ
var body: some View {
Button("+") { count += 1 } // ์ํ๋ฅผ ์ง์ ๋ณ๊ฒฝ
}
}
- @Binding์ ์ฌ์ฉํ๋ฉด ํ์ ๋ทฐ์์ ์์ ๋ทฐ์ ์ํ๋ฅผ ์ง์ ์์ ๊ฐ๋ฅ.
- count += 1์ด ์ง์ ์คํ๋์ด State๊ฐ ๋ณ๊ฒฝ๋จ.
- TCA์ ๋ฌ๋ฆฌ, ์ํ ๋ณ๊ฒฝ์ด ์ด๋์๋ ๋ฐ์ํ ์ ์์ด์ ์์ธกํ๊ธฐ ์ด๋ ค์ธ ์ ์์.
๐ง ๋ง์น๋ฉฐ…
๋ค์ ๊ธ์์๋ ๊ฐ๋จํ ์์ ์ ํจ๊ป ์ต์ ๋ฒ์ ์ TCA์ ํต์ฌ ๊ฐ๋ ๊ณผ ๋์ ๊ณผ์ ์ ์์๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
'SwiftUI' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
[SwiftUI] SwiftUI + TCA: Client ํ๊ณ ๊ฐ๊ธฐโฆ (0) | 2024.07.05 |
---|---|
[SwiftUI] SwiftUI + TCA: ์ต์ ๋ฒ์ TCA์ ๋์ ๊ณผ์ (์์ ํฌํจ) (0) | 2024.07.05 |
[SwiftUI] @AppStorage (0) | 2024.05.23 |
[SwiftUI] @ObservableObject / @Published / @StateObject / @ObservedObject (0) | 2024.05.20 |
[SwiftUI] @ViewBuilder (0) | 2024.05.20 |