[React] Flux 패턴과 MVC 패턴 (feat. Redux)

2022. 4. 8. 23:01React, Javascript

반응형

 

프로그래밍 개발에는 여러가지 디자인 패턴이 존재한다.

특정 프레임워크를 사용하면 코드의 가독성 및 유지보수성, 개발용이성 등을 고려해 특정 디자인 패턴을 사용자에게 강제화 하기도 한다. 

대표적으로 Java 진영의 Spring Framework가 있다. 이는 MVC 디자인 패턴을 어느정도 강요하여 개발을 유도한다.

 

 

프론트 엔드 개발이 복잡해지면서 백엔드 쪽에서만 논의되어 오던 디자인 패턴을 자연스럽게 프론트에서도 고민하게 되었다. 현시점 가장 인기있는 프론트엔드 개발도구인 React는 Flux 패턴이란 것을 지향하고 있다.

 

 

Facebook: MVC Does Not Scale, Use Flux Instead [Updated]

This article has been updated based on community and Jing Chen (Facebook)’s reaction. (See the Update section below.) Facebook came to the conclusion that MVC does not scale up for their needs and has decided to use a different pattern instead: Flux.

www.infoq.com

 

위 글에서처럼 페이스북(현 메타)은 MVC 패턴이 스케일업 하기에는 좋지 않고 그래서 Flux 패턴을 개발했다고 한다.

 

MVC는 규모가 커질수록 controller의 규모가 커지면서 V와 M 사이의 데이터 복잡도, M들간의 종속 업데이트로 개발이 어려워진다.

 

Flux는 데이터가 한방향으로 흘러 직관적이고 규모가 커져도 각 역할에 맞는 개발이 가능하다고 말한다.

 

하지만 이에 반하는 의견을 가진 개발자 집단도 존재하여 아직도 무엇이 정답이다라는 결론은 나오지 않은 상태이다.

정답이 과연 존재하는가도 의문이긴 하다.

 

 

 

1. 자바스크립트 간단 MVC를 적용하기


간단한 Todo 리스트 관리를 MVC 패턴에 맞게 작성해 보았다.

 

 

원칙은 View에서는 Dom을 그리는 것만을 집중한다.

 

Model은 todo 리스트가 가지고 있는 데이터의 상태 관리에만 집중한다.

 

controller는 유저의 행위를 이벤트로 감지하고 이를 데이터 변화가 있을시 model에 알려주고 view를 새로 그리게 하는 역할을 한다.

 

 

먼저 간단한 html을 작성해 준다.

todo List를 출력하는 영역과 입력하는 영역만 있다.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>todo MVC</title>
  </head>
  <body>
    <h1>To Do Manage MVC</h1>
    <input type="text" id="todo" />
    <button id="registerBtn">register</button>

    <h3>Todo List</h3>
    <ul id="todolist"></ul>
    <script type="text/javascript" src="./app.js"></script>
  </body>
</html>

 

 

 

다음으로 app.js에 MVC를 모두 작성했다.

먼저 TodoModel이다.

class TodoModel {

  constructor() {
    // todo list를 관리 할 array 변수를 관리한다.
    this.todoList = [];
  }


  // 투두 리스트를 가져갈 수 있는 메소드를 만든다.
  getTodoList() {
    return this.todoList;
  }

  // 투두가 추가 될때 리스트에 추가하는 메소드를 만든다.
  pushTodoListData(todo) {
    this.todoList.push(todo);
  }
}

 

다음으로 TodoView이다.

 

class TodoView {
  constructor(model) {
    this.model = model;
  }

  //컨트롤러 접근이 필요한 화면 요소를 만들어 준다.
  inputArea = document.getElementById('todo');
  todolist = document.getElementById('todolist')
  registerBtn = document.getElementById('registerBtn')


  //todo가 새로 생겼을 때 화면 요소를 추가하여 리턴해 주는 메소드
  createListItemNode(todo) {
    const listItemNode = document.createElement("li");
    const textNode = document.createTextNode(
      todo
    );
    listItemNode.appendChild(textNode);

    return listItemNode;
  }

  // todo가 추가되었을때 리스트 화면을 그리는 메소드
  registerTask(parentNode, childNode) {
    parentNode.appendChild(childNode);
  }
}

 

마지막으로 TodoController이다.

class TodoController {
  constructor(model, view) {
    this.model = model;
    this.view = view;
    this.registerEventListener(model, view);
  }


  //버튼이 클릭되었을 때를 감지한다.
  registerEventListener(model, view) {
    const registerationBtn = view.registerBtn;
    registerationBtn.addEventListener("click", () => {
      this.addTodoListData(model, view);
    });
  }

