[React] Flux Architecture

seohyun Kang·2022년 9월 5일
0

React

목록 보기
3/9
post-thumbnail

Flux란?

Flux는 Facebook에서 단 방향 데이터 흐름을 만들기 위해 고안한 어플리케이션 아키텍쳐입니다. 특히, 기존의 프레임워크와 다르게 패턴* 의 형태를 하고 있기 때문에 수 많은 코드의 작성없이 바로 Flux를 통해 활용할 수 있습니다.

패턴(Pattern) 혹은 디자인 패턴(Design Pattern)이란?

구성요소

Flux 어플리케이션은 Dispatcher, Stores, Views(React Components) 세가지 부분으로 구성되어 있습니다. 여기서 주의할 점은 기존의 MVC* 모델과 혼동해서는 안된다는 것입니다.

MVC 모델 (Model-View-Controller Model)이란?

물론, Controller도 Flux 어플리케이션에 존재하지만 위계의 최상위에서 Controller-Views의 하위에 Views 관계로 존재하고 있습니다.

Controller-Views는 Stores에서 데이터를 가져와 그 데이터를 자식에게 보내는 역할을 합니다.

MVC Model

Flux Model

Flux는 데이터 흐름이 단방향!

MVC Model에서 Controller와 View가 서로 상호작용하여 Model을 변경하는 것과는 다르게 Flux의 View에서 사용자가 상호작용을 할 때, 그 view는 중앙의 dispatcher를 통해 action을 전파하게 됩니다. 또한, 어플리케이션의 데이터와 비지니스 로직을 가지고 있는 store는 action이 전파되면 이 action에 영향이 있는 모든 view를 갱신하며, 이 방식은 특히 React의 선언형 프로그래밍 스타일에서 view가 어떤 방식으로 갱신해야 되는지 일일이 작성하지 않고서도 데이터를 변경할 수 있는 형태에서 편리합니다.

이유

MVC 모델을 적용한 경우 대형 MVC 어플리케이션에서 종종 나타나는 데이터 간의 의존성과 연쇄적인 갱신은 뒤얽힌 데이터 흐름을 만들고 예측할 수 없는 결과로 이끌게 되어 React Framework를 개발한 Meta(구 Facebook)에서 위와 같은 아키텍쳐를 구상하게 되었다고 합니다.

구성 요소

위에 언급한 바와 같이 Flux는 크게 Dispatcher, Stores, Views + Controller-View를 가지게 되는 것 같습니다. 각각의 요소에 대해서 알아보겠습니다.

1. Dispatcher

Flux 어플리케이션의 중앙 허브로 모든 데이터 흐름을 관리한다.

본질적으로 Store의 Callback을 등록하는데 쓰이고 Action을 Store에 배분해주는 역할을 하며 각각의 Store를 직접 등록하고 콜백을 제공합니다. 이를 통해, Action creator가 새로운 Action이 있다고 Dispatcher에게 알려주면 어플리케이션에 있는 모든 Store는 해당 Action을 앞서 등록한 Callback으로 전달 받는다.

어플리케이션의 규모가 커지게 되면 Dispachter의 역할은 더욱 필수적이다. Store 간에 의존성을 특정적인 순서로 Callback을 실행하는 과정으로 관리하기 때문이다. Store는 다른 Store의 업데이트가 끝날 때까지 선언적으로 기다릴 수 있고 끝나는 순서에 따라 스스로 갱신된다.

Example

가상의 비행 목적지 양식에서 국가를 선택했을 때 기본 도시를 선택하는 예를 보자 :
해당 예시는 상태 변경은 도시 => 국가 순서로 변경하지만, 국가 => 도시 => 비용 순서로 상태가 업데이트됩니다.
Example Link

var flightDispatcher = new Dispatcher();

// 어떤 국가를 선택했는지 계속 추적한다
var CountryStore = {country: null};

// 어느 도시를 선택했는지 계속 추적한다
var CityStore = {city: null};

// 선택된 도시의 기본 항공료를 계속 추적한다
var FlightPriceStore = {price: null};

사용자가 선택한 도시를 변경하면 데이터를 전달한다:

flightDispatcher.dispatch({
  actionType: 'city-update',
  selectedCity: 'paris'
});

이 데이터 변동은 CityStore가 소화한다:

flightDispatcher.register(function(payload) {
  if (payload.actionType === 'city-update') {
    CityStore.city = payload.selectedCity;
  }
});

사용자가 국가를 선택하면 데이터를 전달한다:

