iOS/ReactorKit

[ReactorKit] 리액터킷의 기본 개념

23g 2025. 9. 10.

 

 

안녕하세요 제가 공부하고 있는 리액터킷의 기본 개념을 꼼꼼히 공부하고 이해해보도록해요

 

아래 내용은 리액터킷 깃의 내용을 한글로 제맘대로 적어놓은 것 입니다~

 

이해가 안되는 여러분들에게도 도움이 되면 좋겠네요...

 

모든 내용은 https://github.com/ReactorKit/ReactorKit/blob/master/README.md

 

ReactorKit/README.md at master · ReactorKit/ReactorKit

A library for reactive and unidirectional Swift applications - ReactorKit/ReactorKit

github.com

을 기본으로 합니다!

 

그럼 시작

기본 개념

ReactorKit은 Flux와 Reactive Programming의 조합
사용자 액션과 뷰 상태는 Observable 스트림으로 각 레이어에 전달됨

 

이라는데 

여기서 Observable 스트림이란?

 

어떤 값이 흘러가는 파이브 같은 개념

파이프에 값이 들어가면, 그 값을 구독한 애들이 그 변화를 감지해서 동작함

 

이 스트림은 단방향임:

  • View는 오직 Action만 발생시킬 수 있음
  • Reactor는 오직 State만 방출할 수 있음

여기까지 느낀바는

 

아 그럼 ReactorKit은

- 구독 관계가 있고

- 뭔가 이벤트가 발생하면

- 단방향 스트림에 의해

- 변화를 감지하고

- 어떠한 동작을 하는거구나!!!

 

좀 더 정확히 하자면 "사용자 이벤트가 단방향 스트림을 따라 흘러가면서 상태를 바꾸고, 그 변화를 뷰가 자동으로 반응하는 구조" 인 것


설계 목표

테스트 용이성

: 비즈니스 로직을 뷰와 분리하는 게 가장 큰 목적

Reactor는 View에 의존하지 않음

Reactor와 뷰 바인딩만 테스트하면 됨

 

-> 덕분에 테스트가 쉬워짐

-> 테스트하기 좋은 코드는 좋은 코드~

작게 시작하기

: 앱 전체가 ReactorKit 아키텍처를 따라야 하는 건 아님

일부 화면에만 적용해도 됨

기존 프로젝트도 전체 리팩토링 필요 없음

코드 최소화

: 단순한 기능에 복잡한 코드 필요 없음

ReactorKit은 다른 아키텍처보다 적은 코드로 구현 가능

 

아 글쿤요


View

  • View는 데이터를 보여주는 역할만 함 (예: 뷰컨트롤러, 셀)
  • View는 사용자 입력Action 스트림에 바인딩하고, ReactorStateUI 컴포넌트에 바인딩
    -> 즉, 입력은 Action으로 보내고, 출력은 State에서 받아서 UI에 붙인다
    위에서 봤던 흐름 그림 생각하면 됨
  • 비즈니스 로직 없음 → 오직 Action ↔ State 매핑 정의만 함

뷰 정의 방법
: View 프로토콜 채택 → reactor 프로퍼티 자동 추가

class ProfileViewController: UIViewController, View {
  var disposeBag = DisposeBag()
}

profileViewController.reactor = UserViewReactor() // 이렇게 Reactor 주입

 

reactor가 설정되면 자동으로 bind(reactor:) 호출됨 → 여기서 액션/상태 바인딩 정의하면 됨

func bind(reactor: ProfileViewReactor) {
  // action (View -> Reactor)
  refreshButton.rx.tap.map { Reactor.Action.refresh }
    .bind(to: reactor.action)
    .disposed(by: self.disposeBag)

  // state (Reactor -> View)
  reactor.state.map { $0.isFollowing }
    .bind(to: followButton.rx.isSelected)
    .disposed(by: self.disposeBag)
}

 

이런 코드가 불린다는데...이렇게 정의할 수 있다는데,,,

 

이게 뭔소리냐면

 

