범위

이번 정리에서 볼 흐름.

Action
-> Reducer
-> State 변경
-> Effect 반환
-> Effect가 다시 Action 전송
-> Reducer
-> State 변경

1편에서는 Reducer가 State만 바꾸고 .none을 반환했다.

2편에서는 Reducer가 State를 바꾼 뒤 Effect를 반환하는 경우를 본다.

예제 코드

@Reducer
struct CounterEffectFeature {
    @ObservableState
    struct State: Equatable {
        var count = 0
        var isLoading = false
        var message = "Reducer가 State를 바꾸고 Effect를 반환하는 예제"
    }

    enum Action {
        case incrementButtonTapped
        case delayedIncrementButtonTapped
        case delayedIncrementFinished
    }

    var body: some ReducerOf<Self> {
        Reduce { state, action in
            switch action {
            case .incrementButtonTapped:
                state.count += 1
                state.message = "State만 즉시 변경"

            case .delayedIncrementButtonTapped:
                state.isLoading = true
                state.message = "Effect 실행 중"

                return .run { send in
                    try await Task.sleep(for: .seconds(1))
                    await send(.delayedIncrementFinished)
                }

            case .delayedIncrementFinished:
                state.count += 1
                state.isLoading = false
                state.message = "Effect가 Action을 다시 보낸 뒤 State 변경"
            }

            return .none
        }
    }
}

State만 변경하는 Action

case .incrementButtonTapped:
    state.count += 1
    state.message = "State만 즉시 변경"

이 case는 외부 작업이 없다.

상태만 바꾸고 switch 밖의 return .none으로 내려간다.

return .none

Effect가 없는 Action.

Effect를 반환하는 Action

case .delayedIncrementButtonTapped:
    state.isLoading = true
    state.message = "Effect 실행 중"

    return .run { send in
        try await Task.sleep(for: .seconds(1))
        await send(.delayedIncrementFinished)
    }

이 case는 두 가지를 같이 한다.

  • 바로 State 변경
  • Effect 반환

isLoading과 message는 Action을 받은 즉시 바뀐다.

그 다음 .run Effect를 반환한다.

return .run { send in
    try await Task.sleep(for: .seconds(1))
    await send(.delayedIncrementFinished)
}

.run 내부는 비동기 작업을 실행하는 공간.

작업이 끝나면 send로 다음 Action을 보낸다.

Effect가 다시 보낸 Action

case .delayedIncrementFinished:
    state.count += 1
    state.isLoading = false
    state.message = "Effect가 Action을 다시 보낸 뒤 State 변경"

Effect 안에서 보낸 .delayedIncrementFinished도 결국 Action이다.

Action이기 때문에 다시 Reducer로 들어온다.

Reducer는 이 Action을 받아서 다시 State를 변경한다.

흐름

delayedIncrementButtonTapped
-> isLoading = true
-> return .run
-> Task.sleep
-> send(.delayedIncrementFinished)
-> count += 1
-> isLoading = false

현재 기준.

Reducer는 State를 바꿀 수 있다.
Reducer는 Effect를 반환할 수 있다.
Effect는 작업이 끝난 뒤 Action을 다시 보낼 수 있다.
State 변경은 다시 Reducer에서 일어난다.

.none과 .run

return .none

  • 추가 작업 없음
  • State 변경만 하고 종료

return .run

  • 비동기 작업 실행
  • 작업 결과를 Action으로 다시 전달

Reducer 안에서 직접 오래 걸리는 작업을 끝까지 처리하지 않는다.

Reducer는 Effect를 반환하고 Effect가 다시 Action을 보내는 구조로 이어진다.

정리

1편 기준.

Action -> Reducer -> State

2편 기준.

Action -> Reducer -> State
                 -> Effect -> Action -> Reducer -> State

Effect가 들어와도 State 변경의 위치는 Reducer다.

Effect는 State를 직접 바꾸는 도구가 아니라 다음 Action을 만들어 다시 Reducer로 보내는 도구로 정리한다.

참고