flightDispatcher.dispatch({
  actionType: 'country-update',
  selectedCountry: 'australia'
});

이 데이터는 두 store에 의해 소화된다:

 CountryStore.dispatchToken = flightDispatcher.register(function(payload) {
  if (payload.actionType === 'country-update') {
    CountryStore.country = payload.selectedCountry;
  }
});

CountryStore가 등록한 콜백을 업데이트 할 때 반환되는 토큰을 참조값으로 저장했다. 이 토큰은 waitFor() 에서 사용할 수 있고 CityStore가 갱신하는 것보다 먼저 CountryStore를 갱신하도록 보장할 수 있다.

CityStore.dispatchToken = flightDispatcher.register(function(payload) {
  if (payload.actionType === 'country-update') {
    // `CountryStore.country`는 업데이트 되지 않는다
    flightDispatcher.waitFor([CountryStore.dispatchToken]);
    // `CountryStore.country`는 업데이트가 될 수 있음이 보장되었다

    // 새로운 국가의 기본 도시를 선택한다
    CityStore.city = getDefaultCityForCountry(CountryStore.country);
  }
});

waitFor()는 다음과 같이 묶을 수 있다:

FlightPriceStore.dispatchToken =
  flightDispatcher.register(function(payload) {
    switch (payload.actionType) {
      case 'country-update':
      case 'city-update':
        flightDispatcher.waitFor([CityStore.dispatchToken]);
        FlightPriceStore.price =
          getFlightPriceStore(CountryStore.country, CityStore.city);
        break;
  }
});

country-update는 콜백이 등록된 순서 즉 CountryStore, CityStore, FlightPriceStore 순서로 실행되도록 보장된다.

2. Store

Store는 어플리케이션의 상태와 로직을 포함하고 있다. Store의 역할은 전통적인 MVC의 모델과 비슷하지만 많은 객체의 상태를 관리할 수 있는데 ORM 모델* 이 하는 것처럼 단일 레코드의 데이터를 표현하는 것도 아니고 Backbone* 의 컬랙션과도 다르다. Store는 단순히 ORM 스타일의 객체 컬랙션을 관리하는 것을 넘어 어플리케이션 내의 개별적인 도메인 에서 어플리케이션의 상태를 관리한다.

ORM Model이란?

Backbone Collection이란?

Store는 자신을 Dispatcher에 등록하고 Callback을 제공하며 이 Callback은 Action을 파라미터로 받습니다. Store의 등록된 Callback의 내부에서는 Switch문을 사용한 Action 타입을 활용해서 Action을 해석하고 Store 내부 메소드에 적절하게 연결될 수 있는 훅을 제공한다. 여기서 결과적으로 Action은 Disaptcher를 통해 Store의 상태를 갱신한다. Store가 업데이트 된 후, 상태가 변경되었다는 이벤트를 중계하는 과정으로 View에게 새로운 상태를 보내주고 View 스스로 업데이트하게 만든다.

Example

Dispatcher에 있는 예제를 참고하면, country-update Action과 데이터를 파라미터로 받습니다.

var CountryStore = {country: null};

flightDispatcher.dispatch({
  actionType: 'country-update',
  selectedCountry: 'australia'
});

country-upate Action이 발생했을 때, CountryStore에 dispatchToken을 생성합니다.


// country-update Action이 발생했을 때, Action을 Param으로 받는 Callback
CountryStore.dispatchToken = flightDispatcher.register(function(payload) {
  if (payload.actionType === 'country-update') {
    CountryStore.country = payload.selectedCountry;
  }
});

country-update Action이 발생했을 때, 위의 country-update Action이 발생했을 때 생성한 Token을 waitFor()에서 사용하고 CountryStore가 변경되면 getDefaultCityForCountry 함수로 변경된 CountryStore.country 값으로 해당 국가의 도시 목록을 가져옵니다.

CityStore.dispatchToken = flightDispatcher.register(function(payload) {
  if (payload.actionType === 'country-update') {
    // `CountryStore.country`는 업데이트 되지 않는다
    flightDispatcher.waitFor([CountryStore.dispatchToken]);
    // `CountryStore.country`는 업데이트가 될 수 있음이 보장되었다

    // 새로운 국가의 기본 도시를 선택한다
    CityStore.city = getDefaultCityForCountry(CountryStore.country);
  }
});

3. Actions

Dispatcher는 Action을 호출해 데이터를 불러오고 Store로 전달할 수 있도록 메소드를 제공한다. Action의 생성은 Dispatcher로 Action을 보낼 때 의미있는 헬퍼 메소드로 포개진다.

