[iOS] ReactorKit을 Combine으로 구현하는 법

2026. 1. 18. 18:00·앱/Swift

회사서비스 특성상, 서드파티 라이브러리를 사용할수 없기 때문에 아키텍쳐가 조금 난잡한 경향이 있어서, 리액터킷을 도입하기 위해서

combine으로 리액터킷 프레임워크 만들어봤습니다. 

 

pulse 구현 

state로 구현을 하고 싶었으나, 구현에 시간이 조금 부족해서 그냥 바로 publisher를 방출하는 pulse로 구현하였습니다.

@propertyWrapper
final class Pulse<Value> {
    private let subject: CurrentValueSubject<Value, Never>
    
    var wrappedValue: Value {
        get { subject.value }
        set { subject.send(newValue) }
    }
    
    /// 외부에서 Combine 구독 할수 있도록
    var projectedValue: AnyPublisher<Value, Never> {
        subject.eraseToAnyPublisher()
    }
    
    init(wrappedValue: Value) {
        self.subject = CurrentValueSubject(wrappedValue)
    }
}

 

 

Reactor Protocol 구현

//
//  Reactor.swift
//  ShopLiveStreamerSDK
//
//  Created by yong C on 7/21/25.
//  Copyright © 2025 com.app. All rights reserved.
//

import Foundation
import Combine

protocol ReactorProtocol {
    associatedtype Action
    associatedtype Mutation

    func mutate(action: Action) -> Effect<Mutation>
    func reduce(mutation: Mutation)
}

class Reactor<A, M>: ReactorProtocol {
    typealias Action = A
    typealias Mutation = M
    
    let action = PassthroughSubject<Action, Never>()
    var cancellables = Set<AnyCancellable>()
    
    init() {
        action.flatMap { [weak self] action -> AnyPublisher<Mutation, Never> in
            guard let self = self else { return Empty().eraseToAnyPublisher() }
            return self.mutate(action: action).publisher
        }
        .sink { [weak self] mutation in
            self?.reduce(mutation: mutation)
        }
        .store(in: &cancellables)
    }
    
    func mutate(action: Action) -> Effect<Mutation> { return .empty() }
    
    func reduce(mutation: Mutation) {}
}

Effect 구현

struct Effect<M> {
    let publisher: AnyPublisher<M, Never>
}

extension Effect {
    
    // ✅ 단일 Mutation을 바로 Effect로
    static func just(_ mutation: M) -> Effect {
        Effect(publisher: Just(mutation).eraseToAnyPublisher())
    }

    // ✅ 외부 Publisher를 Effect로 감싸기
    static func from(_ pub: some Publisher<M, Never>) -> Effect {
        Effect(publisher: pub.eraseToAnyPublisher())
    }

    // ✅ 아무 값도 방출하지 않는 Effect
    static func empty() -> Effect {
        Effect(publisher: Empty().eraseToAnyPublisher())
    }

    // ✅ 여러 Effect를 순차적으로 이어붙이기
    static func concat(_ effects: [Effect]) -> Effect {
        let combined = effects.map(\\.publisher)
            .reduce(Empty().eraseToAnyPublisher()) { acc, next in
                acc.append(next).eraseToAnyPublisher()
            }
        return Effect(publisher: combined)
    }

    // ✅ 여러 Effect를 병렬로 실행해서 머지
    static func merge(_ effects: [Effect]) -> Effect {
        Effect(publisher: Publishers.MergeMany(effects.map(\\.publisher)).eraseToAnyPublisher())
    }
}

 

reactorkit에서 action -> mutation -> State로 가는 동작을 구현하기 위해서 Effect라는 형태를 추가하여 

Action -> Mutation -> Effect -> State변화 를 통해서 구현

Effect는 TCA에서 차용하여 고안하였습니다. 

 

Reactor 실제 사용부

 enum Action {
        case viewDidLoad
}
enum Mutation { 
	case updateSomething(value)
}
@Pulse var value: String?

