ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • TCA - Scope
    TCA 2022. 9. 26. 09:23

    안녕하세요. 그린입니다🍏

    이번 포스팅에서는 TCA의 Scope에 대해 무엇인지 알아보고 어떻게 이 Scope 범위를 지정하면 좋은지 학습해보겠습니다🙌

     

    우선 아직도 TCA가 어떤건지 생소한분들이 당연히 많을거라고 생각합니다.

    그런분들이라면 요 포스팅을 먼저 보고 오시면 좋습니다!

    https://green1229.tistory.com/138?category=936861 

     

    Composable Architecture

    안녕하세요. 그린입니다🟢 이번 포스팅에서는 Composable Architecture에 대해 학습해보겠습니다🧑🏻‍💻 왜 알아보게 되었는지? 앞으로는 SwiftUI와 사용자 이벤트를 통한 뷰의 업데이트 등 상태 값

    green1229.tistory.com

     

    우선 Scope를 알아보기 이전에 선행으로 알아야될 개념인 Store라는 친구가 있습니다.
    얘부터 보시죠🕺🏻

     

    Store

    public final class Store<State, Action>

    State와 Action을 받는 Store라는 클래스입니다.

    Store는 앱에서 구동하는 런타임을 나타냅니다.

    앱과 상호작용하는 뷰에 전달할 개체인것이죠.

    TCA에서 결국 우리는 뷰에서 상호작용하는것은 모두 State와 Action으로 이뤄져있습니다.

    사용자의 액션, Action으로부터 State의 변화를 일으키고 뷰에 갱신시키고 이 과정인것이죠.

     

    오늘은 Scope에 초점이니 Store는 이쯤만 알아도 충분합니다!
    그럼 바로 Scope로 넘어가시죠.

     

    일반적인 Scope 사용

    위에서 Store에 대해 알아봤어요.

    이 Store에는 그렇다면 State와 Action이 있을겁니다.

    그렇기에 우리는 뷰에서 이 Store를 가지고 전체적으로 scope를 지정해주는것이 일반적인 그림이에요.

    아래와 같이 말이죠.

    struct MainView: View {
      var store: Store<MainState, MainAction>
    
      var body: some View {
        WithViewStore(store) { viewStore in
          VStack {
            Text(viewStore.title)
              .font(.title)
    
            Button(
              action: {
                viewStore.send(.tappedAction)
              },
              label: {
                Text(viewStore.button)
                  .font(.body)
              }
            )
          }
        }
      }
    }

    보시면 Text와 Button으로 구성된 하나의 뷰가 모두 store를 받아 만들어진 viewStore를 가지고 정의되고 있습니다.

    물론 이 구성이 제일 일반적이고 잘못되었다고 볼 수는 없어요.

    그렇지만 하나 생각을 해볼 수 있습니다.

    View는 Store와 연결되어 변화를 감지하면 갱신시켜버립니다.

    그렇기에 Store에 속해있는 어떠한 State 하나가 변화하면 아예 뷰를 다시 그려버리죠.

    왜냐하면 하나의 scope로 묶여 있는것이기 때문이에요.

    즉, Button의 tappedAction이 발생해 어떠한 title 혹은 button State가 변경되면 뷰가 다시 그려진다는 소리입니다.

     

    그럼 이게 왜 어떤 파장을 일으킬까요?

     

    전체적인 Scope 사용의 문제점

    단순히 생각을 해봅시다.

    같은 Scope에서는 State가 하나라도 바뀌면 다시 그린다고 했죠?

    그럼 하위 뷰들을 가진 State, 즉 네비게이션이 되는 뷰가 있다고 생각해볼께요.

    그리고 이 네비게이션을 동작시키는 State 변수가 있어요.

    이 변수의 초기값은 false였다가 네비게이션될 버튼이 눌리는 순간 true로 변경되고 이에 따라 하위뷰로 전환이 됩니다.

    이러한 변수가 여러개일 시 다 동일한 Scope로 지정한다면 의도치 않은 결과를 나타낼때가 많습니다.

    다른 State가 변경됨으로 뷰를 새로 그려 전환된 화면이 다시 튕겨버리는 불상사를 초래합니다.

    백프로 그렇다는건 아닙니다.

    여기서 중요한것은 하나의 Scope로 정의할때 변화를 감지해 뷰를 다시 갱신시킴으로 우리가 의도하지 않은 결과를 초래할 수 있다는 점이에요.

     

    그럼 Scope를 어떻게 사용하는게 적절할까요?

     

    적절한 Scope 사용하기

    아주 쉽습니다.

    관심사별로 Store에서 갖고있는 State와 Action을 가지고 Scope를 지정해 해당 뷰에서는 해당 Scope로 지정된 Store만 매칭시켜주면 됩니다.

    struct MainState: Equatable {
      var mainTitle: String = "Is Changed Sub Title?"
      var subState: SubState = .init()
    }
    
    enum MainAction {
      case subAction(SubAction)
    }

    이러한 Core단이 있습니다.

    메인이 있고 하위로 Sub가 존재해 State와 Action에도 Sub에서 다뤄줄것들이 있죠?

    그럼 뷰에서는 이렇게 나타내면 됩니다.

    struct MainView: View {
      var store: Store<MainState, MainAction>
      
      var body: some View {
        WithViewStore(store) { viewStore in
          VStack(spacing: 30) {
            Text(viewStore.mainTitle)
              .font(.title)
            
            SubView(
              store: store.scope(
                state: \.subState,
                action: MainAction.subAction
              )
            )
          }
        }
      }
    }

    SubView를 그릴때는 전체적인 Store를 사용하는것이 아닌 scope를 사용해 해당 부분에서 사용될 state와 action을 매칭 해줍니다.

    즉 전체에서 해당 도메인의 작은 부분에만 관심을 이전시켜 하위 뷰로 저장소를 전달하는데 사용되는것이 scope입니다.

    이렇게 된다면 하위에서 Store의 변화가 일어나도 상위 다른 scope로 매칭된곳에서는 영향을 받지 않습니다.

     

    만약 Action이 없고 단순히 State로만 scope를 나누고 싶다?

    가능합니다.

    struct MainState: Equatable {
      var mainTitle: String = "Is Changed Sub Title?"
      var subTitle: String = "SubTitle"
      var bottomTitle: String = "BottomTitle"
    }

    자 이렇게 state 부분만 세개의 title이 있다고 가정해볼께요.

    우리는 이 state를 하나의 Store로 사용하지 않고 3개의 store로 스코핑해 뷰에서 독립적으로 뿌려보겠습니다.

    import ComposableArchitecture
    import SwiftUI
    
    struct MainView: View {
      var store: Store<MainState, MainAction>
      
      var body: some View {
        VStack(spacing: 30) {
          WithViewStore(store.scope(state: \.mainTitle)) { mainTitleViewStore in
            Text(mainTitleViewStore.state)
              .font(.title)
          }
          
          WithViewStore(store.scope(state: \.subTitle)) { subTitleViewStore in
            Text(subTitleViewStore.state)
              .font(.body)
          }
          
          WithViewStore(store.scope(state: \.bottomTitle)) { bottomTitleViewStore in
            Text(bottomTitleViewStore.state)
              .font(.caption)
          }
        }
      }
    }

    이런식으로 WithViewStore에서 전역적으로 사용된 store를 가지고 scope를 각각 잡아줘 해당하는 관심사의 State만을 뽑아줄 수 있습니다.

    그러면 클로저 내 viewStore 인자는 각각의 state를 가지고 지정한 범위의 viewStore가 되는것이죠.

    이러한 Scope 구성은 정답이 있는것이 아니기에 각자 프로젝트에 알맞게 조정을 해보는것이 좋습니다.

     

    마무리

    이렇게 스코핑에 대해 알아봤어요.

    그런데 TCA에서 최근 이렇게 스코핑을 하지 않아도 예전과 같이 예기치 못한 뷰의 에러가 나타나지 않도록 최근 업데이트 되었다고 듣긴 했는데 아직 저도 그 부분을 잘 몰라서...!

    아는 분 있으시다면 코멘트 부탁드립니다ㅎㅎ

    만약 제가 학습해보고 맞다면 수정 및 새 포스팅으로 공유드릴께요🙌

     

    [참고자료]

    https://pointfreeco.github.io/swift-composable-architecture/Store/

     

    ComposableArchitecture - Store

    Initializes a store from an initial state, a reducer, and an environment, and the main thread check is disabled for all interactions with this store.

    pointfreeco.github.io

    'TCA' 카테고리의 다른 글

    TCA - IfLetStore  (0) 2022.10.11
    TCA - Debugging  (2) 2022.10.06
    TCA - pullback  (0) 2022.09.19
    CasePath  (0) 2022.04.06
    Composable Architecture로 랜덤 통신 구현하기  (0) 2021.07.17
Designed by Tistory.