개발 이야기/frontend

MVVM 프레임워크, Flux, Redux 제대로 이해하기

thisisamrd 2024. 8. 30.

 

리액트 애플리케이션에서 효율적인 상태 관리를 위해서는 다양한 패턴과 아키텍처를 이해하는 것이 중요합니다. 이번 글에서는 MVVM, Flux, Redux 패턴을 비교하고, 각각의 특징과 활용법을 알아봅니다.

 

 

 

MVVM 패턴

 

 

 

 

MVVM 프레임워크란 무엇일까?

MVVM(Model-View-ViewModel) 프레임워크는 소프트웨어 개발에서 UI(User Interface)와 로직을 분리하기 위해 사용되는 아키텍처 패턴 중 하나입니다. MVVM은 특히 WPF(Windows Presentation Foundation), Xamarin, 그리고 최근에는 프론트엔드 웹 개발에서 많이 사용됩니다.

 

 

MVVM 패턴은 다음과 같은 세 가지 주요 구성 요소로 나뉩니다.

 

 

1. Model

 

애플리케이션의 데이터와 비즈니스 로직을 다룹니다. 데이터베이스나 웹 서비스로부터 데이터를 가져오고 이를 저장하거나 가공하는 역할을 합니다.

 

 

2. View

 

사용자 인터페이스(UI) 부분입니다. 사용자가 볼 수 있고 상호작용할 수 있는 요소들로 구성됩니다. View는 데이터를 표현하는 데만 집중하며, 직접적인 로직을 처리하지 않습니다.

 

 

3. ViewModel

 

Model과 View를 연결하는 중간 다리 역할을 합니다. View에 표시될 데이터를 준비하고, 사용자의 입력을 처리하여 Model에 반영합니다. ViewModel은 View를 직접 참조하지 않기 때문에 유연한 구조를 유지할 수 있습니다.

 

 

MVVM 패턴을 사용하면 코드의 재사용성을 높이고, 유지보수를 쉽게 하며, 테스트 가능한 코드를 작성하는 데 유리합니다. 많은 프레임워크들이 MVVM을 지원하며, 대표적인 예로는 Angular, React(Redux와 결합할 경우), Vue.js 등이 있습니다.

 

 

 

 

 

 

 

리액트는 MVVM 패턴이라고 할 수 있을까

리액트(React)는 엄밀히 말해 전형적인 MVVM 패턴을 강제하지 않습니다. 대신 컴포넌트 기반 아키텍처를 통해 MVVM과 유사한 구조를 사용할 수 있도록 합니다. 리액트는 "V"인 View 레이어만 다루는 라이브러리로, 상태 관리를 포함한 다른 기능들은 추가 라이브러리나 패턴을 통해 구현하게 됩니다.

 

 

MVVM 패턴과 리액트의 관계를 이해하려면 다음과 같은 점들을 고려해야 합니다.

 

1. View(컴포넌트)

 

리액트 컴포넌트는 View를 나타냅니다. 사용자 인터페이스의 특정 부분을 렌더링하고 사용자와의 상호작용을 처리합니다. 리액트의 JSX 구문은 HTML과 유사한 형태로 UI를 기술하게 해주며, 상태(State)와 속성(Props)을 사용하여 데이터를 화면에 반영합니다.

 

2. ViewModel (상태와 로직 처리)

 

리액트에서 상태와 이벤트 처리는 컴포넌트 내부 또는 Redux 같은 상태 관리 라이브러리를 통해 구현됩니다. 리액트의 컴포넌트는 ViewModel의 역할을 부분적으로 수행하며, 상태(state)와 이벤트 핸들러를 통해 UI의 동작을 제어합니다. Redux나 MobX와 같은 상태 관리 도구를 사용하면 리액트 애플리케이션이 좀 더 명확하게 MVVM 패턴을 따르는 것처럼 보일 수 있습니다.

 

