ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • TCA - Timer
    TCA 2022. 10. 13. 11:18

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

    이번 포스팅에서는 TCA의 Effect 타입에서 사용할 수 있는 Timer에 대해 알아보겠습니다🙋🏻

     

    우선 TCA에서 Effect 타입에 대해서는 알고 계시다는 전제에서 해당 Timer를 알아보겠습니다.

    만약 Effect가 어떤건지 아예 모르신다면 아래 간단한 TCA 소개를 보고 오는것을 추천드려요!

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

     

    Composable Architecture

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

    green1229.tistory.com

    간단히 정리하면 리듀서에서 Action과 Environment를 통해 어떠한 로직을 처리하고 말 그대로 반환 효과를 주는것입니다.

    이 반환 타입은 다른 액션으로 이어질 수도 있고 아무것도 없이 종료될 수도 있습니다.

    결국 리듀서에서 일을 처리하고 리턴해줘야할 타입이 Effect입니다.

    Timer는 여러분이 예상하는것과 동일한 기능을 할거에요ㅎㅎ

     

    그럼 한번 본격적으로 Timer를 파보죠!

     

    Timer?

    Timer는 기본적으로 Combine과 그를 상속받아 만들어진 CombineSchedulers를 임포트 합니다.

    import Combine
    import CombineSchedulers

    스케쥴링과 관련이 있다는게 확 느껴지죠.

    Timer는 지정된 스케쥴러의 현재 시간을 지정된 스케쥴러에서 반복적으로 방출하는 효과 즉 Effect를 리턴합니다.

    물론 Foundation의 Timer.publish(every:tolerance:on:in:options:) API를 사용할 수 있지만 TCA에서는 Timer를 생성하는것을 권장하지 않아요.

    그 이유는 해당 API는 실행 루프에 타이머를 만들기에 테스트를 작성할 때 명시적일 필요가 있습니다.

    또한 기능에서 효과가 어떻게 거쳐가는지 보기 위해 해당 타이머가 끝날때까지 기다려야 합니다.

    TCA에서는 시간 기반 이펙트를 테스트 합니다.

    그렇기에 TestScheduler는 시간을 명시적 또는 즉시 앞당길 수 있기에 효과를 어떻게 방출하는지 확인할 수 있습니다.

    그러나 Timer.publish는 구체적인 런루프를 사용하기에 테스트 중 TestScheduler를 대체할 수 없어요.

    이러한 이유들로 PointFree에서는 Effect.timer라는것을 만들어 제공하고 있습니다.

    이는 DispatchQueue, RunLoop에서 모두 사용 가능합니다.

    Timer는 위에서 잠깐도 말했다 시피 Effect의 extension입니다.

    extension Effect where Failure == Never

     

    그럼 Timer가 무엇인지 개념적으로 확인했고 어디 소속인지 밝혔으니 정의를 보겠습니다!

     

    Timer 정의

    extension Effect where Failure == Never {
      public static func timer<S: Scheduler>(
        id: AnyHashable,
        every interval: S.SchedulerTimeType.Stride,
        tolerance: S.SchedulerTimeType.Stride? = nil,
        on scheduler: S,
        options: S.SchedulerOptions? = nil
      ) -> Self where S.SchedulerTimeType == Output {
    
        Publishers.Timer(every: interval, tolerance: tolerance, scheduler: scheduler, options: options)
          .autoconnect()
          .setFailureType(to: Failure.self)
          .eraseToEffect()
          .cancellable(id: id, cancelInFlight: true)
      }
      
        public static func timer<S: Scheduler>(
        id: Any.Type,
        every interval: S.SchedulerTimeType.Stride,
        tolerance: S.SchedulerTimeType.Stride? = nil,
        on scheduler: S,
        options: S.SchedulerOptions? = nil
      ) -> Self where S.SchedulerTimeType == Output {
        self.timer(
          id: ObjectIdentifier(id),
          every: interval,
          tolerance: tolerance,
          on: scheduler,
          options: options
        )
      }
    }

    보시면 두 static 메서드가 있습니다.

    위가 기본 구현 아래는 위의 구현을 self로 호출합니다.

    차이는 id 파라미터에 있어요.

    id는 Hashable한 객체를 받습니다.

    아래는 Any 즉 아무 타입이나 받을 수 있지만 Hashabl해야 하기에 ObjectIdentifier에 감싸서 self.timer를 호출합니다.

    즉 뭐 구현은 같아요!

    내부에서는 Combine에서 익숙한 Publisher의 Timer를 사용해주고 Effect 타입으로 변환해주는게 다네요.

    interval로 시간 타이머 간격을 주고 on 레이블이 달린 scheduler는 어디서 스케쥴링을 돌지 넣어줍니다.

    options으로 SchedulerOptions을 줄 수 있습니다.

     

    정의는 이정도면 충분할것 같고 어떻게 사용하는지 보시죠🕺🏻

     

    Timer 구현

    Timer는 Reducer에서 Effect 방출에 사용할 수 있습니다.

    해당 Effect가 방출되는 시점의 타이머를 걸어줄 수 있는것이죠.

    아래와 같이 보통 사용됩니다.

    struct AppState {
      var count = 0
    }
    
    enum AppAction {
      case startButtonTapped, stopButtonTapped, timerTicked
    }
    
    struct AppEnvironment {
      var mainQueue: AnySchedulerOf<DispatchQueue>
    }
    
    let appReducer = Reducer<AppState, AppAction, AppEnvironment> { state, action, env in
      struct TimerId: Hashable {}
      
      switch action {
      case .startButtonTapped:
        return Effect.timer(id: TimerId(), every: 1, on: env.mainQueue)
          .map { _ in .timerTicked }
        
      case .stopButtonTapped:
        return .cancel(id: TimerId())
        
      case .timerTicked:
        state.count += 1
        return .none
      }
    }

    보시면 startButtonTapped 액션이 들어오면 Reducer는 timerTicked 액션을 방출하는 역할을 합니다.

    그런데 이 timerTicked 액션 방출의 시간을 1초 지연하고 지속적으로 방출하기 위해 위와 같이 Effect.timer를 이용해 사용합니다.

    그 후 map 오퍼레이터를 이용해 timerTicked Action을 방출해주면 끝입니다.

    보시면 TimerId라는 Hashable한 객체를 만들어 사용하고 있습니다.

    그렇기에 해당 타이머를 종료시키기 위해 stopButtonTapped 액션에서 해당 객체를 이용해 취소시켜주고 있습니다.

    그럼 해당 기능이 어떻게 동작하는지 보시죠!

    start를 누르면 1초 뒤 1초 간격으로 계속 액션이 방출되어 숫자가 올라갑니다.

    도중 stop을 누르면 해제되어 멈추죠.

    이렇게 타이머를 사용할 수 있습니다.

    문자 인증 타이머에도 많이 사용될 수 있겠죠?

     

    마무리

    이렇게 Timer를 알아봤습니다!

    다들 회원가입을 위해 문자 인증은 많이 하기에 실시간으로 타이머를 걸어 체크하는 로직들에서 효과 방출을 위해 사용하면 좋아보여요!

    여담이지만 해당 Timer를 까보면서 사용 예시에서 case let의 잘못된 키워드를 수정하여 ComposableArchitecture에 기여했습니다ㅋㅋㅋ

    이런거 찾을때 늘 재밌네요🙌

    https://github.com/pointfreeco/swift-composable-architecture/pull/1483#pullrequestreview-1139933431

     

    Remove not used let keyword in Timer Use Example comment. by GREENOVER · Pull Request #1483 · pointfreeco/swift-composable-arc

    Removed the unused let keyword from the Timer Use Example comment.

    github.com

     

    [참고자료]

    https://github.com/pointfreeco/swift-composable-architecture/blob/main/Sources/ComposableArchitecture/Effects/Publisher/Timer.swift

     

    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. - GitHub - pointfreeco/swift-composable-architecture: A library for bu...

    github.com

    'TCA' 카테고리의 다른 글

    TCA - Throttling  (2) 2022.10.20
    TCA - Debouncing  (0) 2022.10.17
    TCA - IfLetStore  (0) 2022.10.11
    TCA - Debugging  (2) 2022.10.06
    TCA - Scope  (7) 2022.09.26
Designed by Tistory.