iOS

[iOS] The Composable Architecture(TCA) 도입하기

Hatchling.dev 2024. 2. 28. 23:37

(편의상 편한 말투로 작성하는 점 이해 부탁드립니다.😅)

(부정확한 정보가 있을 수 있습니다. 지적 환영🤗)

 

안녕하세요! Hatchling입니다!

 

오늘은 TCA를 어떻게 사용하는지, 도입하고 어떤 점이 좋았는지에 대해 생각 정리를 해보겠습니다!

혹시나 TCA를 모르시는 분은 아래 링크를 참고해 주세요!

https://woo0dev.tistory.com/19

 

[iOS] SwiftUI + MVVM에 대한 고찰

(편의상 편한 말투로 작성하는 점 이해 부탁드립니다.😅) (부정확한 정보가 있을 수 있습니다. 지적 환영🤗) 오늘 글은 순전히 내 개인적인 궁금증에 의한 글이며 혼자 생각해봤던 내용을 정리

woo0dev.tistory.com

 

 

 

버튼을 눌렀을 때 숫자가 올라가는 예제를 통해 기본적인 사용법을 알아보도록 하겠습니다!

 

1. 앱의 상태: State

State는 Reducer의 현재 상태를 가지고 있는 구조체로 UI나 로직에 필요한 데이터를 가지고 있습니다.

아래는 화면에 표시해 줄 count 변수를 가지고 있는 State 구조체입니다.

@Reducer
struct MainReducer {
    struct State: Equatable {
        var count: Int = 0
    }
}

 

 

Binding 변수를 추가해 볼까요??

짝수인지 아닌지 확인하기 위해 Bool 타입의 Binding 변수를 추가해 주었습니다.

@Reducer
struct MainReducer {
    struct State: Equatable {
        @BindingState var isEven: Bool = false
        var count: Int = 0
    }
}

 

2. 상태 변화를 일으키는 모든 동작: Action

Action 열거형에 사용자 이벤트가 발생하는 action들을 정의할 수 있습니다.

만약 아래처럼 buttonTapped라는 action이 있다고 하면 UI에서는 사용자가 버튼을 눌렀을 때 해당 action을 Effect로 보내게 됩니다.

@Reducer
struct MainReducer {
    // State { }
    enum Action: Equatable {
        case buttonTapped
    }
}

 

 

아래처럼 action에 값을 전달할 수도 있습니다.

@Reducer
struct MainReducer {
    // State { }
    enum Action: BindableAction, Equatable {
        case buttonTapped
        case valueButtonTapped(Int)
        case binding(BindingAction<State>)
    }
}

 

만약 Binding 변수를 추적하고 싶다면 아래처럼 Action에 BindableAction 프로토콜을 추가하여 정의하면 됩니다!

@Reducer
struct MainReducer {
    // State { }
    enum Action: BindableAction, Equatable {
        case buttonTapped
        case binding(BindingAction<State>)
    }
}

 

 

3. 변경을 처리: Reduce

Reduce에서는 발생한 Action에 따라 정의된 동작을 수행합니다.

아래 코드는 buttonTapped Action이 발생하면 State에 있는 count에 1을 더하는 동작을 수행하게 됩니다. 

@Reducer
struct MainReducer {
    // State { }
    // Action { }
    var body: some Reducer<State, Action> {
        Reduce { state, action in
            switch action {
            case .buttonTapped:
                state.count += 1
                return .none
            }
        }
    }
}

 

아래처럼 전달받은 값을 사용할 수도 있습니다.

@Reducer
struct MainReducer {
    // State { }
    // Action { }
    var body: some Reducer<State, Action> {
        Reduce { state, action in
            switch action {
            case .buttonTapped:
                state.count += 1
                return .none
            case .valueButtonTapped(let value):
                state.count = value
                return .none
            }
        }
    }
}

 

Binding 변수를 처리하는 방법은 아래와 같습니다.

