2022년 4월 21일 • ☕️ 6 min read
스유에서 길고 복잡한 뷰를 작성할 때 가독성을 높이기 위한 방법으로 무엇이 있을까?
body내에 코드들을 커스텀뷰로 만들어 메인 뷰단을 이쁘게 만드는 방법이 있을 수 있다. 하지만 이는 데이터 흐름이 복잡해진다는 단점이 있다. 그렇다면 이 데이터 흐름을 간단하게 만들면서 가독성도 높일 수 있지 않을까? 이 글에 커스텀 뷰에서 관리하는 데이터들을 한 곳에 모아 간단한 데이터 흐름을 만드는 내용을 담아보려 한다. (필자의 생각들은 셀프필터링 필요)
먼저 스유의 데이터 흐름을 알아보자.
user에 의하여 action이 발생하고 이에 의해 변화가 일어난다. action에 의해 데이터가 변경되면 View의 State가 변경되며 해당 State에 연결된 View를 새로 그린다.
이처럼 스유는 UI Interface인 View와 앱에 보여질 Model들을 연결(바인딩)하여 관리한다. 이를 도와주는 키워드들로 State
, Binding
등이 있다.
두 키워드들을 간단히 살펴보면, 스유에선 struct
로 이루어진 view
내부의 값을 바꾸기 위해서 @State
가 필요하다. 그리고 @Binding
을 이용해 State나 observable object로 선언된 데이터의 레퍼런스를 공유함으로서 바인딩이 이루어진다. 더 자세한 역할을 알고 싶다면 공식 문서를 읽어보자.
아래는 model을 view에 연결하는 간단한 예시이다.
struct ContentView: View {
@State var text: String = ""
var body: some View {
VStack {
Text(text)
.padding()
ChildView(text: $text)
}
}
}
struct ChildView: View {
@Binding var text: String
var body: some View {
Button {
text = "Hello"
} label: {
Text("Hello 나타나게하기")
.padding()
}
}
}
ContentView
의 text와 ChildView
의 text는 Binding으로 인해 같은 레퍼런스를 공유하고 있다. 그래서 ChildView
에 위치한 버튼을 누르게 되면, 데이터가 변경되고 연결된 뷰의 상태가 업데이트되며 뷰가 새롭게 그려진다.
스유로 복잡한 뷰를 작성하게 되면 View 내의 코드가 어마어마하게 길어진다. 필자는 오늘의 집과 같이 컨텐츠가 많은 복잡한 화면을 만들어보진 않았지만 스크롤이 없는 간단한 화면이어도 길어진 코드때문에 가독성이 떨어지는 것을 경험헀다. 여기서 가독성이란, 코드가 한 눈에 들어오지 않을 뿐만 아니라 해당 뷰가 어떤 데이터를 가지고 있고 어떤 액션을 취하고 있는 지 등을 확인하기 어려움을 의미한다.
그렇다면 위와 같이 긴 코드를 어떻게 가독성을 좋게 만들까?
먼저, 커스텀뷰로 쪼개는 방식을 생각할 수 있다. 하지만 커스텀뷰를 계속 만들다보면 메인뷰단에서 데이터의 흐름이 복잡해질 수 있다.
메인 뷰안에 ChildView1과
ChildView2가
존재하고, ChildView
안에 CardView가
들어가는 경우 경우를 생각해보자.
데이터를 바인딩시켜주기 위하여 메인 뷰단에서 @State
를 선언하고 @Binding
을 이용하여 ChildView를
거쳐 CardView로
전달하여야 한다.
UI요소들이 점점 추가될수록 이러한 데이터 흐름은 더욱 복잡해질 것이다.
여기까지 전부 서론이었고 여기서부터가 이 글에서 하고 싶은 이야기이다.
스유는 View를 State로 관리한다. 데이터가 변경되면 State가 변경되고, 바인딩된 View를 다시 그려준다. 이 개념을 생각하면 View의 State를 한 곳에서 관리하고 EnvironmentObject
를 통해 꺼내 쓰게 된다면 View가 가지고 있는 데이터들을 없애고 데이터 흐름을 모든 State를 관리하는 곳 -> 여러개의 View로 생각해볼 수 있다.
@EnvironmentObject var store: AppStore
위 코드의 store
가 앱의 모든 상태(데이터, bool값 등)을 가지고 있다고 생각하자. 만일 뷰에서 데이터가 필요하다면 위 코드 한줄을 작성하여 store
에서 꺼내서 사용하면 된다. 얼마나 편리한가.
하지만 이렇게 한곳에서 모든 State를 관리하기 위해서는 데이터 흐름이 중요하다. 만일 어느 곳에서나 State에 접근해서 변경하고 뷰에 바인딩할 수 있다고 해보자. 그렇다면 한 개의 State가 여러개의 뷰를 업데이트시키고, 한 개의 뷰가 여러개의 State를 조작한다면 데이터 흐름을 이해하기 어려워진다. 버그는 찾기 어려워지고 데이터 흐름을 이해하는데 시간을 많이 투자해야한다. 이는 UIKit의 대표적인 MVC 패턴에서도 찾아 볼 수 있는 문제이다.
위 그림은 흐름을 이해하기 어려운 상황을 개선하고자 만들어진 “단방향 데이터 흐름”이다. View는 바로 데이터를 변경하지 않고 Action을 넘겨준다. 이 때 action은 반드시 dispatcher를 지나야 하고 데이터 변경이 일어나면 View는 변경된 데이터를 Store를 통해서 받는다. 이 흐름을 통해 예측 불가능한 사이드 이펙트를 줄일 수 있다.
이 데이터 흐름은 2014년 페이스북이 고안해낸 FLux라는 아키텍처 패턴이다. 2015년 Dan Abramov가 ‘reducer’라는 개념과 함께 React에 적용하여 Redux(= React + Flux)가 등장하였다.
그래서 이 Redux 개념은 스유에 적합하지 않을 수 있지만, 코드의 가독성을 높이기 위해 적용해보았다. 먼저 간단한 용어를 정리하고 redux의 단방향 데이터 흐름을 살펴보자.
struct MilgoState {
var homeAppState: AppState = .none
// ...
}
enum MilgoAction {
case showPopup(Program)
}
typealias Reducer<State, Action> = (State, Action) -> State
class Store<State, Action>: ObservableObject {
@Published private(set) var state: State
private let reducer: Reducer<State, Action> // 읽기 전용. 외부에서 set 불가능
private let queue = DispatchQueue( // 하나의 queue에서 작동하여 예상치 못하는 경우 방지
label: "com.woody",
qos: .userInitiated)
init(initial: State,
reducer: @escaping Reducer<State, Action>]) {
self.state = initial
self.reducer = reducer
}
func dispatch(_ action: Action) {
// 현재 State를 새로운 State로 업데이트
queue.sync {
let newState = reducer(self.state, action)
state = newState
}
}
애플에서 그린 데이터 플로우를 위 개념을 이용해서 새로 그려보았다. 흐름을 설명하면 아래와 같다.
SwiftUI + redux와 관련된 내용은 대부분 getting-a-redux-vibe-into-swiftui 이 글을 참고하여 작성하였기 때문에 더 자세하게 알아보고 싶다면 읽어보길 바란다.
// 바인딩
struct ContentView: View {
@EnvironmentObject var store: AppStore
var body: some View {
switch store.state.homeAppState {
case .none:
HomeView()
case .tracking:
TrackingView()
}
}
}
// Action을 dispatch
struct VoiceMentorCardView: View {
@EnvironmentObject var store: MilgoStore
var body: some View {
Button(action: {
withAnimation {
store.dispatch(.showPopup(program))
}
}) {
// UI Code
}
}
}
코드의 가독성을 높이기 위한 고민에서 시작되어 단방향 데이터 흐름을 알아보게 되었다.
커스텀뷰를 만들어 뷰의 body 내 코드량을 나누어 가독성을 높일 수 있고, 이로 인해 복잡해진 데이터 흐름은 @EnvironmentObject
를 사용함으로 한 곳에서 상태를 관리할 수 있다. 이 때 단방향 데이터 흐름을 고려해야 한다.
하지만 body 내에 있는 UI관련 코드들은 여전히 지저분하다.. 🤯 스유는 어려워