ํ”„๋กฌ์•ฑ์— TCA ์ ์šฉํ•˜๊ธฐ

kibo
  • #TCA
  • #SwiftUI
  • #iOS
  • #swift

๋“ค์–ด๊ฐ€๋ฉฐ

์•ˆ๋…•ํ•˜์„ธ์š”. ๋ชจ๋ฐ”์ผํŒ€์—์„œ iOS์•ฑ ๊ฐœ๋ฐœ์„ ๋งก๊ณ  ์žˆ๋Š” ์ •๊น€๊ธฐ๋ณด์ž…๋‹ˆ๋‹ค.

์ตœ๊ทผ fromm iOS์•ฑ์€ ๋” ํฐ ์„œ๋น„์Šค๋กœ์˜ ์„ฑ์žฅ์„ ์œ„ํ•ด ๋ฆฌํŒฉํ† ๋ง์„ ์ง„ํ–‰ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ๋ฆฌํŒฉํ† ๋ง์„ ์ง„ํ–‰ํ•˜๋ฉฐ ์ผ๋ถ€ ํ™”๋ฉด๋“ค์€ SwiftUI๋กœ์˜ ๋ณ€ํ™˜์„ ์‹œ๋„ํ•˜๊ณ  ์žˆ๋Š”๋ฐ์š”. ๊ทธ๋Ÿฌ๋ฉด์„œ SwiftUI์™€ MVVM์ด ์–ด์šธ๋ฆฌ์ง€ ์•Š๋Š”๋‹ค๋Š” ๋‚ด์šฉ๋“ค์„ ๋งŽ์ด ์ ‘ํ•˜๊ฒŒ ๋˜์—ˆ๊ณ  ๋Œ€์•ˆ์œผ๋กœ ์ž์ฃผ ์–ธ๊ธ‰๋˜๋Š” TCA๋ฅผ ๋„์ž…ํ•ด๋ณด๊ธฐ๋กœ ํ•˜์˜€์Šต๋‹ˆ๋‹ค. ๊ทธ๋ž˜์„œ ์ด๋ฒˆ ๊ธ€์—์„œ๋Š” ๊ฐ„๋‹จํ•˜๊ฒŒ TCA๋ฅผ ์ ์šฉํ•˜๋ฉฐ ๋Š๋‚€์ ๋“ค์„ ๊ณต์œ ํ•ด๋ณด๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

TCA๋ž€?

์ƒํƒœ ๊ด€๋ฆฌ ๊ธฐ๋ฐ˜์˜ ๋‹จ๋ฐฉํ–ฅ ์•„ํ‚คํ…์ณ๋กœ MVI, ReactorKit๊ณผ ๋น„์Šทํ•œ ์•„ํ‚คํ…์ณ์ž…๋‹ˆ๋‹ค.

๋™์ž‘ํ•˜๋Š” ๋ชจ์Šต์„ ์ •๋ง ๊ฐ„๋‹จํ•˜๊ฒŒ ํ‘œํ˜„ํ•˜๋ฉด ์•„๋ž˜์™€ ๊ฐ™์Šต๋‹ˆ๋‹ค.

  1. View์—์„œ๋Š” ViewAction์„ ํ†ตํ•ด ์Šคํ† ์–ด์— Action์„ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค.
  2. Action์— ๋”ฐ๋ผ Reducer๋Š” Effect๋ฅผ ๋ฐœ์ƒ์‹œํ‚ค๊ณ  State๋ฅผ ์—…๋ฐ์ดํŠธํ•ฉ๋‹ˆ๋‹ค
  3. Effect๋Š” ๋‹ค์‹œ Action์„ ๋งŒ๋“ค์–ด๋‚ด๊ณ  ์ด ์•ก์…˜์€ ๋‹ค์‹œ Reducer์— ์ „๋‹ฌ๋ฉ๋‹ˆ๋‹ค
  4. State๋Š” ViewState๋กœ ๋ณ€ํ™˜๋˜์–ด View์˜ UI๋ฅผ ์—…๋ฐ์ดํŠธํ•ฉ๋‹ˆ๋‹ค
  5. 2-4๋ฒˆ ๊ณผ์ •์„ Effect๊ฐ€ ์—†์„ ๋•Œ๊ฐ€์ง€ ๋ฐ˜๋ณตํ•ฉ๋‹ˆ๋‹ค.

