안녕하세요 Niro 🚗 입니다!
SwiftUI 에서 핫한 라이브러리로 TCA를 알아보고자 Point-Free 에서 튜토리얼을 제공하고 있어 어떤 동작원리를 갖고 있는지 경험해보고 정리해보고자 합니다!
차근 차근 한단계씩 진행할 예정이니 잘 읽어주세요!
🔗 The Composable Architecture Tutorials
https://pointfreeco.github.io/swift-composable-architecture/main/tutorials/meetcomposablearchitecture
Composable Architecture 에서 feature 가 빌드되는 기본 단위는 Reducer()
매크로와 Reducer
Protocol 입니다!
해당 Protocol 을 준수한다는 의미는 앱의 feature 에 대한 로직과 Action 을 나타낸다고 합니다.
여기서 Action 은 State 를 변경하는 방법과 발생되는 Effect 로 인해 통신을 하고 시스템에 데이터를 받아오는 방법을 포함하고 있습니다.
가장 중요한 것은 feature 의 핵심 로직과 동작을 SwiftUI View 에 완전히 분리하여 빌드할 수 있기 때문에 재사용이 가능하고 테스트하기 쉽다는 장점이 있습니다.
우리는 이제 카운트를 하는 앱을 만들게 되는데 카운트 로직을 캡슐화하는 간단한 Reducer 를 만드는 것부터 시작하려고 합니다!
import ComposableArchitecture
@Reducer
struct CounterFeature {
@ObservableState
struct State {
var count = 0
}
enum Action {
case decrementButtonTapped
case incrementButtonTapped
}
}
CounterFeature.swift
라는 이름의 새 Swift 파일을 생성하고 ComposableArchitecture
를 Import 해줍니다.
그 다음CounterFeature
라는 구조체를 정의하고 Reducer() 매크로로 주석을 작성해주어야 합니다.
여기서 Reducer 매크로는 다양한 작업을 하지만 여기선 Reducer protocol 을 준수하도록 타입을 확장한다는 점만 알고 있으면 됩니다!
그럼 우리는 Reducer
protocol 을 준수하도록 해당 기능이 작업을 수행하는데 필요한 상태를 저장하는 State
유형(주로 Struct) 과 사용자가 feature 에서 수행할 수 있는 모든 작업을 포함하는 Action
유형(주로 Enum)을 추가합니다.
count
수를 보여주기 위한 프로퍼티를 추가한 코드와 button 을 눌러 count 를 증감시키는 case 를 추가한 것을 볼 수 있습니다.
굉장히 직관적이고 어떤 기능을 하고 어떤 행동을 하는지 확인할 수 있는 장점이 있는거 같습니다.
Tip
Action case의 이름은 증감 카운트처럼 수행하려는 로직보다는 증감 버튼 탭처럼 사용자가 UI에서 수행하는 작업의 이름을 따서 짓는 것이 가장 좋습니다. 이렇게 하면 더욱 직관적으로 해당 동작을 파악할 수 있겠죠?
import ComposableArchitecture
@Reducer
struct CounterFeature {
//...
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .decrementButtonTapped:
state.count -= 1
return .none
case .incrementButtonTapped:
state.count += 1
return .none
}
}
}
}
자, 이제 마지막으로 Reducer Protocl 준수하기 위한 body
를 구현해주어야 합니다.
Action 이 주어지면 State 를 업데이트하고 해당 feature 가 네트워크 요청, 알림 등이 모두 외부 세계와의 상호작용하는 Reducer 를 만들어줍니다.
위의 코드를 보면 CounterFeature 는 action 에 맞춰 state.count 를 증감하도록 되어있고 외부에서 실행할 Effect 를 반환해야하지만 여기에서는 아무것도 전달할게 없기 때문에 .none
을 반환했습니다.
우리는 이제 TCA 를 구성하는 가장 기본적인 feature 를 구현했습니다. feature 를 만들었다면 이제 화면에 보여줄 차례겠죠?
우리가 만든 feature 를 reducer 로 만들었기 때문에 SwiftUI 의 View 를 동작시키는 방법을 알아야 합니다.
여기서는 feature 의 런타임을 나타내는 새로운 개념인 Store 가 필요합니다.
struct CounterView: View {
let store: StoreOf<CounterFeature>
var body: some View {
EmptyView()
}
}
Store 는 State 를 업데이트하기 위해 작업을 처리할 수있는 객체로 action 을 수행한 결과를 다시 View 에 전달하게 됩니다.
우리는 SwiftUI 를 통해 기존에 State 래퍼를 통해 관찰가능한 프로퍼티를 만들었지만 여기서는 어떻게 동작을 하는걸까요...?
struct CounterFeature {
@ObservableState
struct State {
var count = 0
}
//...
}
우리는 CounterFeature
를 구현할 때 State
구조체에 ObservableState
매크로를 추가했기 때문에 CounterView
에서 다른 코드 필요없이 데이터를 관찰하게 됩니다.
struct CounterView: View {
let store: StoreOf<CounterFeature>
var body: some View {
VStack {
Text("\(store.count)")
HStack {
Button("-") {
store.send(.decrementButtonTapped)
}
Button("+") {
store.send(.incrementButtonTapped)
}
}
}
}
}
store
에 저장된 CounterFeature
구조체의 count
프로퍼티를 읽고, 그 값을 텍스트로 변환하여 UI에 표시합니다. 이후에 store.count
값이 변경되면 SwiftUI 가 자동으로 해당 텍스트를 업데이트하여 변경된 값을 반영하게 됩니다.
이제 우리는 View 를 보여주기 위한 작업을 완료하였습니다. SwiftUI 의 꽃인 preview 를 통해 바로 화면을 봐야하는데 여기서 설정할 부분이 있습니다...
#Preview {
CounterView(
store: Store(initialState: CounterFeature.State()) {
CounterFeature()
}
)
}
앞서 보았던 CounterView 의 프로퍼티인 store 를 초기화 시켜주어야 합니다. StoreOf<CounterFeature>
를 만들기 위해서는 CounterFeature 의 초기 상태와 기능을 구동하는 reducer 를 지정하는 클로저를 제공해야 합니다. 이렇게하면 미리보기를 통해 CounterView 를 실행할 수 있게 됩니다.
이번에는 애플리케이션의 진입점을 변경하여 feature 를 실행하는 방법에 대해 알아보겠습니다. 이제 전체 애플리케이션에서 해당 기능을 실행할 수 있게 되었습니다. 이를 위해 애플리케이션의 시작 포인트를 수정하여 기능을 실행하고 있는데, 이는 시뮬레이터나 기기에서 실제로 기능을 테스트할 수 있도록 해줍니다.
import ComposableArchitecture
import SwiftUI
@main
struct MyApp: App {
static let store = Store(initialState: CounterFeature.State()) {
CounterFeature()
._printChanges()
}
var body: some Scene {
WindowGroup {
CounterView(store: MyApp.store)
}
}
}
다만 애플리케이션을 구동하는 데 사용되는 store 는 한 번만 생성되어야 합니다. 대부분의 경우에는 Scene 의 루트에 있는 WindowGroup 에 직접 생성하는 것으로 충분하지만, 경우에 따라 정적 변수로 보관한 다음 Scene 에 제공할 수도 있습니다.
또한, Composable Architecture 의 강력한 기능 중 하나는 Reducer 가 SwiftUI 에서 제공하는 도구와 유사한 _printChanges
메서드를 제공한다는 것입니다. 이를 통해 개발자는 쉽게 애플리케이션의 동작을 디버깅하고 이해할 수 있습니다. 콘솔에 출력된 로그를 통해 각 액션에 대한 상태 변경을 추적하고, 개발 과정에서 문제를 해결할 수 있습니다.
이제 시뮬레이터에서 애플리케이션을 실행하고 "+" 및 "-" 버튼을 몇 번 탭하면 콘솔에 정확히 어떤 일이 일어나고 있는지 보여주는 로그가 인쇄됩니다.
항상 값이 제대로 바뀌는지, 동작이 제대로 이루어지는지 print 을 추가해서 확인하고 지우는 작업을 했었는데 정말로 유용한 기능인거 같아요 ㅠㅠㅠㅠㅠ
우리는 Composable Architecture 를 구성하는 아주 간단한 기능을 구현해보았습니다.
화면에 보여줄 count 와 count 를 증감하는 action 을 구성하는 Feature 를 만들었고 Reducer 를 통해 body 내에서 Feature 의 동작을 정의하고 상태를 변경하도록 코드를 작성해보았습니다.
Store 는 feature 의 상태를 갖고 있고 View 에 값을 전달하고 Reducer 를 통해 상태를 변경하는 과정을 볼 수 있었습니다.
TCA 가 굉장히 어렵고 막연한 생각이 많았는데 튜토리얼을 통해 재미를 붙일 수 있어서 너무 좋네요!
두번째 주제인 Side Effect 를 추가해보는 튜토리얼로 돌아오겠습니다!