웹앱의 MVC 패턴의 한계와 리액트의 Flux 패턴

ChoiYongHyeun·2024년 5월 8일
1

리액트

목록 보기
22/31
post-thumbnail

모던 리액트 딥다이브 서적을 뒤적뒤적 읽다가

리액트가 등장한 이유에 대한 글을 보고

책에서 소개하는 리액트의 포스팅을 읽고 왔다.

Why did we build React?

해당 포스팅에서는 리액트가 처음 등장 하여 자리를 잡기 전

리액트를 써야 하는 이유에 대해서 쓴 글로 시작 부분부터 다음과 같이 이야기 한다.

ReactMVC 패턴이 아니다, MVC 패턴이 아니라서 너는 UI 를 더욱 유연하고 재사용 가능하게 할 수 있다.

그 다음엔 React 에서는 View 역할을 하기 위한 template 이 필요 없다고 하며 소개한다.

웹 개발의 역사에 대해서 엄청나게 문외한 입장으로서 React 이전의 웹 개발 형식이 어땠는지에 대해 모르다 보니 해당 글만을 보고 설득 당하진 못했다.

왜냐면 React 밖에 모르니까 ㅋㅋㅋㅋㅋㅋㅋㅋㅋ

그래서 여러 포스팅과 동영상들에 대해서 공부하고 왔다.

과거의 웹 개발 패턴 : MVC


웹사이트가 매우 단순하게 정보 전달의 역할이 주가 되고 서버 사이드 렌더링 방식이 정석이던 시절

주로 웹 사이트 개발 방식은

서버 단에서 데이터를 처리하고 처리된 로직을 HTML , CSS 로 작성된 템플릿 에서 서버단에 저장된 데이터를 이용해 View 를 생성하여 클라이언트에게 보내주는 방식이였다.

이런 방식 속에서 각 필요한 기능 별로 독립적인 계층 구조를 만들기 위해 등장한 패턴이 MVC 패턴이다.

MVC 패턴


MVC 패턴이란 Model - View - Controller 라는 계층적 구조로 나뉘어진 패턴을 의미한다.

웹 페이지를 구성하기 위해선 다음과 같은 것들이 필요하다.

  1. 웹 페이지를 구성하기 위한 데이터 : Model 이 담당
  2. 웹 페이지를 렌더링 하기 위한 템플릿 : View 가 담당
  3. Model , View 사이에 존재하는 비즈니스 로직을 처리 : Controller 가 담당

Model

Model 구조는 View 에서 사용할 데이터들을 저장하는 자료구조로 다음과 같은 특징을 가져아 한다.

  • View 에서 필요한 데이터를 모두 가지고 있어야 한다.
  • View , Controller 에 대해서 어떠한 정보도 알지 말아야 한다.
    즉 , View , Controller 의 로직이 처리되면 안되고 단순히 데이터 처리 역할만을 담당해야 한다.
  • 데이터의 변경이 일어나면 변경된 데이터에 대해서 View , Controller 에게 알릴 수 있어야 한다.

View

View 는 사용자에게 보여지는 화면이라 생각하면 된다.

예전 서버 사이드 렌더링에선 , View 는 단순한 HTML , CSS 들로 이뤄져 Model 이 제공하는 정보를 이용하여 페이지를 보여주는 시각적 역할밖에 하지 못했다.

  • ViewModel 이 주는 자료를 개별적으로 저장하면 안되고 , Model 이 제공하는 정보만을 렌더링 해야 한다.
  • View 는 오로지 View 의 역할에만 충실해야 하며, Model , Controller 의 역할에 책임을 지면 안된다.
  • 변경이 일어나면 변경 통지에 대한 처리 방법을 구현해야 한다.
    View 는 시각적 표현 뿐 아니라 서버와 소통 할 수 있는 창구의 역할을 하기도 한다. 만약 사용자가 다크모드 테마를 눌렀다고 가정해보자 , 해당 이벤트가 발생하면 이벤트에 맞춰 Model 은 데이터를 변경해야 한다.

이렇게 ModelView 에게 데이터를 제공하고 , ViewModel 에게 데이터를 받아 렌더링 하거나 Model 에게 데이터를 전달하는 양방향 데이터 전달 방식을 갖는다.

Controller

웹 페이지는 단순히 데이터를 전달하고 렌더링만 하는 방식이 아니라 다양한 비즈니스 로직이 필요 할 수 있다.

혹은 오고가는 데이터의 전처리가 될 수도 있고 말이다.

이 때 필요한 계층이 Controller 이다.

  • ControllerView , Model 사이에 존재하는 중계자 역할로 Model , View 의 로직을 책임 질 수 있다.

  • Model , View 의 변경을 모니터링 해야 한다.