๊ธฐ์กด ViewModel

๊ธฐ์กด fromm์—์„œ๋Š” MVVM ๊ตฌ์กฐ์™€ Stateful UI๋ฅผ ํ™œ์šฉํ•˜์—ฌ ์•ฑ์„ ๋งŒ๋“ค์—ˆ์Šต๋‹ˆ๋‹ค. ๊ตฌํ˜„๋œ ViewModel์€ ์•„๋ž˜์™€ ๊ฐ™์Šต๋‹ˆ๋‹ค

protocol ViewModelType {

ย  ย  associatedtype State

ย  ย  associatedtype Event

ย  ย  associatedtype Input

ย  ย  func through(_ input: Input)

ย  ย  var event: PassthroughSubject<Event, Never> { get }

ย    var state: State { get }
}

๊ฐ„๋žตํ•˜๊ฒŒ ์„ค๋ช…์„ ๋“œ๋ฆฌ๋ฉด State๋ฅผ ์œ ์ง€ํ•˜๋ฉฐ throughํ•จ์ˆ˜๋ฅผ ํ†ตํ•ด Input์„ ์ฒ˜๋ฆฌํ•˜์—ฌ State๋ฅผ ๋ฐ”๊พธ์–ด์ฃผ๊ฑฐ๋‚˜ Event๋ฅผ ๋‚ด๋ณด๋‚ด๊ณ  ์žˆ๋Š” ํ˜•ํƒœ์ž…๋‹ˆ๋‹ค.


๊ธฐ์กด ViewModel๊ณผ Store์˜ ์ฐจ์ด์ 

์—ฌ๋Ÿฌ ์ฐจ์ด์ ์ด ์žˆ๊ฒ ์ง€๋งŒ ์˜ค๋Š˜ ์„ค๋ช…๋“œ๋ฆฌ๊ณ  ์‹ถ์€ ๋ถ€๋ถ„์€ Reducer๋Š” ์ž‘์€ Reducer๋“ค์˜ ํ•ฉ์„ฑ์œผ๋กœ ํ‘œํ˜„์ด ๊ฐ€๋Šฅํ•˜์ง€๋งŒ ๊ธฐ์กด ViewModel์€ ์ž‘์€ ViewModel๋“ค๋กœ์˜ ํ•ฉ์„ฑ์ด ๋ถˆ๊ฐ€๋Šฅํ•˜๋‹ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

์•„๋ž˜ ์ฑ„ํŒ…๋ฐฉ ์„ค์ • ์˜ˆ์‹œ๋ฅผ ํ†ตํ•ด์„œ ์ฐจ์ด์ ์„ ์‚ดํŽด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.


์œ„ ๊ธฐ๋Šฅ์€ ํ˜„์žฌ ์•„๋ž˜ ๋ฐฉ์‹๋Œ€๋กœ ๊ตฌํ˜„๋˜์–ด ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.
final class ChatRoomSettingViewModel: ViewModelType {
   struct State {
      let nickname: String
      let roomName: String
   }
   
   ...
   
   func through(_ input: AnyPublisher<Input, Never>) {
      input.sink { [weak self] action in 
         switch action {
         case .viewDidLoad:
            ...
            self.chatManager.currentRoom
               .sink { ... 
                   self.state.nickname = room.nickname // 1 & 3
               }
            ...
         }
      }
   }
}

final class ChatRoomNicknameViewModel: ViewModelType {   
   struct State {
      let nickname: String
   }
   ...
   let updateNicknameUsecase: UpdateNicknameUseCase
   
   func through(_ input: AnyPublisher<Input, Never>) {
      input.sink { [weak self] action in 
         switch action {
		 case .changeNickname(let nickname):
		    self?.updateNickname(nickname)
         }
      }
   }
   private func updateNickname(_ nickname: String) {
      updateNicknameUsecase.execute(nickname)
      .sink { ...
         self.chatMananger.updateNickname(nickname) // 2
      }
   }
}

