SSOT에 대한 공부를 하면서 single source를 지키기 위한 아키텍처들의 대한 공부를 진행했다.
참고로 react와 js는 공부를 해보지 않았기 때문에 정확히 맞는 코드인지는 모른다. 각 아키텍처가 가진 특징이나 개선 사항을 공부하기 위한 목적이다.
flux
flux 패턴은 MVC 패턴의 단점으로부터 출발했다고 한다.
MVC 패턴은 간단하지만 치명적인 단점이 있었는데 바로 "양방향 데이터 바인딩" 때문이다.
즉, 데이터가 바뀌면 뷰가 변경되고, 뷰가 변경되면 데이터가 변경되는 구조가 나타나는 것이다.
이렇게 되면 데이트 흐름의 추적이 어렵게 된다. 간단한 앱 규모에서는 찾을 수 있겠지만 어떤 변경이 어디서 발생했는지 추적하기 힘들어지게 된다.
SwiftUI에서도 개발을 하다보면 비슷한 상황이 발생한다.
class UserViewModel: ObservableObject {
@Published var name: String = "홍길동"
}
struct ContentView: View {
@StateObject var viewModel = UserViewModel()
var body: some View {
VStack {
Text("이름: \(viewModel.name)")
TextField("이름 입력", text: $viewModel.name)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
// TextField 값 변경 감지
.onChange(of: viewModel.name) { newValue in
print("TextField에서 이름 변경 감지 → \(newValue)")
// 여기서 또 다른 상태 변경 발생 가능
if newValue.count > 5 {
viewModel.name = String(newValue.prefix(5)) // 강제로 잘라버림
print("onChange에서 다시 ViewModel.name 변경")
}
}
}
.onChange(of: viewModel.name) { newValue in
print("View 전체에서 이름 변경 감지 → \(newValue)")
}
}
}
UserViewModel에 있는 name는 textfield, onChange 등에 의해서 값이 변할 수 있다. 그리고 값이 변경되면 Text, TextField 에 UI가 반영될 것이다. 현재 하나의 뷰에서만 보면 괜찮아 보일 수 있지만 이것이 ViewModel이 아닌 EnvironmentObject로 되어 있어 수 많은 View에서 값을 바꾸고자 한다면? 이게 어디서 어떻게 변경되는지 찾기는 더욱 힘들것이다.

Action
애플리케이션에서 발생하는 변화를 설명하는 객체. 무엇이 일어났는가를 서술한다.
type에는 속성 값을 그리고 payload에는 데이터를 함께 전달하는 것으로 보인다.
const Actions = {
addTodo: (text) => dispatcher.dispatch({ type: "ADD_TODO", payload: text }),
setUser: (user) => dispatcher.dispatch({ type: "SET_USER", payload: user }),
resetTodos: () => dispatcher.dispatch({ type: "RESET_TODOS" })
};
Dispatcher
dispatcher는 모든 데이터 흐름을 관리하는 역할을 한다.
Action이 Dispatcher로 전달되면서 등록된 콜백 함수를 실행하여 Store에 데이터를 전달한다.
class Dispatcher {
constructor() {
this.callbacks = [];
}
register(callback) {
this.callbacks.push(callback);
}
dispatch(action) {
this.callbacks.forEach(callback => callback(action));
}
}
const dispatcher = new Dispatcher();
Stores
애플리케이션의 상태와 그 상태를 변경하는 로직을 담고 있는 컨테이너이다.
스토어는 디스패처에 자신을 등록하고 특정 액션에 반응하여 내부 상태를 스스로 업데이트한다.
class TodoStore {
constructor() {
this.todos = [];
this.listeners = [];
dispatcher.register(this.handleAction.bind(this));
}
handleAction(action) {
switch(action.type) {
case "ADD_TODO":
this.add(action.payload);
break;
case "RESET_TODOS":
this.reset();
break;
}
}
add(todo) {
this.todos.push(todo);
this.emitChange();
}
reset() {
this.todos = [];
this.emitChange();
}
emitChange() {
this.listeners.forEach(callback => callback());
}
subscribe(callback) {
this.listeners.push(callback);
}
getTodos() {
return this.todos;
}
}
const todoStore = new TodoStore();
flux는 MVC의 양방향 데이터 바인딩을 문제점 삼아 action -> dispatcher -> store -> view라는 단방향 구조를 만들게 되었다.
flux의 한계(?)
이걸 한계라고 말해도 될지 모르겠지만 앞으로 나올 redux와 다른 부분이 있어 짚고 넘어가고자 한다.
1. 여러 Store
flux는 Store를 여러개 가진다. 즉, 도메인 기준으로 Store를 가지게 된다는 말인텐데 Dispatch로 하나의 usecase를 처리할 때 다른 store의 정보를 필요로 하는 경우가 있을 것이다.
예시로)
ActivityStore의 업데이트:
- ADD_TODO 액션을 받습니다.
- "[사용자 이름]님이 '[할 일 내용]' 항목을 추가했습니다." 라는 로그를 만들어야 합니다.
- UserStore에 사용자의 이름이 있을 때, 즉 스토어의 업데이트가 다른 스토어의 데이터에 의존해야 하는 상황이 발생
이런 경우 waifFor() 이라는 메서드를 통해서 store 간의 의존성을 관리할 수 있고 최신 데이터를 가져와서 처리를 할 수 있다고 한다.
case 'ADD_TODO':
// UserStore가 이 액션에 대한 처리를 끝낼 때까지 기다린다.
Dispatcher.waitFor([UserStore.dispatchToken]);
// 이제 UserStore의 최신 데이터를 가져올 수 있다.
const userName = UserStore.getUserNameById(action.payload.userId);
const todoText = action.payload.text;
// 자신의 상태(활동 로그)를 업데이트한다.
addLog(`${userName}님이 '${todoText}' 항목을 추가했습니다.`);
break;
각 Store에 대해서 의존성을 가지는 경우 의존성 관계를 파악하고 코드로 관리해야 하기 떄문에 애플리케이션이 커질수록 매우 복잡하고 어려워질 수 있다고 느껴졌다.
https://haruair.github.io/flux/docs/overview.html
Flux | 사용자 인터페이스를 만들기 위한 어플리케이션 아키텍쳐
사용자 인터페이스를 만들기 위한 어플리케이션 아키텍쳐 (한국어 번역)
haruair.github.io
'개발지식' 카테고리의 다른 글
| single source 아키텍처로 가는 길(1) - SSOT(single source of truth) (0) | 2025.08.17 |
|---|---|
| UIKit) 최상단을 계속 유지하는 토스트 메세지 만들기 (0) | 2025.02.17 |
| 클린 아키텍처(CleanArchitecture) 의존성 역전(DIP)에 대해 (2) | 2023.11.13 |
| swift) git 공부하기5 github 연결 git remote add, git push (0) | 2022.08.13 |