범위

이번 정리에서 볼 흐름

ParentFeature
-> Scope
-> ChildFeature

지금까지는 화면 하나에 Feature 하나를 붙였다

이번에는 Feature 안에 다른 Feature를 넣어볼 것이다

예제는 계속 카운터를 사용한다

다만 카운터 자체를 자식 Feature로 두고 부모 Feature가 그 카운터를 화면에 붙인다

이번 글에서 볼 것은 두 가지

Reducer 안의 Scope
View 안의 store.scope

자식 Feature

먼저 기존 카운터와 거의 같은 Feature를 만든다

@Reducer
struct CounterScopeFeature {
    @ObservableState
    struct State: Equatable {
        var count = 0
    }

    enum Action {
        case decrementButtonTapped
        case incrementButtonTapped
    }

    var body: some ReducerOf<Self> {
        Reduce { state, action in
            switch action {
            case .decrementButtonTapped:
                state.count -= 1

            case .incrementButtonTapped:
                state.count += 1
            }

            return .none
        }
    }
}

이 Feature는 혼자서도 완성된 카운터다

가지고 있는 것

count State
증가 Action
감소 Action
count를 바꾸는 Reducer

부모 Feature

부모 Feature는 자식 Feature의 State를 가진다

@Reducer
struct CounterDashboardFeature {
    @ObservableState
    struct State: Equatable {
        var counter = CounterScopeFeature.State()
    }

    enum Action {
        case counter(CounterScopeFeature.Action)
        case resetButtonTapped
    }
}

여기서 counter는 자식 Feature의 State다

부모 State 안에 자식 State가 들어간다

CounterDashboardFeature.State
-> counter: CounterScopeFeature.State
-> count

Action도 같은 구조다

case counter(CounterScopeFeature.Action)

자식에서 발생한 Action은 부모 입장에서 .counter(...)로 감싸진다

Child Action
-> CounterScopeFeature.Action.incrementButtonTapped

Parent Action
-> CounterDashboardFeature.Action.counter(.incrementButtonTapped)

Reducer에서 Scope

부모 Reducer는 Scope로 자식 Reducer를 연결한다

var body: some ReducerOf<Self> {
    Scope(state: \.counter, action: \.counter) {
        CounterScopeFeature()
    }

    Reduce { state, action in
        switch action {
        case .counter:
            break

        case .resetButtonTapped:
            state.counter.count = 0
        }

        return .none
    }
}

Scope가 하는 일

부모 State 중 counter만 자식 Reducer에 넘김
부모 Action 중 counter Action만 자식 Reducer에 넘김

즉 이 코드

Scope(state: \.counter, action: \.counter) {
    CounterScopeFeature()
}

풀어서 보면 이런 의미다

CounterDashboardFeature.State.counter
-> CounterScopeFeature.State

CounterDashboardFeature.Action.counter
-> CounterScopeFeature.Action

그래서 부모 Reducer의 .counter case에서는 직접 처리할 일이 없다

case .counter:
    break

자식 카운터 Action은 CounterScopeFeature가 처리한다

부모는 부모가 가진 Action만 처리하면 된다

case .resetButtonTapped:
    state.counter.count = 0

여기서는 부모가 자식 State를 초기화한다

이런 식으로 부모는 자식 Feature를 조합하면서 필요한 상위 동작만 추가할 수 있다

View에서 store.scope

Reducer에서 Scope를 연결했으면 View에서도 자식 View에 자식 Store를 넘겨야 한다

부모 View는 부모 Store를 가진다

struct CounterScopeView: View {
    let store: StoreOf<CounterDashboardFeature>
}

자식 View는 자식 Store를 가진다

struct CounterScopeChildView: View {
    let store: StoreOf<CounterScopeFeature>
}

부모 Store를 자식 Store로 좁힐 때 store.scope를 쓴다

CounterScopeChildView(
    store: store.scope(state: \.counter, action: \.counter)
)

이 코드가 하는 일

부모 StoreOf<CounterDashboardFeature>
-> 자식 StoreOf<CounterScopeFeature>

자식 View 입장에서는 부모가 있다는 사실을 몰라도 된다

자식 View는 그냥 자기 Store만 사용한다

Button {
    store.send(.incrementButtonTapped)
} label: {
    Image(systemName: "plus")
}

하지만 실제 흐름은 부모 Store를 거친다

ChildView
-> store.send(.incrementButtonTapped)
-> ParentAction.counter(.incrementButtonTapped)
-> Scope
-> CounterScopeFeature
-> state.counter.count += 1

Reducer Scope와 View scope

이 둘은 이름이 비슷하지만 위치가 다르다

Reducer의 Scope
-> 부모 Reducer가 자식 Reducer를 포함하는 구조

View의 store.scope
-> 부모 Store에서 자식 View용 Store를 만드는 구조

둘 다 필요하다

Reducer 쪽 Scope만 있으면 자식 Reducer는 연결되지만 자식 View에 넘길 Store가 없다

View 쪽 store.scope만 있으면 자식 View는 Store를 받을 수 있지만 부모 Reducer가 자식 Action을 처리할 연결이 없다

현재 기준

Scope를 쓰면 Feature를 화면 단위로 나눌 수 있다

중요한 기준은 파일을 작게 쪼개는 것이 아니다

상태와 액션을 가진 기능 단위를 독립된 Feature로 둘 수 있는지가 기준이다

카운터 예제 기준

CounterScopeFeature
-> count를 가진 독립 카운터

CounterDashboardFeature
-> 카운터를 화면에 배치하고 초기화 동작을 추가하는 부모

정리

현재 이해한 Scope

ChildFeature = 독립적으로 State, Action, Reducer를 가진 작은 기능
ParentFeature = ChildFeature.State와 ChildFeature.Action을 포함하는 상위 기능
Reducer Scope = 부모 Reducer 안에서 자식 Reducer를 실행하는 연결
store.scope = 부모 Store를 자식 Store로 좁혀 View에 넘기는 연결

Scope는 작은 Feature를 무조건 많이 만들기 위한 문법이 아니다

이미 독립된 State와 Action을 가진 기능을 상위 화면에 조합하기 위한 문법이다

참고