본문 바로가기

프로그래밍(TA, AA)/자바스크립트

[자바스크립트] flux 개념 이해하기

flux 설명: https://haruair.github.io/flux/docs/overview.html



출처는 haruair.github.io 사이트 입니다. 아래는 학습용으로 작성한 포스팅입니다.



Flux는 Facebook에서 클라이언트-사이드 웹 애플리케이션을 만들기 위해 사용하는 애플리케이션 아키텍처입니다. 단방향 데이터 흐름을 활용해 뷰 컴포넌트를 구성하는 React를 보완하는 역할을 합니다. 이전까지의 프레임워크와는 달리 패턴과 같은 모습을 하고 있기 때문에 수많은 새로운 코드를 작성할 필요없이 바로 Flux를 이용해 사용할 수 있습니다.


Flux 애플리케이션은 다음 핵심적인 세가지 부분으로 구성되어 있습니다: Dispatcher, Stores, Views(React 컴포넌트). Model-View-Controller와 혼동해서는 안됩니다. Controller도 물론 Flux 어플리케이션에 존재하지만 위계의 최상위에서 controller-views-views 관계로 존재하고 있습니다. 이 controller-views는 stores에서 데이터를 가져와 그 데이터를 자식에게 보내는 역할을 합니다. 덧붙여 dispatcher를 돕는 action creator 메소드는 이 어플리케이션에서 가능한 모든 변화를 표현하는 유의적 API를 지원하는데 사용됩니다. Flux 업데이트 주기의 4번째 부분이라고 생각하면 유용합니다.


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


이 프로젝트는 파생되는 데이터를 올바르게 다루기 위해 시작되었습니다. 예를 들면 현재 뷰에서 읽지 않은 메시지가 강조되어 있으면서도 읽지 않은 메시지 수를 상단 바에 표시하고 싶었습니다. 이런 부분은 MVC에서 다루기 어려운데 메시지를 읽기 위한 단일 스레드에서 메시지 스레드 모델을 갱신해야하고 동시에 읽지 않은 메시지 수 모델을 갱신해야하기 때문입니다. 대형 MVC 어플리케이션에서 종종 나타나는 데이터 간의 의존성과 연쇄적인 갱신은 뒤얽힌 데이터 흐름을 만들고 예측할 수 없는 결과로 이끌게 됩니다.


Flux는 store를 이용해 제어를 뒤집었습니다. 일관성을 유지한다는 명목으로 외부의 갱신에 의존하는 방식과 달리 Store는 갱신을 받아들이고 적절하게 조화합니다. Store 바깥에 아무것도 두지 않는 방식으로 데이터의 도메인을 관리해야할 필요가 없어져 외부의 갱신에 따른 문제를 명확하게 분리할 수 있도록 돕습니다. Store는 독립적인 세계를 가지고 있어 setAsRead()와 같은 직접적인 setter 메소드가 없는 대신 dispatcher에 등록한 콜백을 통해 데이터를 받게 됩니다.



구조와 데이터 흐름

Flux 어플리케이션에서의 데이터는 단방향으로 흐릅니다.



단방향 데이터 흐름은 Flux 패턴의 핵심인데 위 다이어그램은 Flux 프로그래머를 위한 제일의 멘탈 모델이 됩니다. dispatcher, store과 view는 독립적인 노드로 입력과 출력이 완전히 구분됩니다. action은 새로운 데이터를 포함하고 있는 간단한 객체로 type 프로퍼티를 구분할 수 있습니다.


view는 사용자의 상호작용에 응답하기 위해 새로운 action을 만들어 시스템에 시스템에 전파합니다.



모든 데이터는 중앙 허브인 dispatcher를 통해 흐릅니다. action은 dispatcher에게 action creator 메소드를 제공하는데 대부분의 action은 view에서의 사용자 상호작용에서 발생합니다. dispatcher는 store를 등록하기 위한 콜백을 실행한 이후에 action을 모든 store로 전달합니다. 등록된 콜백을 활용해 store는 관리하고 있는 상태 중 어떤 액션이라도 관련이 있다면 전달해 줍니다. store는 change 이벤트를 controller-views는 이 이벤트를 듣고 있다가 이벤트 핸들러가 있는 store에서 데이터를 다시 가져옵니다. controller-views는 스스로의 setState() 메소드를 호출하고 컴포넌트 트리에 속해 있는 자식 노드 모두를 다시 렌더링하게 됩니다.