  //이벤트가 일어났다고 model과 view에 알린다.
  addTodoListData(model, view) {
    const currentInputData = view.inputArea.value;
    const todoItemNode = view.createListItemNode(currentInputData);
    const todoListParentNode = view.todolist;
    model.pushTodoListData(currentInputData);
    view.registerTask(todoListParentNode, todoItemNode);
    
  }
}

 

MVC 패턴에서 각 요소에 맞게 todo리스트 등록, 출력 기능을 만들어보았다.

 

사실 이정도의 기능만 가지고는 MVC 패턴이 스케일업시 안좋다는걸 알 수 없다.

 

이 기본 구조아래 기능들의 추가를 상상해 보았다.

 


todo를 완료 했다는 상태 업데이트 기능이 추가되면 어떻게 변할까?

Model의 단순 배열이 객체 배열로 바뀌면서 Model에는 업데이트 메소드가 생기고 View에도 적절한 출력 메소드가 생길 것이다.
Controller는 상태에 따른 분기가 생길 것이고 View나 Model보다는 많은 로직이 생길 것이다.

 

 

확실히 MVC패턴의 단점 중 하나로 나오는 Controller의 규모가 커진다는 상상할 수 있었다.

 

 

 

그렇다면 View와 Model의 양방향 데이터 바인딩으로 인한 개발복잡성 증가도 생길까?

 

 


만약 todo의 진행중과 완료 영역이 나눠져 있고 todo를 마우스로 드래그앤드롭 형태로 옮긴다면 어떻게 변할까?
이때 모든 이벤트를 controller에서 감지하게 한다면 불필요한 코드도 많아지고 controller는 대혼돈의 멀티버스 수준이 될 것이다.
View의 이벤트를 바로 Model에 반영하도록 바꾼다면 코드는 줄겠지만 Facebook이 말한 Model과 View의 복잡도가 늘어날 것이다.

 

 

 

이런 기능들이 하나 둘 늘어나 Facebook같은 규모가 된다면 충분히 페이스북이 말한 MVC의 단점이 들어날만 하다고 생각이 된다.

 

 

반응형

 

2. React로 Flux패턴 적용해 보기


React는 자바스크립트 UI 라이브러리로 부모 컴포넌트에서 자식 컴포넌트로 props를 내려주는 단방향 데이터 바인딩에 가깝다고 할 수 있다.

 

 

Flux | Flux

Application architecture for building user interfaces

facebook.github.io

 

그래서 데이터가 한방향으로만 흐를 수 있도록 action을 통해서 데이터를 바꾸고 이를 하나의 store가 통제하는 flux 패턴이 더 어울리는 것처럼 보인다.

 

이번에는 조금 더 업그레이드 된 todo를 React와 Context API를 활용해서 구현해 보았다.

 

Index.jsx

import React, { useCallback, useReducer } from "react";
import Header from "./components/header";
import TodoList from "./components/list";
import TodoInput from "./components/input";

const initialState = {
  todos: [
    {
      id: 1,
      todoName: "오늘의 할 일",
      todoStatus: "done",
    },
    {
      id: 2,
      todoName: "Blog 글쓰기",
      todoStatus: "doing",
    },
    {
      id: 3,
      todoName: "github 잔디심기",
      todoStatus: "doing",
    },
  ],
};

function reducer(state, action) {
  console.log(action, state);
  switch (action.type) {
    case "ADD_TODO":
      return {
        todos: state.todos.concat({
          ...action.todos,
          id: state.todos.length + 1,
        }),
      };
    case "REMOVE_TODO":
      return {
        ...state,
        todos: state.todos.filter((todo) => todo.id !== action.id),
      };
    case "UPDATE_TODO":
      return {
        ...state,
        todos: state.todos.map((todo) =>
          todo.id === action.id
            ? {
                ...todo,
                todoStatus: todo.todoStatus === "done" ? "doing" : "done",
              }
            : todo
        ),
      };
    default:
      return state;
  }
}

function countTodo(todos) {
  return todos.filter((todo) => todo.todoStatus === "doing").length;
}

export const TodoDispatch = React.createContext(null);
export const TodoState = React.createContext(null);

const Jinho = () => {
  const [state, dispatch] = useReducer(reducer, initialState);
  const { todos } = state;

  const countTodoFunction = useCallback(() => countTodo(todos), [todos]);

  return (
    <TodoState.Provider value={state}>
      <TodoDispatch.Provider value={dispatch}>
        <div className="container">
          <Header count={countTodoFunction(todos)} />
          <TodoList />
          <TodoInput />
        </div>
      </TodoDispatch.Provider>
    </TodoState.Provider>
  );
};

export default Jinho;

 

 

 

TodoList.jsx

 

import React, { useContext } from "react";
import TodoItem from "./TodoItem";
import { TodoState } from "../../../jinho";