3. Model (데이터와 비즈니스 로직)

 

리액트 애플리케이션에서 Model은 API 호출, 데이터베이스 등 외부 데이터 소스와 관련된 부분입니다. 이 데이터는 보통 Redux 같은 상태 관리 도구를 통해 ViewModel에 전달되고, ViewModel은 이를 View로 전달합니다.

 

 

따라서, 리액트 자체는 MVVM을 강제하지 않지만, 구조적으로 MVVM 패턴을 적용할 수 있는 유연성을 제공합니다. 리액트와 Redux, 또는 다른 상태 관리 도구를 결합하면 데이터와 UI의 흐름을 보다 명확하게 나눌 수 있어 MVVM 패턴과 유사한 구조를 구현할 수 있습니다.

정리하자면, 리액트가 MVVM 패턴을 따르는 것이 아니라, 개발자가 리액트 환경에서 MVVM 패턴을 적용할 수 있는 방식으로 코드를 작성할 수 있다는 것입니다.

 

 

 

 

Props Drilling이란?

 

Props Drilling

 

 

Props Drilling은 리액트에서 상태나 데이터를 상위 컴포넌트에서 하위 컴포넌트로 전달할 때 발생하는 문제를 말합니다. 간단히 말해, 데이터가 필요한 컴포넌트에 직접적으로 전달되지 않고, 여러 중간 컴포넌트를 거쳐 전달되는 상황을 의미합니다.

 

 

Props Drilling의 문제

리액트에서 부모 컴포넌트가 자식 컴포넌트에게 데이터를 전달할 때, 그 자식 컴포넌트는 또다시 그 데이터를 하위 컴포넌트에게 전달해야 할 수 있습니다. 이렇게 되면 여러 단계에 걸쳐 props를 전달해야 하는데, 이것이 바로 "Props Drilling"입니다.

 

예를 들어, A 컴포넌트에서 D 컴포넌트로 데이터를 전달해야 하는데, B와 C 컴포넌트를 거쳐야 하는 상황을 생각해보세요. 이 과정에서 B와 C는 실제로는 데이터가 필요 없지만, 단지 데이터 전달을 위해 props를 받아야 하는 경우가 생깁니다.

 

이렇게 되면 컴포넌트가 많이질수록 props를 통해 데이터를 전달하는 과정이 복잡해지므로 코드의 복잡성이 증가하며, 유지보수가 어려워지고 재사용성이 저하된다는 문제가 있습니다.

 

 

 

Props Drilling 해결 방법

리액트에서는 이러한 문제를 해결하기 위한 몇 가지 방법이 있습니다.

 

 

1. Context API 사용

 

Context API는 리액트에서 전역적으로 상태나 데이터를 공유할 수 있게 해줍니다. 이를 통해 props를 통해 데이터를 계속 전달하지 않고, 필요한 컴포넌트에서만 직접 접근할 수 있습니다.

 

 

2. 상태 관리 라이브러리 사용

 

Redux, MobX와 같은 상태 관리 라이브러리를 사용하면 상태를 전역적으로 관리할 수 있습니다. 이를 통해 props drilling 없이 필요한 컴포넌트에서 데이터를 직접 사용할 수 있습니다.

 

 

3. 커스텀 Hooks 사용

 

특정 데이터나 로직을 여러 컴포넌트에서 재사용할 수 있도록 커스텀 훅을 작성하면, 불필요한 props drilling을 피할 수 있습니다.

 

 

 

 

 

 

Flux 패턴이란?

Flux 패턴은 리액트 애플리케이션에서 데이터 흐름을 관리하기 위해 사용되는 아키텍처 패턴입니다. 페이스북에서 제안한 이 패턴은 리액트의 컴포넌트 기반 구조와 잘 맞아떨어지며, 복잡한 상태 관리를 단순화하고 예측 가능하게 만드는 데 중점을 둡니다.

 

 