ํ˜„์žฌ ๋™์ž‘์„ ๋ณด๋ฉด ์•„๋ž˜์™€ ๊ฐ™์ด ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค.

  1. ๋ถ€๋ชจ ๋ทฐ์—์„œ ํ™”๋ฉด ์ง„์ž… ์‹œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ณตํ†ต์œผ๋กœ ๊ด€๋ฆฌํ•˜๊ณ  ์žˆ๋Š” ๊ณณ์—์„œ ๋‹‰๋„ค์ž„์„ ๊ฐ€์ ธ์˜ค๊ณ  ์žˆ์Œ
  2. ์ž์‹ ๋ทฐ์—์„œ ๋‹‰๋„ค์ž„์„ ๋ฐ”๊พธ๊ณ  ๊ณตํ†ต์œผ๋กœ ๊ด€๋ฆฌํ•˜๊ณ  ์žˆ๋Š” ๊ณณ์„ ์—…๋ฐ์ดํŠธ ํ•จ
  3. ๋ถ€๋ชจ ๋ทฐ์—์„œ ํ•ด๋‹น ์ด๋ฒคํŠธ๋ฅผ ๊ฐ์ง€ํ•˜์—ฌ ๋ฐ”๋€ ๋ฐฉ์˜ ๋‹‰๋„ค์ž„์„ ๊ฐ€์ ธ์˜ด

์œ„ ๊ธฐ๋Šฅ์„ TCA๋ฅผ ํ™œ์šฉํ•˜๋ฉด ์•„๋ž˜์™€ ๊ฐ™์ด ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋ฆฌ๋“€์„œ๋ถ€๋ถ„

// ์ฑ„ํŒ…๋ฐฉ ์„ค์ •์˜ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ๊ฐ€์ง€๊ณ  ์žˆ๋Š” ๋ฆฌ๋“€์„œ
public struct ChatRoomSettingFeature: Reducer {
ย  ย public struct State {
ย  ย  ย  public var nickname: NicknameFeature.State(nickname: String)
ย  ย  ย  public var roomName: RoomNameFeature.State(roomName: String)
ย  ย  ย  ...
ย  ย  ย  
ย  ย  ย  public init(chatRoom: ChatRoom) {
ย  ย  ย     nickname = .init(nickname: chatRoom.nickname)
ย  ย  ย     ...
ย  ย  ย  }ย  

ย  ย public enum Action {
ย  ย  ย  case nickname(ChatRoomEditNicknameFeature.Action)
	  ...
ย  ย }

ย  ย public var body: some Reducer<State, Action> {
ย  ย  ย  Reduce { state, action in
ย  ย  ย  ย  ย switch action {
ย  ย  ย  ย  ย default:
ย  ย  ย  ย ย  ย  ย return .none
ย  ย  ย  ย  ย }
ย  ย  ย  }

ย  ย  ย  Scope(state: \.nickname, action: /Action.nickname) {
ย  ย ย  ย  ย  NicknameFeature()
ย  ย  ย  }
	  ...
   }
}

// ์• ์นญ ๋ณ€๊ฒฝ์˜ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ๊ฐ€์ง€๊ณ  ์žˆ๋Š” ๋ฆฌ๋“€์„œ
public struct NicknameFeature: Reducer {
   public struct State {
      var editedNickname: String
      var isChanged: Bool = false
      var nickname: String
      
