모던 리액트 딥다이브 서적을 뒤적뒤적 읽다가
리액트가 등장한 이유에 대한 글을 보고
책에서 소개하는 리액트의 포스팅을 읽고 왔다.
해당 포스팅에서는 리액트가 처음 등장 하여 자리를 잡기 전
리액트를 써야 하는 이유에 대해서 쓴 글로 시작 부분부터 다음과 같이 이야기 한다.
React
는 MVC
패턴이 아니다, MVC
패턴이 아니라서 너는 UI
를 더욱 유연하고 재사용 가능하게 할 수 있다.
그 다음엔 React
에서는 View
역할을 하기 위한 template
이 필요 없다고 하며 소개한다.
웹 개발의 역사에 대해서 엄청나게 문외한 입장으로서
React
이전의 웹 개발 형식이 어땠는지에 대해 모르다 보니 해당 글만을 보고 설득 당하진 못했다.왜냐면
React
밖에 모르니까 ㅋㅋㅋㅋㅋㅋㅋㅋㅋ그래서 여러 포스팅과 동영상들에 대해서 공부하고 왔다.
MVC
웹사이트가 매우 단순하게 정보 전달의 역할이 주가 되고 서버 사이드 렌더링 방식이 정석이던 시절
주로 웹 사이트 개발 방식은
서버 단에서 데이터를 처리하고 처리된 로직을 HTML , CSS
로 작성된 템플릿 에서 서버단에 저장된 데이터를 이용해 View
를 생성하여 클라이언트에게 보내주는 방식이였다.
이런 방식 속에서 각 필요한 기능 별로 독립적인 계층 구조를 만들기 위해 등장한 패턴이 MVC
패턴이다.
MVC
패턴MVC
패턴이란 Model - View - Controller
라는 계층적 구조로 나뉘어진 패턴을 의미한다.
웹 페이지를 구성하기 위해선 다음과 같은 것들이 필요하다.
Model
이 담당View
가 담당Model , View
사이에 존재하는 비즈니스 로직을 처리 : Controller
가 담당 Model
Model
구조는 View
에서 사용할 데이터들을 저장하는 자료구조로 다음과 같은 특징을 가져아 한다.
View
에서 필요한 데이터를 모두 가지고 있어야 한다.View , Controller
에 대해서 어떠한 정보도 알지 말아야 한다.View , Controller
의 로직이 처리되면 안되고 단순히 데이터 처리 역할만을 담당해야 한다.View , Controller
에게 알릴 수 있어야 한다.View
View
는 사용자에게 보여지는 화면이라 생각하면 된다.
예전 서버 사이드 렌더링에선 , View
는 단순한 HTML , CSS
들로 이뤄져 Model
이 제공하는 정보를 이용하여 페이지를 보여주는 시각적 역할밖에 하지 못했다.
View
는 Model
이 주는 자료를 개별적으로 저장하면 안되고 , Model
이 제공하는 정보만을 렌더링 해야 한다. View
는 오로지 View
의 역할에만 충실해야 하며, Model , Controller
의 역할에 책임을 지면 안된다.View
는 시각적 표현 뿐 아니라 서버와 소통 할 수 있는 창구의 역할을 하기도 한다. 만약 사용자가 다크모드 테마를 눌렀다고 가정해보자 , 해당 이벤트가 발생하면 이벤트에 맞춰 Model
은 데이터를 변경해야 한다.이렇게 Model
은 View
에게 데이터를 제공하고 , View
는 Model
에게 데이터를 받아 렌더링 하거나 Model
에게 데이터를 전달하는 양방향 데이터 전달 방식을 갖는다.
Controller
웹 페이지는 단순히 데이터를 전달하고 렌더링만 하는 방식이 아니라 다양한 비즈니스 로직이 필요 할 수 있다.
혹은 오고가는 데이터의 전처리가 될 수도 있고 말이다.
이 때 필요한 계층이 Controller
이다.
Controller
는 View , Model
사이에 존재하는 중계자 역할로 Model , View
의 로직을 책임 질 수 있다.
Model , View
의 변경을 모니터링 해야 한다.
좀 더 예시를 들어 생각해보자 ,
View
가 현재 1 페이지에 있을 때 1페이지 버튼을 또 눌렀다고 가정해보자
Controller
가 존재하지 않았다면 Model
은 1 페이지에 대한 정보를 이용해 다시 템플릿을 작성 할 것이고 사용자에게 제공했을 것이다.
하지만 Controller
가 존재했다면 View
로 인해 발생한 이벤트 이후 View
의 현재 상태를 보고 Model
에게 정보를 전달하지 않을 수도 있다.
이처럼 Controller
는 Model , 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
패턴에서 View
과 Model
은 서로에게 데이터를 전송하기도 , 받기도 하는 방식으로 작동한다.
물론 사이에 Controller
라는 중계자를 넣어 직접적인 의존성이 존재하지 않더라도
여전히 간접적인 의존성은 존재한다.
이러한 문제는 예기치 못한 데이터 전송으로 인해 문제가 발생 할 수 있다.
예를 들어 사용자가 본인의 닉네임을 수정했다고 해보자
닉네임을 수정하고 나서 , 다시 본인의 페이지를 보니 수정된 닉네임이 적용이 안됐다.이 때, 닉네임을 수정할 때 문제가 발생했을까 ? 수정된 닉네임을 받아올 때 문제가 발생했을까 ?
양방향 데이터 흐름으로 인해 문제를 추적하기 어렵다.
MVC
패턴은 주로 템플릿이 존재하는 서버단에서 Model
에서 데이터를 템플릿에 변경하여 브라우저에 완성된 View
를 전달하는 방식으로 작동한다.
이는 잦은 네트워크 요청으로 인해 낮은 UX
를 제공 할 수 있다. 이는 비단 MVC
패턴 뿐 아니라 서버사이드 렌더링의 단점이기도 하다.
View
에서 발생 할 수 있는 이벤트가 매우 다양하게 늘어난다고 가정해보자
그렇게 되면 비즈니스 로직이 늘어남에 따라 Controller
가 매우 거대해질 수 밖에 없다.
거대해진 모듈은 관리를 힘들게 한다.
혹은 하나의 View
에서 여러 Model
에서 데이터를 제공 받는다고 가정해보자
그렇다면 데이터의 흐름은 제공받는 Model
의 개수에 따라 2배씩 증가하여 더욱 추적 하기 어렵게 만든다.
이러한 상황을 Massive View Controller
라고 한다.
Model
이 늘어남에 따라 데이터의 흐름이 늘어나고 , 컨트롤러의 역할은 더욱 커진다.
결국 웹 앱이 복잡해짐에 따라
복잡한 데이터 흐름을 최대한 깔끔하게 유지해야 하고, 컨트롤러가 하나의 역할에 대한 책임을 질 수 있도록 새로운 아키텍쳐의 필요성이 대두되었다.
현재의
Controller
만 보더라도 하는 일이 한 두개가 아니다.View , Model
서로가 서로의 존재를 몰라야 하기 때문에Controller
에서 두 계층간의 소통을 중계하기 위해 여러 가지 일을 하고 있는 모습을 볼 수 있다.
Flux
패턴Flux
패턴은 기존 양방향 데이터 흐름과 뚱뚱해지는 컨트롤러 들로 인해 발생하는 단점들을
보완한 패턴으로 MVC
패턴에 비해 몇 가지 추가된 여러 계층들이 존재한다.
Flux
패턴은 리액트의 상태 관리와 가장 관련있는 패턴으로
useReducer
나 Redux
등을 이용해봤다면 익숙한 패턴이다.
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
Store
는 View
에게 제공하기 위한 정보들을 저장하고 있으며, 저장된 정보를 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;
Dispatcher
는 action
객체를 받아 actionHandlers
에 담긴 콜백함수를 실행한다.
actionHandlers
는 register
라는 메소드를 통해 핸들러가 담긴 배열이다.
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
에 핸들러로 등록되는 것은 Store
의 handleActions
메소드이다.
즉, 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
을 받아 Dispatcher
의 dispatch
메소드를 실행시키는 순수한 자바스크립트 객체이다.
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.Lisnter
에 render
메소드를 등록시키는 모습을 볼 수 있다.
이를 통해 Store
의 데이터가 변경되면 View
의 render
메소드가 실행되도록 설계되어있다.
엔트리파일에선 이렇게 생성된 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
패턴을 활용하여 컴포넌트를 더욱 예측 조작하고 생성 할 수 있도록 만들었다.