Flux 패턴의 주요 개념

Flux 패턴은 다음과 같은 네 가지 주요 요소로 구성되어 있습니다.

 

1. Action(액션)

  • 애플리케이션에서 발생하는 모든 사용자 입력이나 이벤트를 설명하는 객체입니다.
  • Action은 데이터를 포함하며, 이벤트의 유형(type)과 관련된 정보를 담고 있습니다. 예를 들어, "ADD_ITEM", "REMOVE_ITEM"과 같은 유형이 있을 수 있습니다.
  • Action은 Action Creator라는 함수를 통해 생성됩니다. 이 함수는 특정 이벤트가 발생했을 때 해당 이벤트에 맞는 액션 객체를 반환합니다.

 

2. Dispatcher(디스패처)

  • Flux의 중심 허브 역할을 하며, 모든 액션을 받아서 이를 스토어(Store)로 전달합니다.
  • Dispatcher는 액션을 수신하고, 이 액션이 어떤 스토어에 영향을 미칠지 결정합니다. 모든 스토어는 Dispatcher의 등록된 콜백을 통해 액션을 수신합니다.
  • 각 스토어는 자신이 처리할 액션을 정의하고, Dispatcher는 그에 맞게 해당 스토어로 액션을 보냅니다.

 

3. Store(스토어)

  • 애플리케이션의 상태(state)를 보관하는 곳입니다.
  • 모든 비즈니스 로직과 상태 변화를 처리하는 역할을 합니다. 여러 개의 스토어가 존재할 수 있으며, 각 스토어는 특정 데이터나 기능에 대한 상태를 관리합니다.
  • Store는 자신의 상태가 변경되면 뷰(View)에게 변경 사항을 알려줍니다. 뷰는 Store를 직접 수정하지 않고, 필요한 데이터를 Store에서 읽어와 UI를 업데이트합니다.

 

4. View(뷰)

  • 리액트 컴포넌트로 구성된 사용자 인터페이스(UI)입니다.
  • Store로부터 상태를 받아와 렌더링하고, 사용자로부터 입력을 받습니다.
  • 사용자가 View에서 어떤 액션을 취하면, 이는 Action Creator를 호출하여 새로운 Action을 생성하고, 그 Action은 Dispatcher를 통해 스토어로 전달됩니다.

 

Flux 패턴의 데이터 흐름

Flux의 가장 큰 특징은 단방향 데이터 흐름을 강제하는 구조입니다. 다음과 같은 순서로 데이터가 흐릅니다.

 

  1. View에서 사용자 입력이 발생합니다.
  2. Action Creator가 호출되어 Action을 생성합니다.
  3. Action이 Dispatcher에 의해 디스패치됩니다.
  4. Dispatcher는 해당 Action을 모든 Store에 전달합니다.
  5. Store는 Action에 따라 자신의 상태를 업데이트합니다.
  6. Store가 상태 변경을 View에 알리고, View는 새로운 상태에 따라 UI를 재렌더링합니다.

 

Flux 패턴의 장점

  • 예측 가능한 상태 관리: 단방향 데이터 흐름으로 인해 상태 변화가 예측 가능하고 추적하기 쉽습니다.
  • 디버깅 용이성: 상태 변경이 어디서, 어떻게 발생했는지 명확하게 알 수 있어 디버깅이 쉽습니다.
  • 모듈성: 각 요소가 독립적으로 작동하므로 유지보수와 확장이 용이합니다.

 

 

 

 

Flux와 Redux의 차이점

 

Flux와 Redux는 모두 리액트(React) 애플리케이션에서 상태 관리를 위해 사용되는 아키텍처 패턴이지만, 서로 다른 방식으로 구현됩니다. Redux는 Flux의 개념을 기반으로 만들어졌지만, 상태 관리의 복잡성을 줄이고 더 예측 가능하고 직관적인 방식을 제공하기 위해 개선되었습니다. 두 패턴의 차이점을 명확히 이해하기 위해 몇 가지 핵심 요소를 중심으로 살펴보겠습니다.

 

 

 