func bind(reactor: ProfileViewReactor) {
    // 1️⃣ Action 바인딩 (View → Reactor)
    // refreshButton이 눌리면 tap 이벤트가 발생
    refreshButton.rx.tap
        // 버튼 탭 이벤트를 Reactor가 처리할 Action.refresh로 변환
        .map { Reactor.Action.refresh }
        // 변환된 Action을 Reactor의 action 스트림으로 전달
        .bind(to: reactor.action)
        // disposeBag에 담아 구독 해제 시 메모리 관리
        .disposed(by: self.disposeBag)

    // 2️⃣ State 바인딩 (Reactor → View)
    // Reactor의 상태(State) 스트림 구독
    reactor.state
        // 상태 중 isFollowing 값만 추출
        .map { $0.isFollowing }
        // 추출한 값을 followButton의 isSelected 속성과 연결
        .bind(to: followButton.rx.isSelected)
        // disposeBag에 담아 구독 해제 시 메모리 관리
        .disposed(by: self.disposeBag)
}

 

이렇게 정의할 수 있다 이거임

 

스토리 보드 지원

은 스토리 보드 안쓸거라 생략

 

Reactor

  • Reactor는 뷰 상태를 관리하는 UI 비의존 레이어
  • 모든 로직을 Reactor에 위임하고, View와 분리됨 → 테스트 용이
  • Reactor 정의 시 Reactor 프로토콜 채택 → Action, Mutation, State, initialState 정의 필요

Action → Mutation → State 흐름으로 동작

  • mutate() : Action을 받아서 Mutation 스트림 생성 (비동기 작업, API 호출 수행)
  • reduce() : Mutation + 이전 State → 새로운 State 생성 (순수 함수)
  • transform() : Action, Mutation, State 스트림 변환/결합 가능

그냥 아 글쿤 하세요

class ProfileViewReactor: Reactor {
  // 사용자의 입력/행동을 표현하는 이벤트
  // 유저가 뭔가 했다!!!
  enum Action {
    case refreshFollowingStatus(Int)
    case follow(Int)
  }

  // Action과 State 사이의 다리 (중간 단계)
  // State에 어떤 변화가 일어날지 정의
  enum Mutation {
    case setFollowing(Bool)
  }

  // 화면(View)이 보여줘야 하는 상태 값
  // 지금 화면이 어떤 상태여야 하는가!
  struct State {
    var isFollowing: Bool = false
  }

  let initialState: State = State()
}

 

Reactor는 Action을 받아서 Mutation → State로 바꿔주는 역할을 함

사용자 입력 (Action)
        ↓ mutate()
중간 변화 (Mutation)
        ↓ reduce()
화면 상태 (State)

 

잠만 reduce가 어디서 나왔냐면요

 

이사진 많이 보셨쥬 ㅋ

 

리액터라는 프로토콜에

 

mutate()라는 메서드와 reduce()가 있는거임!!!

 

 

  • mutate
    -> 유저 액션(Action)이 들어오면 → "이 액션 때문에 어떤 변화(Mutation)가 생겨야 하나?"를 결정
    -> 예: .refresh → "로딩 시작" + "유저 목록 불러오기"
  • reduce
    -> Mutation이 생겼으니 → "State를 어떻게 바꿔야 하나?"를 적용
    -> 예: .setLoading(true) → state.isLoading = true

 

그럼 여기까지 내용 코드로 봅시다.

// Action: 사용자가 할 수 있는 입력
enum Action {
    case refresh
}

// Mutation: 상태에 가해질 변화
enum Mutation {
    case setLoading(Bool)
}

// State: 화면이 표현할 상태
struct State {
    var isLoading: Bool = false
}

final class MyReactor: Reactor {
    let initialState = State()

    // Action -> Mutation
    func mutate(action: Action) -> Observable<Mutation> {
        switch action {
        case .refresh:
            return Observable.just(.setLoading(true))
        }
    }

    // Mutation -> State
    func reduce(state: State, mutation: Mutation) -> State {
        var newState = state
        switch mutation {
        case .setLoading(let isLoading):
            newState.isLoading = isLoading
        }
        return newState
    }
}

 

 

이제 왜인지는 묻지마셈

 

왜냐고???

 

이게 ReactorKit의 기본 틀인 것임

 

왜가 아니라 걍 일케 쓰는거래요

 

 

 

여기에 transform() 까지 해야하는데 그건 패쓰하겠삼

 

왜냐면 곧 자리 떠야해서 ㅎㅎ

 

 

댓글