좀 더 예시를 들어 생각해보자 ,

View 가 현재 1 페이지에 있을 때 1페이지 버튼을 또 눌렀다고 가정해보자

Controller 가 존재하지 않았다면 Model 은 1 페이지에 대한 정보를 이용해 다시 템플릿을 작성 할 것이고 사용자에게 제공했을 것이다.

하지만 Controller 가 존재했다면 View 로 인해 발생한 이벤트 이후 View 의 현재 상태를 보고 Model 에게 정보를 전달하지 않을 수도 있다.

이처럼 ControllerModel , View 의 중계자 역할을 하기 위해 두 계층과 강한 의존성을 가지고 있다.

자바스크립트 코드로 알아보는 MVC 패턴

사실 백문이 불여일견이라고 챗 지피티 목을 조른 후 MVC 패턴에 대한 예시를 가져와봤다.

완전 정통 MVC 패턴과 다를 수 있다. 대부분 MVC 패턴은 자바와 JSP 로 구성되어 있으나 나는 해당 언어를 모른다.

매우 단순하게 body 를 클릭하면 Model 이 가지고 있는 name 값이 변경되어 렌더링이 다르게 되는 예시를 가져와봤다.

Model

// Model
export default class Model {
  constructor(name) {
    this.name = name;
  }

  getName() {
    return this.name;
  }

  setName(name) {
    this.name = name;
  }
}

Model 은 렌더링 하기 위한 데이터를 저장하는 저장소이다.

View

// View
export default class View {
  constructor() {
    this.userNameDisplay = document.createElement('div');
    document.body.appendChild(this.userNameDisplay);
  }

  render(userName) {
    this.userNameDisplay.textContent = `User Name: ${userName}`;
  }
}

View 에선 단순하게 userName 을 받아 해당 userName 일 렌더링 하는 역할만을 담당하며

다른 계층 (Model , Controller 등에 대한 정보는 모르고 있다.)

Controller

// Controller
export default class Controller {
  constructor(model, view) {
    // Controller 는 model ,view 모두 알고 있음
    this.model = model;
    this.view = view;

    // 데이터의 흐름 model -> controller -> view
    this.view.render(this.model.getName()); // 초기 뷰 렌더링
    this.initEventListeners(); // 이벤트 리스너 설정
  }

  initEventListeners() {
    document.addEventListener('click', () => {
      const newName = prompt('Enter new name:');
      if (newName) {
        // 데이터의 흐름 view -> controller -> model
        this.model.setName(newName);
        // 데이터의 흐름 model -> controller -> view
        this.view.render(this.model.getName());
      }
    });
  }
}

이후 컨트롤러는 Model , View 와 모두 연결되어 있으며

필요한 비즈니스 로직을 해당 계층에서 처리한다.

이를 통해 MVC 패턴으로 구현된 app 이란 웹 페이지가 생성되었다.

이러한 패턴은 복잡한 웹 앱의 로직을 독립적인 계층 구조의 조합으로 나눔으로서

책임과 역할을 확실하게 분리하여 모든 로직을 하나의 계층에서 처리 할 때 보다 비교적 유연한 아키텍쳐를 가진 웹 페이지를 만들 수 있었다.

웹 앱에서의 MVC 패턴의 한계

이전과 같이 단순한 구조의 웹 페이지에서 MVC 패턴은 충분히 잘 작동했다.

하지만 웹 페이지가 점점 웹 앱의 형태로 변경되어 가고

웹 페이지에서 매우 많은 데이터 (상태) 변경과 이벤트가 발생함에 따라 여러 문제들이 발생했다.

양방향 데이터 전달 방식

MVC 패턴에서 ViewModel 은 서로에게 데이터를 전송하기도 , 받기도 하는 방식으로 작동한다.

물론 사이에 Controller 라는 중계자를 넣어 직접적인 의존성이 존재하지 않더라도

여전히 간접적인 의존성은 존재한다.

이러한 문제는 예기치 못한 데이터 전송으로 인해 문제가 발생 할 수 있다.

예를 들어 사용자가 본인의 닉네임을 수정했다고 해보자
닉네임을 수정하고 나서 , 다시 본인의 페이지를 보니 수정된 닉네임이 적용이 안됐다.

이 때, 닉네임을 수정할 때 문제가 발생했을까 ? 수정된 닉네임을 받아올 때 문제가 발생했을까 ?

양방향 데이터 흐름으로 인해 문제를 추적하기 어렵다.

성능 문제