@Reducer
struct MainReducer {
    // State { }
    // Action { }
    var body: some Reducer<State, Action> {
        Reduce { state, action in
            switch action {
            case .buttonTapped:
                state.count += 1
                return .none
            case .binding(\.$isEven):
                // isEven 값이 변할 때마다 실행될 코드
                return .none
            }
        }
    }
}

 

지금까지는 Reducer에서 단순하게 State를 변경시키는 방법들을 알아봤는데요!

혹시 저 action 마다 보이는 return .none이 무엇인지 궁금하셨나요??

Reducer에서는 Effect를 발생시킬 수 있는데요!

단순 Effect인 .concatenate와 비동기 Effect인 .run이 있습니다!!

 

.concatenate

아래 코드는 State의 count가 짝수라면 buttonTapped Effect를 발생시키는 코드입니다. 저 코드의 count는 무조건 짝수일 수밖에 없겠네요!

@Reducer
struct MainReducer {
    // State { }
    // Action { }
    var body: some Reducer<State, Action> {
        Reduce { state, action in
            switch action {
            case .buttonTapped:
                state.count += 1
                if state.count % 2 == 0 {
                    let effect: Effect<Action> = .send(.buttonTapped)
                    return .concatenate(effect)
                }
                return .none
            }
        }
    }
}

 

 

.run

아래처럼 작성하면 .run 클로저 안에서 비동기 작업을 수행하게 됩니다.

@Reducer
struct MainReducer {
    // State { }
    // Action { }
    var body: some Reducer<State, Action> {
        Reduce { state, action in
            switch action {
            case .buttonTapped:
                return .run { [cnt = state.count] send in // state.count 값 캡쳐
                    // 비동기 작업 코드
                } catch {
                    // non-cancellation에 대한 에러 처리
                }
            }
        }
    }
}

 

 

 

자 이렇게 오늘은 TCA를 활용하는 방법을 알아보았는데요!

실제 사이드 프로젝트에 도입하여 사용해 보니 정말 편하게 느껴졌습니다.

물론 프로젝트 규모도 작고 혼자서 진행하다보니 실제 현업에서는 사용할 때와 느낌이 조금 다를 수 있습니다(머쓱😅)

 

뭐가 편하냐! 라고 물으신다면

1. 코드 자체가 단순해집니다.

    State와 Action만을 사용해 앱의 동작을 정의하기 때문에 코드가 단순해 가독성이 좋아요! (Combine을 사용하다보면 다양한 property wrapper를 사용하면서 꼬이는 경우도 가끔 있었다는 건 안비밀😅)

2. 테스트 코드를 작성하기 편합니다.

    Reducer가 순수 함수로 작성되기 때문에 테스트 코드를 작성하기 좋습니다. 특정 State에 대한 Action이 예상대로 동작하는지 알기 쉽습니다.

3. 재사용성이 좋습니다.

    Reducer 단위로 기능을 정의하기 때문에 새로운 기능을 추가하거나 재사용하기 편합니다.

 

 

 

오늘의 포스팅은 여기까지👏

저도 아직 작디 작은 사이드 프로젝트에 도입해 본 경험 뿐이라 정말 찍먹만 해봤다고 생각합니다!

혹시나 프로젝트를 더 진행하면서 얘깃거리가 생긴다면 다시 돌아오도록 하겠슴다!!

혹시나 부족한 부분이나 틀린 부분이 있다면 댓글 남겨주세요!! (비난이 아닌 비판! 사랑 그 자체😍)

 

 

출처

https://github.com/pointfreeco/swift-composable-architecture

 

GitHub - pointfreeco/swift-composable-architecture: A library for building applications in a consistent and understandable way,

A library for building applications in a consistent and understandable way, with composition, testing, and ergonomics in mind. - pointfreeco/swift-composable-architecture

github.com

https://axiomatic-fuschia-666.notion.site/SwiftUI-iOS-TCA-1-0-596f01cfa306427ea47779406da676e1

 

SwiftUI 상태 관리부터 테스트까지 iOS 개발자를 위한 TCA 1.0 | Notion

Built with Notion, the all-in-one connected workspace with publishing capabilities.

axiomatic-fuschia-666.notion.site