      public init(nickname: String) {
         self.nickname = nickname
         self.editedNickname = nickname
      }
   }
   public enum Action {
ย  ย  ย  case nicknameChanged(String)
ย  ย  ย  case onAppear
ย  ย  ย  case saveButtonTapped
ย  ย  ย  case setNicknameResponse<TaskResult<SetNicknameResponse>>
   }
   ...
   public var body: some Reducer<State, Action> {
      Reduce { state, action in 
         switch action {
         case let .nicknameChanged(changed):
            state.editedNickname = changed
         ...
         case let .setNicknameResponse(.success(response)):
            state.nickname = state.editedNickname
            state.isChanged = false
            return .none
         ...
         }
      }
   }
}

UIViewController๋ถ€๋ถ„

...
import ComposableArchitecture

// ์ฑ„ํŒ…๋ฐฉ ์„ค์ •์˜ ํ™”๋ฉด์— ํ•ด๋‹นํ•˜๋Š” UIViewController
public final class ChatRoomSettingViewController: UIViewController {
   let store: StoreOf<ChatRoomSettingFeature>
ย  ย let viewStore: ViewStore<ViewState, ViewAction>
ย  ย ...
ย  ย 
ย  ย struct ViewState: Equatable {
ย  ย    ...
ย  ย    init(state: ChatRoomSettingFeature.State) {
ย  ย       // ์—ฌ๊ธฐ์„œ Reducer์˜ Domain๋ชจ๋ธ์—์„œ์˜ ViewModel๋กœ์˜ ์ „ํ™˜์„ ํ•ฉ๋‹ˆ๋‹ค
ย  ย    }
ย  ย }
ย  ย 
ย  ย enum ViewAction {
ย  ย    // ์ด ๋ทฐ์—์„œ ์ž…๋ ฅ๋ฐ›์„ ์•ก์…˜๋“ค์„ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค
ย  ย }
ย  ย 
ย  ย public init(store: StoreOf<ChatRoomSettingFeature>) {
ย  ย    self.store = store
ย  ย    self.viewStore = ViewStore(
ย  ย       store, 
ย  ย       observe: ViewState.init, 
ย  ย       send: ChatRoomSettingFeature.Action.init
ย  ย    )
ย  ย    ...
ย  ย }
}

// ์• ์นญ ๋ณ€๊ฒฝ ํ™”๋ฉด์— ํ•ด๋‹นํ•˜๋Š” UIViewController
public final class NicknameViewController: UIViewController {
   let store: StoreOf<NicknameFeature>
ย  ย // ์œ„์™€ ๋™์ผ
ย  ย ...
}

ChatRoomSettingFeature์˜ State์™€ Action์„ ๋ณด๋ฉด NicknameFeature์˜ State์™€ Action๊ฐ™์€ ๋” ์ž‘์€ ๋ฆฌ๋“€์„œ๋“ค์˜ State์˜ ์กฐํ•ฉ์œผ๋กœ ์ด๋ฃจ์–ด์ง„ ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ธฐ์กด๊ณผ ๋น„๊ตํ•ด ๊ฐ€์žฅ ๋‘๋“œ๋Ÿฌ์ง„ ์ฐจ์ด์ ์œผ๋กœ๋Š” ๊ธฐ์กด์—๋Š” โ€œ์ฑ„ํŒ…๋ฐฉ ์„ค์ •ํ™”๋ฉดโ€์—์„œ ์• ์นญ์„ ์ตœ์‹ ์œผ๋กœ ์œ ์ง€ํ•˜๋Š” ๊ฒƒ์˜ ์ฑ…์ž„์ด ๋ถ€๋ชจ์˜ ViewModel์— ์žˆ์—ˆ๋‹ค๋ฉด ์ง€๊ธˆ์€ โ€œ์ž์‹ Reducer์™€ ์• ์นญ์„ ๊ณต์œ ํ•˜๊ณ  ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ํ•ด๋‹น ์ฑ…์ž„์ด ์‚ฌ๋ผ์ง„ ๊ฒƒโ€์ž…๋‹ˆ๋‹ค.

์ด์ œ ์ฑ„ํŒ…๋ฐฉ ์„ค์ • ๋„๋ฉ”์ธ์—์„œ ์• ์นญ๊ณผ ๊ด€๋ จ๋œ ๋กœ์ง์€ ํ•œ ๊ณณ์—๋งŒ ์œ ์ง€๋˜๊ณ  ์žˆ์Œ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ถ”ํ›„ ์• ์นญ๊ณผ ๊ด€๋ จ๋œ ๋กœ์ง์„ ๋ณ€๊ฒฝํ•˜๊ฑฐ๋‚˜ ์ถ”๊ฐ€ํ•  ๋•Œ ๊ฐ€์ • ์ ํ•ฉํ•œ ๊ณณ์€ ์–ด๋””์ธ๊ฐ€?์— ๋Œ€ํ•œ ์งˆ๋ฌธ์„ ํ•˜๊ฒŒ ๋œ๋‹ค๋ฉด ์‰ฝ๊ฒŒ NicknameFeature๋ผ๊ณ  ์˜ˆ์ธกํ•˜๊ฑฐ๋‚˜ ํŒ€์›๋“ค์˜ ๋™์˜๋ฅผ ์–ป๊ธฐ ์‰ฌ์šธ ๊ฒƒ ๊ฐ™๋‹ค๋Š” ์ƒ๊ฐ์ด ๋“ญ๋‹ˆ๋‹ค.

๋‘ ๊ตฌ์กฐ์˜ ์ฐจ์ด์ ์„ ์ •๋ฆฌํ•˜์ž๋ฉด

