ํ๋กฌ์ฑ์ TCA ์ ์ฉํ๊ธฐ
- #TCA
- #SwiftUI
- #iOS
- #swift
๋ค์ด๊ฐ๋ฉฐ
์๋ ํ์ธ์. ๋ชจ๋ฐ์ผํ์์ iOS์ฑ ๊ฐ๋ฐ์ ๋งก๊ณ ์๋ ์ ๊น๊ธฐ๋ณด์ ๋๋ค.
์ต๊ทผ fromm iOS์ฑ์ ๋ ํฐ ์๋น์ค๋ก์ ์ฑ์ฅ์ ์ํด ๋ฆฌํฉํ ๋ง์ ์งํํ๊ณ ์์ต๋๋ค. ๋ฆฌํฉํ ๋ง์ ์งํํ๋ฉฐ ์ผ๋ถ ํ๋ฉด๋ค์ SwiftUI๋ก์ ๋ณํ์ ์๋ํ๊ณ ์๋๋ฐ์. ๊ทธ๋ฌ๋ฉด์ SwiftUI์ MVVM์ด ์ด์ธ๋ฆฌ์ง ์๋๋ค๋ ๋ด์ฉ๋ค์ ๋ง์ด ์ ํ๊ฒ ๋์๊ณ ๋์์ผ๋ก ์์ฃผ ์ธ๊ธ๋๋ TCA๋ฅผ ๋์ ํด๋ณด๊ธฐ๋ก ํ์์ต๋๋ค. ๊ทธ๋์ ์ด๋ฒ ๊ธ์์๋ ๊ฐ๋จํ๊ฒ TCA๋ฅผ ์ ์ฉํ๋ฉฐ ๋๋์ ๋ค์ ๊ณต์ ํด๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
TCA๋?
์ํ ๊ด๋ฆฌ ๊ธฐ๋ฐ์ ๋จ๋ฐฉํฅ ์ํคํ ์ณ๋ก MVI, ReactorKit๊ณผ ๋น์ทํ ์ํคํ ์ณ์ ๋๋ค.
๋์ํ๋ ๋ชจ์ต์ ์ ๋ง ๊ฐ๋จํ๊ฒ ํํํ๋ฉด ์๋์ ๊ฐ์ต๋๋ค.
- View์์๋ ViewAction์ ํตํด ์คํ ์ด์ Action์ ์ ๋ฌํฉ๋๋ค.
- Action์ ๋ฐ๋ผ Reducer๋ Effect๋ฅผ ๋ฐ์์ํค๊ณ State๋ฅผ ์ ๋ฐ์ดํธํฉ๋๋ค
- Effect๋ ๋ค์ Action์ ๋ง๋ค์ด๋ด๊ณ ์ด ์ก์ ์ ๋ค์ Reducer์ ์ ๋ฌ๋ฉ๋๋ค
- State๋ ViewState๋ก ๋ณํ๋์ด View์ UI๋ฅผ ์ ๋ฐ์ดํธํฉ๋๋ค
- 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
}
}
}
ํ์ฌ ๋์์ ๋ณด๋ฉด ์๋์ ๊ฐ์ด ๋์ํฉ๋๋ค.
- ๋ถ๋ชจ ๋ทฐ์์ ํ๋ฉด ์ง์ ์ ๋ฐ์ดํฐ๋ฅผ ๊ณตํต์ผ๋ก ๊ด๋ฆฌํ๊ณ ์๋ ๊ณณ์์ ๋๋ค์์ ๊ฐ์ ธ์ค๊ณ ์์
- ์์ ๋ทฐ์์ ๋๋ค์์ ๋ฐ๊พธ๊ณ ๊ณตํต์ผ๋ก ๊ด๋ฆฌํ๊ณ ์๋ ๊ณณ์ ์ ๋ฐ์ดํธ ํจ
- ๋ถ๋ชจ ๋ทฐ์์ ํด๋น ์ด๋ฒคํธ๋ฅผ ๊ฐ์งํ์ฌ ๋ฐ๋ ๋ฐฉ์ ๋๋ค์์ ๊ฐ์ ธ์ด
์ ๊ธฐ๋ฅ์ 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๋ก ๋ณํํ๋ ๋ ์์ธํ ๊ณผ์ ๊ณผ ์ด์ ์ ๋ํด์๋ ํ ๋ฒ ๋ ์๊ฐ๋๋ฆด ์ ์๊ธฐ๋ฅผ ๋ฐ๋๋๋ค.