[리팩토링] 03. 구조개선 - App진입점을 TCA로 컨버팅

2026. 1. 15. 23:02·탭탭 - TapTap/리팩토링
728x90

컨버팅 필요성?

기존 탭탭 앱 진입점은 TCA 구조가 적용되어 있지 않은 일반적인 SwiftUI 방식으로 구현되어 있었습니다. 예를 들어, WindowGroup 안에서 launchState에 따라 SplashView, 온보딩, 홈 화면을 swift로 분기하고, DispatchQueue나 .alert를 직접 사용하여 상태를 관리하는 식이었습니다.

 

이 방식 자체가 성능상 문제를 일으키거나 기능상의 제약이 있는 것은 아니었지만, 앱 전체 구조의 일관성이 떨어지고 상태 관리가 흩어져 유지보수가 어려워진다는 단점이 있었습니다. 그래서 앱 진입점도 TCA 기반으로 통일하여 상태와 이벤트를 일관되게 관리할 수 있도록 리팩토링하게 되었습니다.

 

AppFeature 구현

먼저, 앱 진입점에서 사용할 Reducer를 구현해야 했습니다. 기존 코드에서는 앱 버전 체크나, UserDefault 같은 비즈니스 로직이 View 안에 작성되어 있었고, 이를 통해 온보딩 진행 여부를 판단했습니다.

 

TCA에서는 이런 비즈니스 로직과 사이드 이펙트는 Reducer에서 발생시키고, 실제 구현은 Dependency(클라이언트)로 분리하는 것이 권장됩니다. 그래서 앱 버전 체크와 UserDefault 관련 로직을 클라이언트로 구현하고, Reducer에서는 이를 호출하여 상태를 업데이트하도록 구조를 개선했습니다.

@Reducer
struct AppFeature {
  @ObservableState
  struct State: Equatable {
    var launchState: LaunchState = .splash
    var onboarding: OnboardingFeature.State?
    
    init() {}
    
    @Presents var alert: AlertState<Action.Alert>?
    enum LaunchState: Equatable {
      case splash
      case onboarding
      case home
    }
  }
  
  enum Action: Equatable {
    case onAppear
    case checkAppVersion
    case updateCheckResult(Bool)
    case openAppStore
    case splashFinished
    case onboarding(OnboardingFeature.Action)
    case alert(PresentationAction<Alert>)
     
     @CasePathable
     enum Alert: Equatable {
       case openAppStore
     }
  }
  
  @Dependency(\.userDefaultsClient) var userDefaultsClient
  @Dependency(\.appVersionCheckClient) var appVersionCheckClient
  
  var body: some ReducerOf<Self> {
    Reduce { state, action in
      switch action {
      case .onAppear:
        return .run { send in
          try await Task.sleep(for: .seconds(1.55))
          await send(.checkAppVersion)
        }
        
      case .checkAppVersion:
        return .run { send in
          do {
            let needUpdate = try await appVersionCheckClient.isUpdateAvailable()
            await send(.updateCheckResult(needUpdate))
          } catch {
            await send(.updateCheckResult(false))
          }
        }
      
      case .updateCheckResult(let result):
        if result {
          state.alert = AlertState { 
            TextState("업데이트 필요")
          } actions: {
            ButtonState(action: .openAppStore) {
              TextState("업데이트하기")
            }
          } message: {
            TextState("새로운 버전이 있습니다.\n업데이트 후 다시 이용해주세요.")
          }
        } else {
          return .send(.splashFinished)
        }
        return .none
        
      case .openAppStore:
        return .none
        
      case .splashFinished:
        let hasCompletedOnboarding = userDefaultsClient.loadOnboardingState()
        
        if hasCompletedOnboarding {
          state.launchState = .home
          state.onboarding = nil
        } else {
          state.launchState = .onboarding
          state.onboarding = OnboardingFeature.State()
        }
        return .none
      
      case .alert(.presented(.openAppStore)):
        return .run { _ in
          await appVersionCheckClient.openAppStore()
        }
        
      case .onboarding(.delegate(.onboardingCompleted)):
        state.onboarding = nil
        state.launchState = .home
        return .none
        
      case .onboarding:
        return .none
        
      case .alert:
        return .none
      }
    }
    .ifLet(\.$alert, action: \.alert)
    .ifLet(\.onboarding, action: \.onboarding) {
      OnboardingFeature()
    }
  }
}

 

개선 후

기존 코드에서는 View에서 비즈니스 로직까지 처리하고 있고, 앱 전체 구조의 일관성이 떨어지는 문제가 있었습니다. 구조를 개선하면서 View에서는 UI와 관련된 로직만 담당하도록 하고, 비즈니스 로직은 Reducer와 클라이언트로 분리하여 책임을 명확히 구분했습니다. 이 덕분에 코드의 가독성이 올라가고, 상태 관리와 이벤트 처리를 TCA 방식에 맞춰 일관성 있게 구성할 수 있었습니다.

 

또한, 이번 구조 개선은 단위 테스팅 측면에서도 큰 장점이 있었습니다. 기존 코드에서는 View 안에 앱 버전 체크나 UserDefault 조회 같은 비즈니스 로직이 섞여 있어, 테스트 시 실제 앱 상태나 환경에 의존해야 했기 때문에 단위 테스트를 작성하기 어려웠습니다.

 

반면, 리팩토링을 진행하면서 비즈니스 로직을 클라이언트로 분리했기 때문에, 테스트 시 위처럼 해당 클라이언트를 통해 독립적으로 단위 테스팅을 수행할 수 있게 되었습니다. 

 

'탭탭 - TapTap > 리팩토링' 카테고리의 다른 글

[리팩토링] 01. TapTap 리팩토링을 시작하다. - 도식화 및 문서화  (1) 2026.01.05
'탭탭 - TapTap/리팩토링' 카테고리의 다른 글
  • [리팩토링] 01. TapTap 리팩토링을 시작하다. - 도식화 및 문서화
여성일
여성일
  • 여성일
    성일노트
    여성일
  • 전체
    오늘
    어제
    • 분류 전체보기 N
      • 탭탭 - TapTap
        • 리팩토링
        • 트러블슈팅
        • 개발일지
      • 애플 디벨로퍼 아카데미
        • 챌린지 회고
        • 하루의 날씨
      • Swift Student Challenge 202..
      • AI를 잘쓰는 개발자가 될래요
      • 우리 같이 협업하자
      • ToyProject - 사카마카 (살까말까 고민 ..
      • ToyProject - Book2OnNon (모바..
      • ToyProject - 바꿔조 (환율 계산기)
      • iOS N
        • iOS
        • Vapor
        • Design Pattern
        • CoreData
        • Tuist
        • RxSwift
        • ReactorKit
        • TCA N
      • Swift
        • Swift 기본기
        • UIkit
        • SwiftUI
      • 원티드 프리온보딩 챌린지 iOS 과정
  • 블로그 메뉴

    • 링크

    • 공지사항

    • 인기 글

    • 태그

    • 최근 댓글

    • 최근 글

    • hELLO· Designed By정상우.v4.10.6
    여성일
    [리팩토링] 03. 구조개선 - App진입점을 TCA로 컨버팅
    상단으로

    티스토리툴바