아키텍처의 중요성을 일깨워준 Apple Developer Academy @ POSTECH에서 함께 공부하는 Woody에게 감사합니다!
이전에 애플 개발자 아카데미의 첫 챌린지로, SwiftUI로 저의 두번째 프로젝트를 해보았습니다.
이전에 했던 첫번째 프로젝트는 일주일 동안 SiwftUI를 사용해야 음악 플레이어앱을 만드는 것이었고 그 당시에는 정말 쉽지 않았습니다.
기능적인 부분 때문이기도 하지만, SwiftUI라는 새로운 프레임워크를 처음 다루어본다는 측면에서 낯설음 때문이지 않았나 싶습니다.
제가 SwiftUI 첫번째 프로젝트를 마치고 면접장을 가서 받았던 질문은
"MVC를 쓰는 회사에 MVVM을 도입하자고 설득해야 하는 상황이면, 어떻게 설득할것인지" 였습니다.
정말 간단한 질문이라고 생각했지만, 꼬리질문이 이어지면서 생각보다 MVC와 MVVM 패턴을 생각없이 사용하고 있었다는 것을 깨달았습니다.
이후 공부를 하면서 제가 스스로 던진 질문하나는 이것입니다.
"왜 SwiftUI에서 MVVM을 사용해야 하는가, 더 나은 아키텍처가 있는가"
이 질문은 현재 진행형이지만, ViewModel의 역할이 너무 애매하게 생각되는 와중에 오늘은 MVVM의 대안이 될 수 있는 한가지 아키텍처를 소개하려고 합니다.
Redux는 리액트에서 상태를 더 효율적으로 관리하는 데 사용하는 상태 관리 라이브러리인데, Swift에서는 ReSwift 라이브러리가 현재 사용되고 있습니다. (UIKit에서 사용되는 듯...)
GitHub - ReSwift/ReSwift: Unidirectional Data Flow in Swift - Inspired by Redux
Unidirectional Data Flow in Swift - Inspired by Redux - GitHub - ReSwift/ReSwift: Unidirectional Data Flow in Swift - Inspired by Redux
github.com
그렇다면, Redux 아키텍처는 Swift에서 어떤 특징을 가지고 어떤 장점을 가지길래 MVVM의 대안으로 사용될 수 있는걸까요?
여전히 공부할 길이 멀지만,
Redux 아키텍처의 첫인상은 "그래서 왜 쓰는거야?"에 대한 대답은 아직 속시원하게 할 수 없다는 것입니다.
MVVM가 SwiftUI에서 왜 쓰면 안되는지, 이를테면 불필요하게 한단계 더 과정을 거치고 복잡해진다던지, 이미 선언적 프레임워크이기 때문에 ViewModel이 이미 불필요하다던지... 등등 여러가지를 댈 수 있지만, Redux는 아직 쓰면 안되는 이유를 댈 수 없다는 것에 만족해야 할까요?
현재 아주 기초적인 단계이지만, 써야하는 이유를 생각해보자면, MVVM에서는 어려웠던 복잡성을 줄인 상태에서 State를 관리하는 정도가 될 수 있을 것 같지만 이 부분에 대해서는 직접 사용해보면서 느끼는 수 밖에 없을 것 같습니다.
Redux의 구조는 다음과 같습니다.
저는 정말 생소했는데... 예를 들어, 버튼을 눌러 화면의 숫자가 1씩 올라가는 기능을 만든다고 해보자고요!
먼저 State는 상태값을 저장하는 객체입니다.
이 객체 안에는 var Value: Int = 0 처럼 값이 저장되어있겠죠?
Action은 사용자의 동작이 일어났을 때 참조하는 객체입니다.
사용자가 버튼을 눌렀네? 어떤 동작을 위한 버튼이야? 아.. +1 을 해주는 동작이구나.. 이 동작은 AddValue이라고 지정되어있네!!
Store은 위의 State와 Action이 모두 저장되어 있는 Observable 오브젝트입니다.
그리고 다음에 설명할 Reducer을 가지고 있기도 하죠!!
사용자가 누른 버튼에 대한 Action을 Reducer로 보내서 상태값의 변경을 처리해주는 역할을 합니다.
+1 버튼을 눌렀네? Action에서 "AddValue"라는 액션을 참조하여, Reducer로 해당 액션을 실행하도록 명령해주는 것이죠.
Reducer는 들어오는 액션을 필터링하여, State값을 변경해주는 역할을 합니다.
"AddValue"라는 동작이네? State 값에 +1 해줘야겠다.
그리고 실제로 +1을 해주게 됩니다.
다시 말해서,
User가 View에서 +1 이라는 버튼을 누르게 되면,
해당 Action은 Store 내부에 있는 Dispatch를 통해 Reducer로 가게 됩니다.
Reducer는 해당 Action을 분기처리하고, State값을 변경하는 역할을 합니다.
기존의 0값을 가지던 변수는 +1 이 되겠죠??
이렇게 State 값이 변경되면, View에는 +1 이 된 값이 User에게 보여지게 되는 것입니다.
이를 바탕으로 간단한 코드를 작성해보았습니다.
파일이 많게 느껴질 수도 있지만, 이해하고 나면 생각보다 간단한 로직으로 돌아간다고 느끼실 수 있을거에요
AppState.swift
//
// AppState.swift
// Redux
//
// Created by Noah Park on 2022/04/17.
//
import Foundation
struct AppState {
var currentNum: Int = 0
}
AppStore.swift
//
// AppStore.swift
// Redux
//
// Created by Noah Park on 2022/04/17.
//
import Foundation
typealias AppStore = Store<AppState, AppAction>
final class Store<State, Action>: ObservableObject{
@Published private(set) var state: State
init(state: State, reducer: @escaping Reducer<State, Action>) {
self.state = state
self.reducer = reducer
}
func dispatch(action: Action){
reducer(&self.state, action)
}
}
AppAction.swift
//
// AppAction.swift
// Redux
//
// Created by Noah Park on 2022/04/17.
//
import Foundation
enum AppAction: String {
case addValue
}
Reducer.swift
//
// Reducer.swift
// Redux
//
// Created by Noah Park on 2022/04/17.
//
import Foundation
typealias Reducer<State, Action> = (inout State, Action) -> Void
func appReducer(_ state: inout AppState, _ action: AppAction) -> Void {
switch action {
case .addValue:
state.currentNum += 1
}
}
ContentView.swift
//
// ContentView.swift
// Redux
//
// Created by Noah Park on 2022/04/17.
//
import SwiftUI
struct ContentView: View {
let store = AppStore(state: AppState.init(currentNum: 0), reducer: appReducer(_:_:))
var body: some View {
DeviceView()
.environmentObject(store)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
NumView.swift
//
// NumView.swift
// Redux
//
// Created by Noah Park on 2022/04/17.
//
import SwiftUI
struct NumView: View {
@EnvironmentObject var store : AppStore
func addValue() {
self.store.dispatch(action: .addValue)
}
var body: some View {
VStack {
Text("\(self.store.state.currentNum)")
.font(.system(size: 300, weight: .bold, design: .monospaced))
Button(action: {
self.addValue()
}, label: {
Text("+1")
.fontWeight(.bold)
})
}
}
}
struct NumView_Previews: PreviewProvider {
static var previews: some View {
let store = AppStore(state: AppState.init(currentNum: 0), reducer: appReducer(_:_:))
NumView()
.environmentObject(store)
}
}
'Swift 아키텍처' 카테고리의 다른 글
[ReactorKit] (2) Reactorkit 내부 구조를 뜯어보고 사용하자 (0) | 2024.03.03 |
---|---|
[RIBs] RIBs 분석 (0) | 2024.01.08 |
[Reactorkit] (1) Reactorkit 도입기와 여전히 남은 고민들 (0) | 2023.08.06 |
[MVC] iOS의 MVC 아키텍처에 대해서 (0) | 2022.09.06 |
[Redux] Redux를 이용한 상태관리 후기 (with SwiftUI) (2) | 2022.05.15 |