Flux란? Flux vs MVC, Flux vs Redux

Flux란? Flux vs MVC, Flux vs Redux

Tags
React.js
Published
January 15, 2025
Author
Seongbin Kim
작성 중
 
  • 이 글의 내용
    • Flux가 무엇이고, Redux와의 차이점을 이해합니다.
    • Flux를 실제로 사용해보고 구체적으로 각 구성 요소를 이해합니다.
 
  • 주요 참고 자료
 

1. Flux vs MVC

 

1-1. 소개

  • Facebook에서 개발하고 사용된 패턴이자 라이브러리입니다.
  • React로 View를 구축할 때 애플리케이션 상태를 관리하기 위해 개발되었습니다.
  • 주어진 Model을 매번 re-render하는 View와 잘 어울립니다.
 

1-2. 역사

  • Flux는 F8 2014 컨퍼런스에서 페이스북 개발자들이 발표했습니다. (10:50 ~ 24:00)
    • Video preview
    • Flux는 발표 당시 오픈소스로 공개되지 않았고, 발표 당시에는 Dispatcher 구현체만 공개되었습니다.
    • 그 때문에 패턴으로 받아들여지고 여러 가지 구현체가 등장했습니다.
    • Redux 1.0이 나온 시점에서야 구현체가 공개되었습니다.
    • Redux는
  • (tmi) React는 JSConf US 2013 컨퍼런스에서 페이스북 개발자들이 오픈소스로 공개하고 발표했습니다.
    • Video preview
 

1-3. 요약

 

해결하고자 한 문제점

  • 채팅 회귀 버그들이 자주 발생
    • 소스 코드의 다른 부분을 수정했을 때 채팅 회귀가 발생
 

원인

  • 예측하기 어려운 렌더링 결과(코드 실행 결과)
    • 좋지 못한 설계 - 과도하게 복잡한 Controller 코드
      • Controller는 이벤트, API 응답을 처리
      • 특정 이벤트, API 응답을 처리할 때, 갱신할 Model이 많아질수록, Controller가 복잡해짐
      • Controller가 복잡하므로 실행 결과를 예측하기 어려워짐
      • 연관 Model 간 연쇄 갱신 사례:
        • DM 목록 모델 에서 특정 DM을 읽음으로 갱신하는 경우 읽지 않은 개수 모델 에서 개수를 갱신
        • (MVC 참고)
 

해결 방법

  • 렌더링 결과를 더 쉽게 예측할 수 있게 설계
    • Action → Dispatcher → Store → View
    • Controller의 책임을 Model과 View로 이동
      • Model의 public setter 제거
      • Model의 setter를 활용하던 Controller 제거
      • Model이 Event를 수신하고 직접 처리
      • View는 Model의 변경 사항을 수신해 직접 처리 (React)
    • 렌더링 원인을 명확화
      • 상태 변경은 Action을 발행해야만 가능하게 변경
    • 렌더링 과정을 단순화
      • 각 렌더링의 원인은 하나의 Action이도록
        • (prevState, action) ⇒ nextState
        • State 마다 렌더링 수행
          • 상태 반영(reduce) 도중에는 Action 발행을 금지 (Store → Action ❌)
          • 별도의 State 변경이 필요한 경우, 새로운 Action → … → View 단계를 거치도록.
 

결과

  • 단방향 데이터 흐름
    • Action → Dispatcher → Store → View (→ Action)
      • notion image
      • Event, Web API를 처리할 때 Controller가 하지 않고, Action 발행만 하는 모습.
 

효과

  • 원인이었던 채팅 회귀 버그 해소
  • 버그의 원인 파악이 더 쉬워짐
  • 단위 테스트 작성이 더 쉬워짐
 

1-4. 해결하고자 한 문제점

  • 계속 반복되는 채팅 버그의 회귀때문에 구조적인 문제가 있다고 판단하고, 구조를 개선하고자 했다고 합니다.
 

사례: 채팅 버그

  • 당시 facebook에는 계속해서 채팅 회귀 버그들이 있었다고 합니다.
  • 가장 유명한 것이 읽지 않은 메시지 개수 버그라고 합니다.