Flux VS Redux

 

 

 

1. 스토어의 개수와 관리 방식

 

Flux 패턴에서는 여러 개의 스토어(Store)가 존재할 수 있으며, 각 스토어는 애플리케이션의 특정 부분에 대한 상태를 관리합니다. 이 구조는 애플리케이션이 복잡해질수록 각기 다른 기능에 대해 별도의 스토어를 필요로 할 수 있습니다. 반면, Redux는 단 하나의 전역 스토어만 사용합니다. Redux의 모든 애플리케이션 상태는 이 단일 스토어에 저장되고, 상태는 항상 불변성을 유지하도록 설계됩니다. 이러한 단일 스토어 구조는 상태를 예측 가능하게 하고, 애플리케이션의 상태 관리가 단순하고 투명하게 이루어질 수 있도록 돕습니다.

 

 

 

2. 액션의 전달과 상태 변화 처리 방식

 

Flux는 중앙 집중식 허브 역할을 하는 Dispatcher를 통해 액션(Action)을 모든 스토어에 전달합니다. Dispatcher는 각 액션을 스토어로 디스패치하며, 스토어는 해당 액션을 받아 자신의 상태를 업데이트합니다. Redux에서는 Dispatcher가 존재하지 않습니다. 대신, 상태 변화는 Reducer라는 순수 함수에 의해 처리됩니다. Redux에서 액션은 리듀서에 직접 전달되고, 리듀서는 현재 상태와 액션을 기반으로 새로운 상태를 반환합니다. 이 방식은 불필요한 중앙 집중화 요소를 제거하고, 상태 변화를 보다 직관적이고 간결하게 관리할 수 있게 합니다.

 

 

 

3. 상태 불변성에 대한 접근 방식

 

Flux에서는 스토어의 상태가 불변성을 유지할 필요가 없습니다. 스토어가 자신의 상태를 직접 변경할 수 있어, 상태 관리가 다소 자유롭게 이루어집니다. 하지만 Redux에서는 상태 불변성을 강제합니다. 상태는 직접적으로 변경될 수 없으며, 대신 새로운 상태 객체를 반환하여 상태를 업데이트해야 합니다. 이러한 접근 방식은 상태 변화를 예측 가능하게 하고, 상태 관리 중 발생할 수 있는 버그를 줄이는 데 큰 도움이 됩니다.

 

 

 

4. Reducer와 Store의 역할 분담

 

Flux에서 스토어는 상태와 비즈니스 로직을 모두 포함합니다. 각 스토어는 상태를 저장하고, 액션에 따라 상태를 수정하는 로직을 가지고 있습니다. Redux에서는 Reducer가 상태와 액션을 받아 새로운 상태를 반환하는 순수 함수로 동작합니다. 반면, 스토어는 단순히 상태를 보유하는 역할만 수행하며, 상태 변경 로직을 직접 포함하지 않습니다. 이로 인해 Redux는 코드의 예측 가능성과 테스트의 용이성을 크게 향상시킵니다.

 

 

 

5. 미들웨어(Middleware) 지원 여부

 

Flux는 미들웨어 개념을 포함하고 있지 않으며, 비동기 로직 처리는 각 스토어에서 개별적으로 관리해야 합니다. Redux는 다양한 미들웨어를 통해 비동기 로직과 같은 사이드 이펙트(부작용) 처리를 지원합니다. Redux Thunk나 Redux Saga와 같은 미들웨어를 사용하면 비동기 액션을 손쉽게 처리하거나, 상태 변경 전에 로깅을 추가하는 등의 기능을 쉽게 확장할 수 있습니다.

 

 

 

