웹 애플리케이션이 복잡해지면 상태(state) 관리가 점점 어려워진다. 특히 MVC 패턴에서는 Model과 View 사이의 관계가 많아질수록 데이터 흐름이 꼬이고, 상태 추적이 어려워지는 문제에 직면하게 된다.
이러한 문제를 해결하기 위해 Facebook에서 제안한 아키텍처가 바로 Flux 패턴이다.
Flux는 단방향 데이터 흐름을 기반으로 상태를 예측 가능하게 관리하는 아키텍처 패턴이다. MVC처럼 컴포넌트가 서로 참조하는 구조가 아니라, 명확한 흐름을 가진 파이프라인 구조를 지닌다.
Flux의 핵심 구성은 아래와 같다:
Action → Dispatcher → Store → View
Action은 사용자의 이벤트(예: 버튼 클릭)나 API 응답처럼 애플리케이션에서 발생하는 모든 의도를 명시적으로 표현한다.
// action.ts
export const markAsRead = (messageId: number) => ({
type: 'MARK_AS_READ',
payload: { id: messageId },
});
Dispatcher는 모든 액션을 중앙 집중식으로 처리하는 허브 역할을 한다. 어떤 Action이 발생했을 때, 어떤 Store에게 전달할지 결정한다.
// dispatcher.ts
import { Dispatcher } from 'flux';
const dispatcher = new Dispatcher();
export default dispatcher;
Store는 상태(state)를 관리하는 계층이다. Action을 받아 내부 상태를 갱신한 뒤, View에 알려준다.
// messageStore.ts
import { EventEmitter } from 'events';
import dispatcher from './dispatcher';
class MessageStore extends EventEmitter {
private messages: { id: number; read: boolean }[] = [];
getMessages() {
return this.messages;
}
handleActions(action: any) {
switch (action.type) {
case 'MARK_AS_READ':
this.messages = this.messages.map((msg) =>
msg.id === action.payload.id ? { ...msg, read: true } : msg
);
this.emit('change');
break;
}
}
}
const messageStore = new MessageStore();
dispatcher.register(messageStore.handleActions.bind(messageStore));
export default messageStore;
View는 Store로부터 상태를 받아 화면을 렌더링한다. 단방향이기 때문에 View → Store로 직접 접근하지 않고, 오직 Action을 통해 상태를 변경한다.
// MessageList.tsx (React 기준)
import { useEffect, useState } from 'react';
import messageStore from './messageStore';
import { markAsRead } from './action';
import dispatcher from './dispatcher';
export function MessageList() {
const [messages, setMessages] = useState(messageStore.getMessages());
useEffect(() => {
const onChange = () => setMessages(messageStore.getMessages());
messageStore.on('change', onChange);
return () => messageStore.removeListener('change', onChange);
}, []);
const handleClick = (id: number) => {
dispatcher.dispatch(markAsRead(id));
};
return (
<ul>
{messages.map((msg) => (
<li key={msg.id} onClick={() => handleClick(msg.id)}>
{msg.read ? <s>읽음</s> : '읽지 않음'}
</li>
))}
</ul>
);
}
MVC는 규모가 작을 때는 효율적이지만, 애플리케이션이 커질수록 Model과 View 간의 의존성과 데이터 흐름이 복잡해지는 경향이 있다. 특히 "읽음 / 읽지 않음" 같은 상태 변경이 여러 컴포넌트에 영향을 줄 때, Model 간 동기화가 어렵고 버그 추적도 복잡해진다.
반면 Flux는 모든 데이터 흐름을 Action → Dispatcher → Store → View 순서로 고정하기 때문에 다음과 같은 장점이 생긴다.
많은 React 개발자들에게 친숙한 Redux는 사실 Flux 패턴을 따르는 라이브러리다.
Redux는 Flux의 복잡한 Dispatcher를 없애고 Reducer와 Store만으로 상태 관리를 단순화했다.
// Redux의 간단한 reducer 예시
function messageReducer(state = [], action) {
switch (action.type) {
case 'MARK_AS_READ':
return state.map((msg) =>
msg.id === action.payload.id ? { ...msg, read: true } : msg
);
default:
return state;
}
}