notion image
notion image
  • 채팅 기능에는 Model이 여러 개가 있어 Controller에서 여러 개의 Model, View를 다루고 있었습니다.
    • notion image
 

설계 개선: Client MVC

  • 처음에는 문제가 없었지만, Model, View가 많아지고 서로 연관될수록 기능을 추가하는 과정이 어려워졌고, 전체적으로 동작의 흐름을 예측할 수 없어졌다고 합니다.
    • notion image
 

1-5. 해결 방법

 

Controller 사용 → Controller 제거, Event 기반으로

  • AS-IS: 이벤트, API 응답 처리 시 Controller(MVC)로 처리
    • (ex) new message API Handler
      • ChatTab, MainMessages, UnseenThreads 모델의 상태를 갱신
      • 화면을 리렌더링
  • TO-BE: 각 UI 요소에서 응답해야 하는 Event를 직접 처리
    • API 응답 수신 시 Action이 발행되고, 관련된 Model들에게 전달
    • 각 Model이 독립적으로 Action을 처리
      • (ex) 신규 메시지가 있는 경우
        • new message Action을 발행
          • ChatTab, MainMessages 모델은 UI 상에 메시지 추가
          • UnseenThreads 모델은 조회하지 않은 메시지 목록에 추가
      • (ex) 메시지를 읽은 경우
        • mark seen Action을 발행
          • UnseenThreads 모델은 조회하지 않은 메시지 목록에서 해당 메시지를 제거
 

View는 Model의 상태를 구독하고 자동 반영

  • AS-IS: Controller가 직접 View의 갱신을 호출
    • (ex) 신규 메시지가 있는 경우
      • messagesView.appendMessage(newMessage);
  • TO-BE: View는 Model을 구독하고 변경 사항이 있으면 스스로 re-render 수행
    • (ex) 신규 메시지가 있는 경우
      • ChatTab, MainMessages 모델은 UI 상에 메시지 추가
      • messagesViewmessages를 받아 화면을 스스로 갱신
 

API 데이터 그대로 사용 → 클라이언트 상태 모델 추가

  • AS-IS: 신규 메시지 알람 기능의 경우 new message 이벤트만 존재
    • 이벤트마다 그 개수만 더하고 빼는 방식을 사용
    • (도메인 계층이 별도로 없고 API 반환 값을 그대로 사용하는 코드만 있는 방식)
  • TO-BE: unseen messages(조회하지 않은 메시지) 라는 클라이언트 상태를 새로 정의
    • “조회하지 않은 메시지 목록”을 명시적으로 관리
    • 조회하지 않은 메시지의 개수는 이 목록의 길이를 활용
    • (클라이언트에 도메인 계층이 생겨 코드의 의도가 더 명확해지는 방식)
    • notion image
 

렌더링 과정 단순화 (렌더링 원인 명확화)

  • AS-IS: Controller에서 View에 반영되기 전에 여러 Model을 갱신
    • 여러 단계가 한 번의 렌더링 사이클에서 실행되므로 복잡
      • (ex) new message, mark unseen이 한 번에 실행되고 화면에 반영
  • TO-BE: 한 단계에 한 번의 렌더링으로 단순화
    • 신규 상태를 View가 렌더링하기 전까지 상태 갱신을 금지
      • Store에서 스스로 신규 Action을 발행할 수 없도록 제한
      • 항상 (prevState, action) => nextState인 구조 (action[] ❌)
        • (ex) new message 발행 이후 렌더링, 이후 mark unseen 발행
      • Action → Dispatcher → Store → View 순서를 항상 지켜야 함
        • notion image
 

1-6. 도입 효과

  • 채팅 버그 해소
  • 신규 입사자의 코드 파악 시간 감소
  • 버그 원인을 더 쉽게 찾을 수 있게 개선
 

2. (WIP) Redux와의 차이점

 

동일한 점

  • Action이라는 Event 기반입니다.
  • 단방향 데이터 흐름입니다.