  • ์ž์‹๊ณผ ๋ถ€๋ชจ๊ฐ„์— ๋ฐ์ดํ„ฐ ๊ณต์œ ๊ฐ€ ๋ณด๋‹ค ์ˆ˜์›”ํ•ด์กŒ๋‹ค
  • ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ์ด ์ผ์–ด๋‚˜๋Š” ๊ณณ์ด ํ•œ์ •๋˜์—ˆ์œผ๋ฉฐ ์˜ˆ์ธก์ด ์‰ฝ๋‹ค

๋งˆ์น˜๋ฉฐ

์ง€๊ธˆ๊นŒ์ง€ ๊ฐ„๋‹จํ•˜๊ฒŒ ์ €ํฌ ์•ฑ์˜ ๋ฆฌํŒฉํ† ๋ง ๊ณผ์ •์—์„œ ์ ์šฉ๋œ TCA ์˜ˆ์ œ๋ฅผ ํ†ตํ•ด ์ด์ ์— ๋Œ€ํ•ด์„œ ํ™•์ธํ•ด๋ณด์•˜์Šต๋‹ˆ๋‹ค. ๋ถ€์กฑํ•œ ๋‚ด์šฉ์ด๊ธฐ์— PointFree ํ™ˆํŽ˜์ด์ง€์˜ ๊ฐ•์˜๋ฅผ ๋“ฃ๊ฑฐ๋‚˜ Github์— ์ œ๊ณต๋˜๋Š” ์˜ˆ์ œ๋ฅผ ํ†ตํ•ด ๋”์šฑ ์ž์„ธํ•œ ๋‚ด์šฉ์„ ํ™•์ธํ•˜์‹œ๊ธฐ๋ฅผ ๊ถŒ์žฅ๋“œ๋ฆฌ๋Š”๋ฐ”์ž…๋‹ˆ๋‹ค!

์ถ”ํ›„ ๋” ๋งŽ์€ ๊ณณ์— ์ ์šฉํ•˜๊ฒŒ ๋˜๋ฉด ๊ธฐ์กด ์•„ํ‚คํ…์ณ์—์„œ TCA๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ๋” ์ž์„ธํ•œ ๊ณผ์ •๊ณผ ์ด์ ์— ๋Œ€ํ•ด์„œ๋„ ํ•œ ๋ฒˆ ๋” ์†Œ๊ฐœ๋“œ๋ฆด ์ˆ˜ ์žˆ๊ธฐ๋ฅผ ๋ฐ”๋ž๋‹ˆ๋‹ค.

โ† ๋ชฉ๋ก์œผ๋กœ ๋Œ์•„๊ฐ€๊ธฐ

Art Changes Life

๋…ธ๋จธ์Šค์™€ ํ•จ๊ป˜ ์—”ํ„ฐํ…Œํฌ ์‚ฐ์—…์„ ํ˜์‹ ํ•ด๋‚˜๊ฐˆ ๋ฉค๋ฒ„๋ฅผ ์ฐพ์Šต๋‹ˆ๋‹ค.

์ฑ„์šฉ ์ค‘์ธ ๊ณต๊ณ  ๋ณด๊ธฐ