Action creator는 라이브러리에서 제공하는 도움 메소드로 메소드 파라미터에서 action을 생성하고 type을 설정하거나 dispatcher에게 제공하는 역할을 합니다.


모든 action은 store가 dispatcher에 등록해둔 callback을 통해 모든 store에 전송됩니다.


action에 대한 응답으로 store가 스스로 갱신을 한 다음에는 자신이 변경되었다고 모두에게 알립니다.


controller-view라고 불리는 특별한 view가 변경 이벤트를 듣고 새로운 데이터를 store에서 가져온 후 모든 트리에 있는 자식 view에게 새로운 데이터를 제공합니다.


이 구조는 함수형 반응 프로그래밍을 다시 재현하는 것을 쉽게 만들거나 데이터-흐름 프로그래밍, 흐름 기반 프로그래밍을 만드는데 쉽도록 돕습니다. 어플리케이션에 흐르는 데이터 흐름이 양방향 바인딩이 아닌 단방향으로 흐르기 때문입니다. 어플리케이션의 상태는 store에 의해서 관리되고 어플리케이션의 다른 부분과는 완전히 분리된 상태로 남습니다. 두 store 사이에 의존성이 나타나도 둘은 엄격하게 위계가 괸리되어 dispatcher에 의해 동기적으로 변경되는 방법으로 관리됩니다.


이와 같은 구조는 우리의 어플리케이션이 함수형 반응 프로그래밍(functional reactive programming)이나 더 세부적으로 데이터-흐름 프로그래밍(data-flow programming) 또는 흐름 기반 프로그래밍(Flow-based programming)을 연상하게 한다는 사실을 쉽게 떠올리게 합니다. 즉 데이터의 흐름이 양방향ㅇ 바인딩이 아닌 단일 방향으로 흐르게 됩니다. 어플리케이션의 상태는 store에 의해 관리를 해서 어플리케이션의 다른 부분들과 결합도를 극히 낮춘 상태로 유지될 수 있습니다. store의 사이에서 의존성이 생긴다고 해도 dispatcher에 의해 엄격한 위계가 유지되어 동기적으로 갱신되는 방식으로 관리됩니다.


양방향 데이터 바인딩은 연속적인 갱신이 발생하고 객체 하나의 변경이 다른 객체를 변경하게 되어 실제 필요한 업데이트보다 더 많은 분량을 실행하게 됩니다. 어플리케이션의 규모가 커지면 데이터의 연속적인 갱신이 되는 상황에서는 사용자 상호작용의 결과가 어떤 변화를 만드는지 예측하는데 어려워집니다. 갱신으로 인한 데이터 변경이 단 한 차례만 이뤄진다면 전체 시스템은 좀 더 예측 가능하게 됩니다.




단일 dispatcher

dispatcher는 Flux 어플리케이션의 중앙 허브로 모든 데이터의 흐름을 관리합니다. 본질적으로 store의 콜백을 등록하는데 쓰이고 action을 sotre에 배분해주는 간단한 작동방식으로 그 자체가 특별하게 똑똑한 것은 아닙니다. 각각의 store를 직접 등록하고 콜백을 제공합니다. action creator가 새로운 action이 있다고 dispatcher에게 알려주면 어플리케이션에 있는 모든 stiore는 해당 action을 앞서 등록한 callback으로 전달 받게 됩니다.


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


Facebook이 실제로 사용하는 dispatcher는 npm, Bower, 또는 Github에서 확인할 수 있습니다.



Stores

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


예를 들면, Facebook의 돌아보기 편집기에서 지속적으로 재생된 시간과 플레이어 상태를 지속적으로 추적하기 위해 TimeStore를 활용합니다. 같은 애플리케이션에서 ImageStore는 이미지 콜렉션을 지속적으로 추척합니다. TodoMVC 예제의 TodoStore도 비슷하게 할 일 항목의 콜렉션을 관리합니다. store는 두 모델 컬렉션의 특징을 보여주는 것과 동시에 싱글턴 모델의 논리적 도메인으로 역할을 합니다.