유사점

  • reduce 함수가 있습니다.
    • Redux에서 reducer는 함수 형태로 구성합니다.
    • Flux에서 reduce는 Store의 메소드입니다.
  • connect 함수와 같은 create/createfunctional 함수가 있습니다.
    • 일종의 mapToStateToProps가 있습니다.
    • Flux는 별도로 구독할 Store의 배열을 추가로 입력받습니다. (Redux는 단일 스토어이므로 필요 없습니다)

차이점

  • 기능 마다의 Store를 사용하며, Multi Store로 앱이 구성됩니다.
    • Dispatcher라는 구성 요소가 있습니다.
      • Multi Store때문에 필요한 요소입니다.
      • 모든 Store는 Dispatcher에 등록됩니다.
      • Dispatcher는 Action 실행의 단일 진입점입니다.
      • Dispatcher는 등록된 모든 Store에 Action을 전달합니다.
    • 특정 Action 처리 시 Store 간의 순서를 명시적으로 결정해야 합니다.
      • 이를 waitFor라는 헬퍼 함수를 호출함으로써 구현합니다.
  • Store 정의 시에는 Store를 상속받는 class로 정의해야 합니다. (React의 Component class 같이)
  • mapDispatchToProps가 없습니다. 대신 Action Creator가 순수 함수가 아니라, dispatch 호출까지 수행합니다.

부족한 점

  • Middleware 정도의 확장성은 없습니다.
    • 대신 Store에서 비동기적으로 Action을 발행합니다.
    • (Middleware는 대부분 비동기 동작을 하기 때문에 문제가 없어보입니다.)
 

3. Flux vs Redux

  • Flux에서는 어떻게 작성하고, Redux와 개념적으로, 코드 상으로 다른 부분을 비교해서 정리합니다.
 

3-1. 요약 (WIP)

 
 

3-2. Todolist 예시

  • 출처
    • MVC 예제
    • Chat 예제 (3.1.0 이후에 제거됨)
 

기능 목록

  • list 조회
  • item 생성 (텍스트 컨텐츠 등록)
  • item 제거
  • item의 완료 여부 toggle
  • item의 텍스트 컨텐츠 수정
 

기본 데이터 초기화

  • addTodo UI가 없는 상태로, 직접 Store를 호출
// 렌더링 기보 createRoot(document.getElementById("root")!).render( <StrictMode> <App /> </StrictMode>, ); // 인위적으로 초기화 TodoActionDispatchers.addTodo("Hello, world 1"); TodoActionDispatchers.addTodo("Hello, world 2"); TodoActionDispatchers.addTodo("Hello, world 3");
 

list 조회: View —구독→ Store

  • UI
    • notion image
  • Store의 정보를 구독
    • Redux 방식: connect
      • // Redux function mapStateToProps(state) { return { todos: state.todos } } export default connect(mapStateToProps)(TodoView)
    • flux 방식: create
      • flux/utilsContainer.create를 사용
        • createFunctional은 Function Component 연결 시 사용
      • Flux는 multi store 체제이기 때문에, 구독할 store의 목록도 전달해야 합니다. (getStores)
      • const getStores = () => { return [TodoStoreInstance]; }; const getState = () => ({ todos: TodoStoreInstance.getState(), }); export const ConnectedTodoContainer = Container.createFunctional( TodoView, getStores, getState, );
  • Store 구성
    • 공통점:
      • Store 선언이 조금 다르지만, reduce = (state, action) => state인 함수가 있습니다.
    • Redux 방식:
      • 단일 Store 구조이므로 하나만 생성하면 됩니다.
      • Store 생성에는 reducer와 initialState, middleware가 필요합니다.
      • const todos = (state, action) => { switch (action.type) { default: return state } } // 단일 reducer를 사용 중이므로 combineReducer를 사용하지 않는 상황 // 별도의 middleware가 없음 const store = createStore(todos, {})
    • Flux 방식:
      • 생성자에서 Dispatcher를 등록합니다.
      • Facebook에서는 주로 immutable 라이브러리를 사용한 듯 합니다.
      • class TodoStore extends ReduceStore<TodoStoreState, TodoActionTypes> { constructor() { super(TodoDispatcher); } getInitialState() { return Immutable.OrderedMap<string, TodoInstanceType>(); } reduce(state: TodoStoreState, action: TodoActionTypes) { switch (action.type) { default: return state; } } }
 

