단방향 데이터 흐름
View | send(Action) v Store.send() | v Reducer(state, action) | → new State v State (Single Source) | @Published v View (자동 갱신) | +—— Effect (비동기) | async/await v Action (다시 전송)
3가지 핵심 원칙
  • Single Source of Truth — AppState 하나
  • State is Read-Only — Action으로만 변경
  • Pure Reducer — 같은 입력 = 같은 출력
  • Effect가 비동기 사이드 이펙트 처리
  • Action이 모든 이벤트를 enum으로 나열
내장 개념
State Reducer Effect

State — 앱의 모든 상태를 하나의 struct에.

Reducer — (inout State, Action) -> [Effect] 순수 함수.

Effect — 비동기 작업 후 다시 Action을 전송.

 06-Redux/Finish/Store.swift
import Foundation
import Combine

// MARK: - Redux Store (완성)

// ---- State: 앱의 모든 상태 ----

struct AppState: Equatable {
    var tasks: [TaskItem] = []
    var searchText = ""
    var filterPriority: TaskItem.Priority? = nil
    var isLoading = false
    var error: String? = nil

    // Computed property — State에서 파생
    var filteredTasks: [TaskItem] {
        tasks.filter { task in
            let matchSearch = searchText.isEmpty ||
                task.title.localizedCaseInsensitiveContains(searchText)
            let matchPriority = filterPriority == nil || task.priority == filterPriority
            return matchSearch && matchPriority
        }
    }
}

// ---- Action: 가능한 모든 이벤트 ----

enum AppAction: Equatable {
    case loadTasks
    case tasksLoaded([TaskItem])
    case addTask(title: String, priority: TaskItem.Priority)
    case taskAdded(TaskItem)
    case toggleTask(id: UUID)
    case deleteTask(id: UUID)
    case setSearchText(String)
    case setFilter(TaskItem.Priority?)
    case setError(String?)
}

// ---- Effect: 비동기 사이드 이펙트 ----

struct Effect {
    let run: @Sendable () async -> AppAction?
}

// ---- Reducer: 순수 함수 ----
// (State, Action) -> [Effect]
// 같은 입력이면 항상 같은 결과 → 테스트 최강

func appReducer(state: inout AppState, action: AppAction) -> [Effect] {
    switch action {

    case .loadTasks:
        state.isLoading = true
        return [Effect { .tasksLoaded(TaskStorage.shared.loadAll()) }]

    case .tasksLoaded(let tasks):
        state.isLoading = false
        state.tasks = tasks
        return []

    case .addTask(let title, let priority):
        let trimmed = title.trimmingCharacters(in: .whitespacesAndNewlines)
        guard !trimmed.isEmpty else { state.error = "Empty title"; return [] }
        let task = TaskItem(title: trimmed, priority: priority)
        // 저장은 Effect가 담당 (사이드 이펙트 분리)
        return [Effect { TaskStorage.shared.add(task); return .taskAdded(task) }]

    case .taskAdded(let task):
        state.tasks.insert(task, at: 0)
        return []

    case .toggleTask(let id):
        if let i = state.tasks.firstIndex(where: { $0.id == id }) {
            state.tasks[i].isCompleted.toggle()
            let tasks = state.tasks
            return [Effect { TaskStorage.shared.saveAll(tasks); return nil }]
        }
        return []

    case .deleteTask(let id):
        state.tasks.removeAll { $0.id == id }
        let tasks = state.tasks
        return [Effect { TaskStorage.shared.saveAll(tasks); return nil }]

    case .setSearchText(let t): state.searchText = t; return []
    case .setFilter(let p): state.filterPriority = p; return []
    case .setError(let m): state.error = m; return []
    }
}

// ---- Store: Reducer를 실행하는 런타임 ----

@MainActor
final class Store: ObservableObject {
    @Published private(set) var state: AppState
    private let reducer: (inout AppState, AppAction) -> [Effect]

    init(initial: AppState = AppState(),
         reducer: @escaping (inout AppState, AppAction) -> [Effect] = appReducer) {
        self.state = initial
        self.reducer = reducer
    }

    func send(_ action: AppAction) {
        let effects = reducer(&state, action)
        for effect in effects {
            Task {
                if let next = await effect.run() { send(next) }
            }
        }
    }
}

// ---- 테스트 예시 ----
// Reducer가 순수 함수이므로 Store 없이 직접 테스트 가능
//
// func test_addTask() {
//     var state = AppState()
//     let effects = appReducer(state: &state, action: .addTask(title: "Buy milk", priority: .high))
//     // effects를 실행하지 않고 State 변화만 검증
//     // (비동기 Effect는 별도 테스트)
// }
핵심 개념
Single Source of Truth
AppState 하나가 앱의 모든 상태를 소유합니다. 어느 화면에서든 같은 State를 구독하므로 상태 불일치가 없습니다.
순수 함수 Reducer
Reducer는 (inout State, Action) -> [Effect]입니다. 외부 상태를 참조하지 않아 같은 입력에 항상 같은 결과를 냅니다. XCTest로 단독 테스트가 매우 쉽습니다.
Effect 분리
비동기 작업(네트워크, 저장)은 Effect로 분리하여 Reducer 밖에서 실행합니다. Reducer를 순수 함수로 유지하면서 사이드 이펙트를 안전하게 처리합니다.
단방향 흐름의 장점
상태 변화가 항상 View → Action → Reducer → State 순서로만 흘러 버그 추적이 쉽습니다. 타임트래블 디버깅, 상태 재현이 가능하며 TCA의 기반 철학입니다.
항목
MVVM
Redux
상태 위치
ViewModel 분산
AppState 하나에 집중
상태 변경
어디서든 직접 변경 가능
Action으로만 변경
Reducer 테스트
ViewModel 전체 필요
순수 함수 단독 테스트
복잡도
단순한 앱에 적합
초기 보일러플레이트 많음