const TodoList = () => {
  const { todos } = useContext(TodoState);

  return (
    <>
      {todos.map((todo) => (
        <TodoItem
          key={todo.id}
          todoId={todo.id}
          todoName={todo.todoName}
          todoStatus={todo.todoStatus}
        />
      ))}
    </>
  );
};

export default TodoList;

 

TodoItem.jsx

 

import React, { useContext } from "react";
import { TodoDispatch } from "../../../jinho";

const TodoItem = React.memo(function TodoItem(props) {
  const { todoId, todoName, todoStatus } = props;
  const dispatch = useContext(TodoDispatch);
  return (
    <li>
      <input
        type="checkbox"
        checked={todoStatus === "done" ? true : false}
        onChange={() => dispatch({ type: "UPDATE_TODO", id: todoId })}
      />
      <span>{todoName}</span>
    </li>
  );
});

export default TodoItem;

 

TodoInput.jsx

import React, { useContext, useRef } from "react";
import { TodoDispatch } from "../../../jinho";

const TodoInput = React.memo(function TodoInput() {
  const dispatch = useContext(TodoDispatch);
  const inputRef = useRef();
  return (
    <>
      <input type="text" ref={inputRef} placeholder="할 일을 입력하세요" />
      <button
        onClick={() =>
          dispatch({
            type: "ADD_TODO",
            todos: { todoName: inputRef.current.value, todoStatus: "doing" },
          })
        }
      >
        추가
      </button>
    </>
  );
});

export default TodoInput;

 

코드들의 핵심은 dispatch시 보내는 action과 index.jsx가 가지고 있는 initialState 즉 store에 데이터를 업데이트 하고 그것을 바탕으로 view가 업데이트 된다는 것이다.

 

내 생각에는 Flux는 MVC와 아예 다른 패턴이 아니라 MVC 패턴의 업그레이드 버전 처럼 보인다.

 

controller 역할을 하는 Dispatcher가 action으로 좀더 직관적이고 자세한 명령을 받아서 수행하는 것이 controller의 복잡도 증가를 줄이고 Model과 View 사이를 느슨하게 해 준 것으로 보인다.

 

 

하지만 아래 글을 보면 다른 의견들도 상당히 있다.

 

 

Facebook: MVC Does Not Scale, Use Flux Instead

Posted in r/programming by u/Fapmate • 418 points and 378 comments

www.reddit.com

 

좀 더 대규모의 개발에서 디자인 패턴에 대한 고민들을 더 해봐야 하겠지만 Flux는 Flux대로 괜찮다고 생각한다.

 

아무리 MVC의 짭이고 Facebook이 MVC를 잘못 적용한 것이라고 해도 각 단계의 직관성을 줬다는 측면과 현 시점 가장 인기있는 자바스크립트 라이브러리와 잘 맞는다는 측면에서 그렇게 생각한다.

 

 

 

3. Redux, 또 다른 Flux?


Flux 패턴을 활용한 React의 전역 상태관리 방식으로 Redux가 많이 채택된다.

 

 

Redux는 Reducer, action, store로 구성된다. 완벽하게 flux 패턴을 따라 구현되지는 않았지만 기본 사상은 같은 선상에 있다고 볼 수 있다.

 

Redux는 Dispatcher를 명시적으로 생성하지 않고도 Flux를 구현할 수 있도록 작성되었으므로 Dispatcher를 생략할 수 있다. 실제 디스패치 동작은 스토어의 dispatch 메소드를 호출하여 실행한다.

 

앞서 Flux 형태를 만들어본 Context API와 비교했을 때 많은 부가 기능들을 지원한다.

Context API는 공식문서에도 나와있듯이 props drilling을 위한 것이지 전역 상태관리를 지원하는 기능이 아니다.

 

대규모의 애플리케이션에서는 결국 redux와 같은 전역상태관리 tool 필요하다.

 

 

 

4. 마무리


사실 React를 적용하는 큰 규모의 프로젝트를 경험해보지 못한 경험으로 디자인 패턴에 대한 생각을 말하기에는 무리가 있을 수 있다.

 

하지만 React를 쓰는 개발자로서 최적의 개발패턴이 무엇인지에 대한 고민은 끊임없이 해야한다고 생각한다.

 

결국 MVC를 활용해 간단한 애플리케이션을 만들면 효율성이 증가하는 것이고, 확장성을 고려해 redux등을 활용한 flux 패턴을 이용했다면 시간은 오래 걸려도 추후에는 더 빛을 볼 수도 있다.

 

결국 정답은 없는 듯 하다. 적절한 상황에 맞는 문제해결 능력. 결국 코딩보다는 문제해결 능력인가 보다.

반응형