MVC 패턴은 주로 템플릿이 존재하는 서버단에서 Model 에서 데이터를 템플릿에 변경하여 브라우저에 완성된 View 를 전달하는 방식으로 작동한다.

이는 잦은 네트워크 요청으로 인해 낮은 UX 를 제공 할 수 있다. 이는 비단 MVC 패턴 뿐 아니라 서버사이드 렌더링의 단점이기도 하다.

확장성 제한

View 에서 발생 할 수 있는 이벤트가 매우 다양하게 늘어난다고 가정해보자

그렇게 되면 비즈니스 로직이 늘어남에 따라 Controller 가 매우 거대해질 수 밖에 없다.

거대해진 모듈은 관리를 힘들게 한다.

혹은 하나의 View 에서 여러 Model 에서 데이터를 제공 받는다고 가정해보자

그렇다면 데이터의 흐름은 제공받는 Model 의 개수에 따라 2배씩 증가하여 더욱 추적 하기 어렵게 만든다.

이러한 상황을 Massive View Controller 라고 한다.

Model 이 늘어남에 따라 데이터의 흐름이 늘어나고 , 컨트롤러의 역할은 더욱 커진다.

결국 웹 앱이 복잡해짐에 따라

복잡한 데이터 흐름을 최대한 깔끔하게 유지해야 하고, 컨트롤러가 하나의 역할에 대한 책임을 질 수 있도록 새로운 아키텍쳐의 필요성이 대두되었다.

현재의Controller 만 보더라도 하는 일이 한 두개가 아니다. View , Model 서로가 서로의 존재를 몰라야 하기 때문에 Controller 에서 두 계층간의 소통을 중계하기 위해 여러 가지 일을 하고 있는 모습을 볼 수 있다.

Flux 패턴

Flux 패턴은 기존 양방향 데이터 흐름과 뚱뚱해지는 컨트롤러 들로 인해 발생하는 단점들을

보완한 패턴으로 MVC 패턴에 비해 몇 가지 추가된 여러 계층들이 존재한다.

Flux 패턴은 리액트의 상태 관리와 가장 관련있는 패턴으로

useReducerRedux 등을 이용해봤다면 익숙한 패턴이다.

Controller 에서 Action,Dispatcher 의 조합으로 변경

이전 MVC 패턴에선 하나의 Controller 에서 Model 의 데이터 변경 (이하 상태 변경) 을 관리했다면

Flux 패턴에선 Action ,Dispatcher 라는 두 가지 계층으로 나눠 처리한다.

Action

Action 계층은 발생한 이벤트와 , 저장할 상태를 담은 Action 객체를 생성하는 계층이다.

View 에서 발생한 이벤트는 Action 계층으로 전달되며 ACtion 계층은 발생한 이벤트에 맞게 Action 객체를 생성하여 Dispatcher 객체로 전달한다.

Dispatcher

Dispatcher 계층에선 Action 객체를 받아 필요한 비즈니스 로직을 처리하고

해당 상태를 Model 에 해당하는 Store 에 저장한다.

즉, Dispatcher 의 역할은 Action 객체에 저장된 값에 따라 Store 에 저장한다.

Store

StoreView 에게 제공하기 위한 정보들을 저장하고 있으며, 저장된 정보를 View 에게 제공한다.

클라이언트 사이드 렌더링 방식으로 변경됨에 따라 Store 는 페이지 별 뿐 아니라, DOM node 별로 , 해당 View 에게 필요한 정보를 제공한다.

View


View 는 하나의 거대한 템플릿이 될 수도 있지만 , 웹 페이지 구성요서에 해당하는 DOM 혹은 DOM 들이 모여 이뤄진 컴포넌트를 의미 한다.

View 들은 Store 에게서 필요한 상태를 전달받아 렌더링 하기도 하며

발생한 이벤트 (상태를 변경시키는) 를 Action 에게 전달한다.

자바스크립트 코드로 알아보는 Flux 패턴


Dispatcher

class DispatcherClass {
  constructor() {
    this.isDispatching = false;
    this.actionHandlers = [];
  }

  dispatch(action) {
    if (!this.isDispatching) {
      this.isDispatching = true;
      this.actionHandlers.forEach((handler) => handler(action));
      this.isDispatching = false;
    }
  }

  register(handler) {
    this.actionHandlers.push(handler);
  }
}

const Dispatcher = new DispatcherClass();

export default Dispatcher;

Dispatcheraction 객체를 받아 actionHandlers 에 담긴 콜백함수를 실행한다.

actionHandlersregister 라는 메소드를 통해 핸들러가 담긴 배열이다.

Store

import Dispatcher from './Dispatcher.js';