요약하자면, Redux는 Flux의 단방향 데이터 흐름 개념을 차용하면서도 스토어를 단일화하고 상태 불변성을 강제하며, Reducer를 통한 상태 관리를 도입하여 구조를 단순화했습니다. 이를 통해 Redux는 예측 가능성, 디버깅의 용이성, 확장성을 갖춘 강력한 상태 관리 도구로 자리 잡게 되었습니다.

 

 

 

 

Flux와 Redux 사용 예시

Flux와 Redux의 차이점을 예시 코드로 보여드리겠습니다. 두 패턴 모두 리액트에서 상태 관리를 다루는 방식이기 때문에, Flux와 Redux를 간단히 구현하는 예시를 통해 각각의 구조와 동작 방식을 비교해볼 수 있습니다.

 

Flux 패턴 예시

Flux는 여러 개의 스토어와 Dispatcher를 사용하는 구조입니다. 아래는 간단한 To-Do 애플리케이션을 Flux 패턴으로 구현한 예시입니다.

 

 

    Flux 구조

  • Action: 액션을 정의
  • Dispatcher: 액션을 디스패치
  • Store: 상태를 관리
  • View: 리액트 컴포넌트가 View 역할

 

예시 코드

import React, { useState, useEffect } from 'react';

// Dispatcher
const dispatcher = {
  _callbacks: [],
  register(callback) {
    this._callbacks.push(callback);
  },
  dispatch(action) {
    this._callbacks.forEach(callback => callback(action));
  }
};

// Actions
const addTodoAction = (text) => ({
  type: 'ADD_TODO',
  payload: text
});

// Store
const todoStore = {
  _todos: [],
  _listeners: [],
  addChangeListener(listener) {
    this._listeners.push(listener);
  },
  removeChangeListener(listener) {
    this._listeners = this._listeners.filter(l => l !== listener);
  },
  getTodos() {
    return this._todos;
  },
  handleActions(action) {
    switch (action.type) {
      case 'ADD_TODO':
        this._todos.push(action.payload);
        this._listeners.forEach(listener => listener());
        break;
      default:
        break;
    }
  }
};

dispatcher.register(todoStore.handleActions.bind(todoStore));

// View (React Component)
const TodoApp = () => {
  const [todos, setTodos] = useState(todoStore.getTodos());
  const [inputValue, setInputValue] = useState('');

  useEffect(() => {
    const updateTodos = () => setTodos([...todoStore.getTodos()]);
    todoStore.addChangeListener(updateTodos);
    return () => todoStore.removeChangeListener(updateTodos);
  }, []);

  const handleAddTodo = () => {
    dispatcher.dispatch(addTodoAction(inputValue));
    setInputValue('');
  };

  return (
    <div>
      <h2>Flux To-Do List</h2>
      <input value={inputValue} onChange={(e) => setInputValue(e.target.value)} />
      <button onClick={handleAddTodo}>Add Todo</button>
      <ul>
        {todos.map((todo, index) => (
          <li key={index}>{todo}</li>
        ))}
      </ul>
    </div>
  );
};

export default TodoApp;

 

 

 

Redux 패턴 예시

Redux에서는 단일 스토어를 사용하며, 상태는 Reducer에 의해 관리됩니다. 아래는 같은 To-Do 애플리케이션을 Redux 패턴으로 구현한 예시입니다.

 

 

  Redux 구조

  • Store: 애플리케이션의 전체 상태 보유
  • Reducer: 상태와 액션을 받아 새로운 상태를 반환하는 순수 함수
  • Action: 상태를 변경하기 위한 행동을 설명
  • View: 리액트 컴포넌트가 View 역할

 

예시코드

import React from 'react';
import { createStore } from 'redux';
import { Provider, useDispatch, useSelector } from 'react-redux';

// Actions
const ADD_TODO = 'ADD_TODO';

const addTodo = (text) => ({
  type: ADD_TODO,
  payload: text
});

// Reducer
const initialState = {
  todos: []
};