delete, toggle: View에서 Action 발행(dispatch)

  • UI
    • notion image
  • Action Creator를 Store로 dispatch되도록 연동하는 작업이 필요합니다.
    • Redux 방식: connect
      • // 자동으로 bindActionCreators 호출 const mapDispatchToProps = { deleteTodo, toggleTodo, } export default connect(null, mapDispatchToProps)(TodoApp)
      • mapDispatchToProps는 ActionCreator 함수의 반환값을 Store에 dispatch하는 새 함수를 반환합니다.
      • 컴포넌트에서는 deleteTodo, toggleTodo를 호출하면 그대로 dispatch됩니다.
      • Store가 하나이기 때문에 어느 Store의 dispatch 메소드를 사용할지 결정할 필요가 없습니다.
    • Flux 방식: create(Functional)
      • const getStores = () => { return [TodoStoreInstance]; }; const getState = () => ({ todos: TodoStoreInstance.getState(), deleteTodo: TodoActionDispatchers.deleteTodo, toggleTodo: TodoActionDispatchers.toggleTodo, }); export const ConnectedTodoContainer = Container.createFunctional( TodoView, getStores, getState, );
      • getState = Redux의 mapStateToProps + mapDispatchToProps 입니다.
      • Store에 dispatch 메소드가 있지 않고, 별도의 Dispatcher가 있습니다.
      • Dispatcher는 App 수준에서 singleton입니다.
      • Dispatcher는 모든 Store에 Action을 전파하기 때문에, 모든 Action의 진입점이어야 합니다.
        • export const TodoDispatcher = new Dispatcher<TodoActionTypes>();
          export const TodoActionDispatchers = { deleteTodo: (id: string) => { TodoDispatcher.dispatch({ type: TodoActionTypeConstants.DELETE_TODO, id, }); }, toggleTodo: (id: string) => { TodoDispatcher.dispatch({ type: TodoActionTypeConstants.TOGGLE_TODO, id, }); }, };
        • ActionCreator가 Dispatcher 인스턴스를 직접 호출합니다.
 

add item: TodoDraftStore라는 별도의 Store 추가

  • UI
    • notion image
  • Store 추가
    • Redux 방식: 단일 Store
      • // 신규 reducer 정의 export const todoDraftReducer = (state = initialDraftState, action) => { switch (action.type) { case TodoActionTypeConstants.UPDATE_DRAFT: return { ...state, draftText: action.text, }; case TodoActionTypeConstants.ADD_TODO: return { ...state, draftText: "", }; default: return state; } }; const rootReducer = combineReducers({ draft: todoDraftReducer, todos: todoReducer, }); // Store Creation export const todoStore = createStore(rootReducer);
      • 별도의 reducer를 정의한 후 combineReducer를 사용합니다.
    • Flux 방식: 다중 Store
      • class TodoDraftStore extends ReduceStore<TodoDraftState, TodoActionTypes> { constructor() { super(TodoDispatcher); } getInitialState() { return { draftText: "", }; } reduce(state: TodoDraftState, action: TodoActionTypes): TodoDraftState { switch (action.type) { case TodoActionTypeConstants.UPDATE_DRAFT: return { ...state, draftText: action.text, }; case TodoActionTypeConstants.ADD_TODO: return { ...state, draftText: "", }; default: return state; } } }
        const getStores = () => { return [TodoStoreInstance, TodoDraftStoreInstance]; }; const getState = () => ({ // ... draftText: TodoDraftStoreInstance.getState().draftText, // ... }); export const ConnectedTodoContainer = Container.createFunctional( TodoView, getStores, getState, );
      • 신규 Store이므로 getStores에 추가해두고, getState에도 추가해둡니다.
 

item text 수정