할 일 목록 어플리케이션에서 할 일 아이템의 문구를 변경하고 싶다고 가정하자. updateText(todoId, newText)와 같은 함수 시그니쳐를 이용해 TodoActions 모듈 내에 Action을 만든다. 이 메소드는 View의 이벤트 핸들러로부터 호출되어 실행할 수 있고 그 결과로 사용자 상호작용에 응답할 수 있게 된다. 이 Action creator 메소드는 type 을 추가할 수 있다. 이 type을 이용해 Action이 Store에서 해석될 수 있도록, 적절한 응답이 가능하도록 한다. 예시에서와 같이 TODO_UPDATE_TEXT와 같은 이름의 타입을 사용한다.

Action은 서버와 같은 다른 장소에서 올 수 있다. 예를 들면 data를 초기화 할 때 이런 과정이 발생할 수 있다. 또한 서버에서 에러 코드를 반환하거나 어플리케이션이 제공된 후에 업데이트가 있을 때 나타날 수 있다.

4. Controller-Views & Views

React는 조화롭고 자유로운 형태로 다시 랜더링 할 수 있는 View를 View 레이어로 제공한다. 복잡한 View 위계의 상위를 살펴보면 Store에 의해 이벤트를 중계할 수 있는 특별한 종류의 View가 있다. 이 View를 Controller-View라고 부르는데 Store에서 데이터를 얻을 수 있는 glue 코드를 제공하고 데이터를 위계대로 자식들에게 전달하도록 돕는다. 페이지의 광범위한 영역을 관리하는 Contoller-View를 가지게 된다.

Store에게 이벤트를 받으면 Store의 퍼블릭 getter 메소드를 통해 새로 필요한 데이터를 처음으로 요청하게 된다. 그 과정에서 setState() 또는 forceUpdate() 메소드를 호출하게 되고 그 호출 과정에서 자체의 render() 메소드와 하위 모든 자식의 render() 메소드를 실행한다.

전체적인 Store의 상태를 단일 객체로 만들어 하위에 있는 View에 전달하게 되는데 다른 자식들도 필요한 부분이라면 데이터를 사용할 수 있도록 한다. 또한 Controller-View는 위계의 최상위에서 마치 Controller와 같은 역할을 지속적으로 수행해 하위에 있는 View가 가능한 한 순수하게, 함수적으로 유지될 수 있도록 한다. 또한 Store의 전체 상태를 단일 객체로 흘려 보내는데 이 방식은 관리해야 하는 프로퍼티 수를 줄이는 효과도 있다.

때때로 컴포넌트의 단순함을 유지하기 위해 위계 깊은 곳에서 Contoller-Views가 추가적으로 필요할 때가 있다. 중간에 Contoller-Views를 넣으면 특정 데이터 도메인에 관계된 위계 영역을 감싸서 독립적으로 만드는데(encapsulate) 도움이 된다. 하지만 조심해야 한다. 위계 내에서 만든 Controller-View는 단일의 데이터 흐름과 상충해 잠재적으로 새로운 데이터 흐름의 시작점에서 충돌할 수 있다.

내부에 Controller-View를 추가하는 것을 결정할 때에는 여러 데이터 업데이트의 흐름이 위계와 다른 방향으로 흐르지 않도록 고려해 단순함의 균형을 유지해야 한다. 여러 데이터가 업데이트 되면 이상한 효과를 만들어 React의 렌더링 메소드가 다른 Controller-View에 의해 반복적으로 실행되서 디버깅의 어려움을 가중할 가능성이 있다. 내부 Controller-View를 만드는 것을 결정할 때, 데이터를 갱신하기 위해 위계에서 여러 방향으로 흐르는 복잡성에 반해 단순한 컴포넌트의 이점에서 균형을 찾아야 한다. 여러 방향으로의 데이터 갱신은 이상한 효과를 만들 수 있다. 특히 React의 렌더 메소드는 여러 Controller-View를 갱신하기 위해 반복적으로 실행이 되어버려 디버깅의 어려움을 가중할 수도 있다.

Finally

문서는 읽었으니 이제 Tutorial ToDo List 작업을 통해 프로젝트에 어떻게 적용해야할지 익혀봐야겠습니다.

(미비한 부분은 추후 todolist 작업 후에 새로 업데이트 하려고 합니다.)


Reference : What is the flux architecture

0개의 댓글