class StoreClass {
  constructor() {
    this.user = { name: 'John Doe' };
    this.listeners = [];

    Dispatcher.register(this.handleActions.bind(this));
  }

  handleActions(action) {
    switch (action.type) {
      case 'CHANGE_NAME':
        this.user.name = action.payload;
        this.emitChange();
        break;
      default:
    }
  }

  addChangeListener(callback) {
    this.listeners.push(callback);
  }

  removeChangeListener(callback) {
    this.listeners = this.listeners.filter((listener) => listener !== callback);
  }

  emitChange() {
    this.listeners.forEach((listener) => listener());
  }

  getUser() {
    return this.user.name;
  }
}

const Store = new StoreClass();

export default Store;

Dispatcher 에 핸들러로 등록되는 것은 StorehandleActions 메소드이다.

즉, Dispatcher 에서 Action객체가 등록되면 Store 에 담긴 정보가 업데이트 된다는 것을 알 수 있다.

handleActions내부를 보면emitChange 란 메소드가 존재하는데

해당 메소드는 handleActions 가 시행 된 후 시행 될 콜백 함수들이 담긴 배열을 순회하며 콜백함수들을 시행한다.

Action

import Dispatcher from './Dispatcher.js';

const Actions = {
  changeName(newName) {
    Dispatcher.dispatch({
      type: 'CHANGE_NAME',
      payload: newName,
    });
  },
};

export default Actions;

Actions 객체는 newName 을 받아 Dispatcherdispatch 메소드를 실행시키는 순수한 자바스크립트 객체이다.

View

import Actions from './Action.js';
import Store from './Store.js';

class ViewClass {
  constructor() {
    this.userNameDisplay = document.createElement('div');
    document.body.appendChild(this.userNameDisplay);

    Store.addChangeListener(this.render.bind(this));
  }

  render() {
    this.userNameDisplay.textContent = `User Name: ${Store.getUser()}`;
  }
}

const View = new ViewClass();

export default View;

View 클래스는 브라우저에 띄울 DOM 노드들을 생성하고 render 메소드를 통해 브라우저에 렌더링 한다.

이 때 contsructor 단계를 살펴보면 Store.Lisnterrender 메소드를 등록시키는 모습을 볼 수 있다.

이를 통해 Store 의 데이터가 변경되면 Viewrender 메소드가 실행되도록 설계되어있다.

엔트리파일에선 이렇게 생성된 View 클래스를render 를 통해 실행시켜주기만 하면 된다.

import View from './View.js';

document.addEventListener('click', () => {
  const newName = prompt('Enter new name:');
  if (newName) {
	Actions.changeName(newName);
  }
});

View.render();

MVC 패턴과 다르게 무엇이 달라졌고 뭐가 그리 대단할까 ?


Flux 패턴의 가장 큰 특징은 Controller 계층이 Action , Dispatcher 라는 계층으로 나뉘어졌다는 것일 것이다.

이전 MVC 패턴에서 Controller 는 데이터를 View -> Model , Model -> View 두 방향으로 보내는 양방향 데이터 흐름을 갖는 계층이였다.

이를 통해 Controller 계층은 View , Model 모두 신경써야 하며 , 데이터의 흐름이 양방향이라 Controller 자체가 무거울뿐더러 코드의 흐름을 파악하기 쉽지 않다.

하지만 Flux 패턴에서 각 계층의 데이터 흐름은 단방향으로 흐른다. 이를 통해 데이터의 이동 흐름을 한 눈에 파악하기도 쉬울 뿐더러 단방향적 의존성을 갖는 단순한 계층들이기에

새로운 계층을 추가하거나, 수정하는 등의 행위에서 MVC 패턴에 비해 자유롭다.

이전 문제가 발생했던 닉네임 수정 때를 예시로 든다면
데이터를 Store 에 보내는 순간 문제가 발생했는지를 확인하려면 단순히 Action , Dispatcher 계층을 살펴보면 되고

데이터를 받아오는데 문제가 발생했는지를 보겠다면 Store -> View 에서의 문제만 확인하면 된다.

위 이미지 예시는 Flux 패턴으로 구성된 Redux 의 사용 예시이다 .

Action -> Dispatcher -> Store 로 데이터 흐름이 존재 할 때

이전 Store , 현재 생성된 Action 객체, 이후 Store 의 모습등을 파악하는 것이 가능하다.

이처럼 리액트는 Flux 패턴을 활용하여 컴포넌트를 더욱 예측 조작하고 생성 할 수 있도록 만들었다.

profile
빨리 가는 유일한 방법은 제대로 가는 것이다

0개의 댓글