override func mutate(action: Action) -> Effect<Mutation> {
  switch action {
  case .viewDidLoad:
      let onBoardingState = UserDefaults.standard.bool(forKey: "didShowStreamOnboarding") == false
      let onBoardingStateMutation: Effect<Mutation> = onBoardingState ? .just(.updateOnboardingState(true)) : .empty()
      let updateCurrentDeviceID = dependency.updateCurrentDeviceID()
          .map { _ in Mutation.empty }
          .replaceError(with: .empty)
      
      return .concat([
          .just(.updateNeedToLockOrientation(.portrait, .portrait)),
          .just(.updateAudioLevelMeterState(.arrangeLayers)),
          .just(.updateSetUpInitialStream),
          setCamera(),
          getWebViewPlayerURL(),
          onBoardingStateMutation,
          .callAPI(updateCurrentDeviceID)
      ])

 

이때

  var updateCurrentDeviceID: (
  ) -> AnyPublisher<ShopLiveBaseResponse, ShopLiveError>
 

이렇게 퍼블리셔로 들어오는 값은, .callAPI로 감싸주면 된다.

실제로 여러개의 publisher가 연속적으로 호출되거나, 성공, 혹은 실패 케이스를 처리하려면 해당부분에서 처리해주면됨,

 

에러 케이스 구현 

  .catch({ [weak self] error in
                        guard let self else { return Just([Mutation.empty]) }
                        let result: [Mutation] = [
                            [handleAPIError(error: error)],
                            handleCampaginAvailabilityError(error)
                        ].flatMap { $0 }
                        return Just(result)
                    })

 

캐치에서 에러를 받아, Mutation으로 변경 하여 Publisher로 방출

 

ViewController 사용부

init(
    reactor: SRTStreamReactor
) {
    super.init(nibName: nil, bundle: nil)
    defer {
        self.reactor = reactor
        bind(reactor: reactor)
    }
}

바인딩

func bind(reactor: SRTStreamReactor) {
    bindAction(to: reactor)
    bindState(from: reactor)
}

func bindState(from reactor: SRTStreamReactor) {
    reactor.$setUpInitialStream
        .receive(on: DispatchQueue.main)
        .compactMap { $0 }
        .sink { [weak self] _ in
            guard let self else { return }
            onSetUpInitialStream()
        }
        .store(in: &cancellables)
    
    reactor.$streamingState
        .receive(on: DispatchQueue.main)
        .sink { [weak self] state in
            guard let self else { return }
            self.updateStreamingState(state)
        }
        .store(in: &cancellables)
    
    reactor.$webViewURL
        .receive(on: DispatchQueue.main)
        .sink { [weak self] url in
            guard let self else { return }
            self.loadWebView(url)
        }
        .store(in: &cancellables)

 

네트워크

네트워크 주입부는, https://matdongsane.tistory.com/208 참고

반응형
저작자표시 비영리 변경금지 (새창열림)

'앱 > Swift' 카테고리의 다른 글

[Swift] GCD와 swift concurrency가 나오게된 배경과, concurrency 기본개념  (1) 2026.01.19
[iOS] moya를 combine을 이용해서 구현해보자  (0) 2026.01.18
Reactorkit의 Pulse 구현부에 대해서  (0) 2025.02.18
Moya Mock Data 사용하기(feat. test Double)  (0) 2025.01.31
reactorkit에서 testcode 작성 하는법(feat. nimble)  (0) 2025.01.31
'앱/Swift' 카테고리의 다른 글
  • [Swift] GCD와 swift concurrency가 나오게된 배경과, concurrency 기본개념
  • [iOS] moya를 combine을 이용해서 구현해보자
  • Reactorkit의 Pulse 구현부에 대해서
  • Moya Mock Data 사용하기(feat. test Double)
진ddang
진ddang
안녕하세요 진땅의 개발자 블로그 입니다. 피드백은 환영입니다. 깃헙 : https://github.com/it794613
    반응형
  • 진ddang
    맛동산이
    진ddang
  • 전체
    오늘
    어제
    • 분류 전체보기 (202)
      • 일기 그리고 목표 (1)
      • 웹 (20)
        • 리액트 (19)
      • 앱 (118)
        • Swift 문법 (15)
        • Swift (68)
        • SwiftUI (32)
        • 리액트 네이티브 (3)
      • CS (30)
        • 컴퓨터그래픽스 (8)
        • 운영체제 (6)
        • 네트워크 (16)
      • 알고리즘 (13)
        • 백준 (12)
        • 프로그래머스 (1)
      • 대외활동 (7)
        • ict한이음(2022.04) (2)
        • 멋쟁이 사자처럼 (5)
        • Apple Developer Academy (0)
      • 다양한 내용들 (8)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    위젯킷
    widgetkit
    웹
    리액트
    네트워크
    컴퓨터그래픽스
    swift concurrency
    Protocol
    문법
    cs
    영남대
    c++
    스유
    spritekit
    대외활동
    Swift
    composable architecture
    widget
    알고리즘
    SwiftUI
    TCA
    백준
    멋사
    후기
    dispatchqueue
    스위프트
    ReactorKit
    uikit
    운영체제
    멋쟁이사자처럼
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
진ddang
[iOS] ReactorKit을 Combine으로 구현하는 법
상단으로

티스토리툴바