const todoReducer = (state = initialState, action) => {
  switch (action.type) {
    case ADD_TODO:
      return { ...state, todos: [...state.todos, action.payload] };
    default:
      return state;
  }
};

// Store
const store = createStore(todoReducer);

// View (React Component)
const TodoApp = () => {
  const todos = useSelector((state) => state.todos);
  const dispatch = useDispatch();
  const [inputValue, setInputValue] = React.useState('');

  const handleAddTodo = () => {
    dispatch(addTodo(inputValue));
    setInputValue('');
  };

  return (
    <div>
      <h2>Redux To-Do List</h2>
      <input value={inputValue} onChange={(e) => setInputValue(e.target.value)} />
      <button onClick={handleAddTodo}>Add Todo</button>
      <ul>
        {todos.map((todo, index) => (
          <li key={index}>{todo}</li>
        ))}
      </ul>
    </div>
  );
};

// Main App Component
const App = () => (
  <Provider store={store}>
    <TodoApp />
  </Provider>
);

export default App;

 

 

 

 

참고 1. Payload란?

Payload는 프로그래밍과 소프트웨어 개발에서 일반적으로 "데이터의 실질적인 내용" 또는 "전송된 데이터의 유용한 부분"을 의미합니다. 이 개념은 특히 상태 관리와 API 통신에서 많이 사용됩니다. Redux와 같은 상태 관리 라이브러리에서, payload는 액션 객체가 상태를 변경하기 위해 전달하는 실제 데이터를 가리킵니다. 액션 객체는 type과 함께 payload를 포함하는 것이 일반적인 패턴입니다.

 

예를 들어, 아래 리듀서 예시에서 ADD_TODO 액션이 발생하면, 리듀서는 payload에 포함된 새로운 할 일 데이터를 현재 상태에 추가합니다.

 

// 액션 정의

const addTodo = (text) => {
  return {
    type: 'ADD_TODO',
    payload: {
      id: new Date().getTime(), // 각 할 일에 대한 고유 ID
      text: '양치하기' // 할 일의 내용
    }
  };
};

 

// 리듀서 정의

const todoReducer = (state = [], action) => {
  switch (action.type) {
    case 'ADD_TODO':
      return [...state, action.payload]; // 새로운 할 일을 기존 상태에 추가
    default:
      return state;
  }
};

 

 

 

 

참고 2. 보일러 플레이트란?

보일러 플레이트(Boilerplate)란 소프트웨어 개발에서 자주 반복되는 코드의 템플릿이나 기본 골격을 의미합니다. 즉, 프로젝트를 시작하거나 특정 기능을 구현할 때마다 여러 번 반복해서 작성해야 하는 코드 블록을 가리킵니다. 

 

 

리액트(React) 프로젝트를 새로 시작할 때 create-react-app을 사용하면, 기본적인 파일 구조와 설정이 자동으로 생성됩니다. 이때 생성되는 코드와 폴더 구조가 보일러플레이트에 해당합니다. 예를 들어, index.js와 App.js 파일, public 폴더의 기본 HTML 파일 등은 모든 리액트 프로젝트에서 거의 동일한 형태로 존재하는 보일러플레이트 코드입니다.

 

// index.js (리액트 기본 보일러플레이트 예시)
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import './index.css';

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
);

 

위의 예시는 리액트 애플리케이션을 시작하기 위한 기본 코드로, 새로운 프로젝트를 생성할 때마다 거의 동일한 형태로 반복해서 작성됩니다.

 

 

 

 

결론

MVVM은 애플리케이션의 구조적 설계를 돕는 아키텍처 패턴이고, Flux와 Redux는 리액트에서 효율적인 상태 관리를 위한 패턴입니다. 각각의 패턴을 이해하고 적절하게 활용하면, 애플리케이션의 유지보수성과 확장성을 크게 향상시킬 수 있습니다.

 

 

댓글