위에서 언급한 것과 같이 store는 자신을 dispatcher에 등록하고 callback을 제공합니다. 이 callback은 action을 파라미터로 받습니다. store의 등록된 callback의 내부에서는 switch문을 사용한 action 타입을 활용해서 action을 해석하고 store 내부 메소드에 적절하게 연결될 수 있는 훅을 제공합니다. 여기서 결과적으로 action은 dispatcher를 통해 store의 상태를 갱신합니다. store가 업데이트 된 후, 상태가 변경되었다는 이벤트를 중계하는 과정으로 view에게 새로운 상태를 보내주고 view 스스로 업데이트하게 만듭니다.



Views와 Controller-Views

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


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


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


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


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



Actions

dispatcher는 action을 호출해 데이터를 불러오고 store로 전달할 수 있도록 메소드를 제공합니다. action의 생성은 dispatcher로 action을 보낼 때 의미있는 헬퍼 메소드로 포개집니다. 할 일 목록 어플리케이션에서 할 일 아이템의 문구를 변경하고 싶다고 가정하겠습니다. updateText(todoId, newText)와 같은 함수 시그니쳐를 이용해 TodoActions 모듈 내에 action을 만듭니다. 이 메소드는 view의 이벤트 핸들러로부터 호출되어 실행할 수 있고 그 결과로 사용자 상호작용에 응답할 수 있게 됩니다. 이 action creator 메소드는 type을 추가할 수 있습니다. 이 type을 이용해 action이 store에서 해석될 수 있도록, 적절한 응답이 가능하도록 합니다. 예시에서와 같이 TODO_UPDATE_TEXT와 같은 이름의 타입을 사용합니다.


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



Dispatcher에 대해서

앞서 언급한 것처럼 dispatcher는 store 간의 의존성을 관리할 수 있습니다. 이 기능은 dispatcher 클래스에 포함된 waitFor() 메소드를 통해 가능합니다. TodoMVC는 극단적으로 단순해서 이 메소드를 사용할 필요가 없지만 복잡한 대형 어플리케이션에서는 생명과도 같습니다.


TodoStore에 등록된 callback은 명시적으로 기다려 코드가 진행되는 동안 다른 의존성이 먼저 업데이트 되도록 기다립니다.


case 'TODO_CREATE':
Dispatcher.waitFor([
PrependedTextStore.dispatcheToken,
YetAnotherStore.dispatchToken
]);

TodoStore.create(PrependedTextStore.getText() + '' + action.text);
break;


waitFor()는 단일 인수만 받는데 dispatcher에 등록된 인덱스를 배열로 받습니다. 이 인덱스를 대개 dispatch token이라 부릅니다. 그러므로 waitForm()을 호출하는 store는 다른 store의 상태에 따라 어떤 방식으로 자신의 상태를 갱신할 수 있는지 알 수 있게 됩니다.


dispatch token은 register() 메소드에서 반환하는데 이 메소드는 callback을 dispatcher에 등록할 때 사용됩니다.


PrependedTextStore.dispatchToken = Dispatcher.register(function(payload) {
// ...
});


Dispatcher

Dispatcher는 등록된 callback에 데이터를 중계할 때 사용됩니다. 일반적인 pub-sub 시스템과는 다음 두항목이 다릅니다.


- 콜백은 이벤트를 개별적으로 구독하지 않습니다. 모든 데이터 변동은 등록된 모든 콜백에 전달됩니다.

- 콜백이 실행될 때 콜백의 전체나 일부를 중단할 수 있습니다.


소스 코드를 보려면 Disaptcher( https://github.com/facebook/flux/blob/master/src/Dispatcher.js )에서 확인할 수 있습니다.



API

- register(function callback): string 모든 데이터 변동이 있을 때 실행될 콜백을 등록합니다. waitFor()에서 사용 가능한 토큰을 반환합니다.

- unregister(string id): void 토큰으로 콜백을 제거합니다.

- waitFor(array<string> ids): void 현재 실행한 콜백이 진행되기 전에 특정 콜백을 지연하게 합니다. 데이터 변동에 응답하는 콜백에만 사용해야 합니다.

- dispatch(object payload): void 등록된 모든 콜백에 데이터를 전달합니다.

- isDispatching(): boolean이 Dispatcher가 지금 데이터를 전달하고 있는지 확인합니다.



예시

가상의 비행 목적지 양식에서 국가를 선택했을 때 기본 도시를 선택하는 예를 살펴보겠습니다.


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;
}
});