Flux와 Backbone을 사용하는 React 앱의 간단한 데이터 흐름: 예제가 포함된 자습서
게시 됨: 2022-03-11React.js는 환상적인 라이브러리입니다. 때로는 슬라이스 Python 이후로 가장 좋은 것처럼 보입니다. 그러나 React는 프론트엔드 애플리케이션 스택의 한 부분일 뿐입니다. 데이터 및 상태 관리와 관련하여 제공할 수 있는 것이 많지 않습니다.
React의 제작자인 Facebook은 Flux의 형태로 몇 가지 지침을 제공했습니다. Flux는 React Views, Action Dispatcher 및 Stores를 사용하는 단방향 데이터 흐름을 중심으로 구축된 "애플리케이션 아키텍처"(프레임워크가 아님)입니다. Flux 패턴은 이벤트 제어의 중요한 원칙을 구현하여 몇 가지 주요 문제를 해결하므로 React 애플리케이션을 훨씬 쉽게 추론, 개발 및 유지 관리할 수 있습니다.
여기에서는 제어 흐름의 기본 Flux 예제를 소개하고 Store에 누락된 사항과 백본 모델 및 컬렉션을 사용하여 "Flux 호환" 방식으로 격차를 채우는 방법에 대해 설명합니다.
(참고: 편의와 간결함을 위해 예제에서 CoffeeScript를 사용합니다. CoffeeScript가 아닌 개발자도 따라할 수 있어야 하며 예제를 의사 코드로 취급할 수 있습니다.)
Facebook의 Flux 소개
백본은 보기, 모델, 컬렉션 및 경로를 포함하는 훌륭하고 검증된 작은 라이브러리입니다. 이는 구조화된 프론트엔드 애플리케이션을 위한 사실상 의 표준 라이브러리이며, 후자가 2013년에 도입된 이후로 React 앱과 쌍을 이루고 있습니다. 지금까지 Facebook.com 외부의 React의 대부분의 예에는 Backbone이 함께 사용된다는 언급이 포함되어 있습니다.
불행히도, React View 외부의 전체 애플리케이션 흐름을 처리하기 위해 Backbone에만 의존하는 것은 불행한 문제를 야기합니다. 내가 처음 React-Backbone 애플리케이션 코드 작업을 시작했을 때 내가 읽은 "복잡한 이벤트 체인"은 히드라와 같은 머리를 기르는 데 오래 걸리지 않았습니다. UI에서 모델로 이벤트를 보낸 다음 한 모델에서 다른 모델로 이벤트를 보내고 다시 되돌리면 누가, 어떤 순서로, 왜 변경했는지 추적하기 어렵습니다.
이 Flux 튜토리얼은 Flux 패턴이 어떻게 이러한 문제를 쉽고 간단하게 처리하는지 보여줍니다.
개요
Flux의 슬로건은 "단방향 데이터 흐름"입니다. 다음은 해당 흐름이 어떻게 보이는지 보여주는 Flux 문서의 편리한 다이어그램입니다.
중요한 것은 React --> Dispatcher --> Stores --> React
에서 물건이 흐른다는 것입니다.
각 주요 구성 요소가 무엇이며 어떻게 연결되는지 살펴보겠습니다.
문서에서는 다음과 같은 중요한 경고도 제공합니다.
플럭스는 프레임워크라기보다 패턴에 가깝고 어떤 강력한 종속성이 없습니다. 그러나 우리는 종종 EventEmitter를 Store의 기반으로 사용하고 React를 View의 기반으로 사용합니다. 다른 곳에서는 쉽게 구할 수 없는 Flux의 한 조각은 Dispatcher입니다. 이 모듈은 Flux 도구 상자를 완성하는 데 사용할 수 있습니다.
따라서 Flux에는 세 가지 구성 요소가 있습니다.
- 보기(
React = require('react')
) - 디스패처 (
Dispatcher = require('flux').Dispatcher
) - 상점(
EventEmitter = require('events').EventEmitter
)- (또는 곧 보게 되겠지만
Backbone = require('backbone')
)
- (또는 곧 보게 되겠지만
관점들
여기에서 React에 대해 설명하지 않겠습니다. Angular보다 훨씬 더 선호한다는 점 외에는 React에 대해 많이 쓰여졌기 때문입니다. Angular와 달리 React 코드를 작성할 때 혼란 을 거의 느끼지 않지만 물론 의견은 다를 수 있습니다.
디스패처
Flux Dispatcher는 상점을 수정하는 모든 이벤트가 처리되는 단일 장소입니다. 이를 사용하려면 각 상점이 모든 이벤트를 처리하는 단일 콜백을 register
해야 합니다. 그런 다음 Store를 수정하고 싶을 때마다 이벤트를 dispatch
합니다.
React와 마찬가지로 Dispatcher는 잘 구현된 좋은 아이디어라고 생각합니다. 예를 들어 사용자가 할 일 목록에 항목을 추가할 수 있도록 하는 앱에는 다음이 포함될 수 있습니다.
# in TodoDispatcher.coffee Dispatcher = require("flux").Dispatcher TodoDispatcher = new Dispatcher() # That's all it takes!. module.exports = TodoDispatcher
# in TodoStore.coffee TodoDispatcher = require("./TodoDispatcher") TodoStore = {items: []} TodoStore.dispatchCallback = (payload) -> switch payload.actionType when "add-item" TodoStore.items.push payload.item when "delete-last-item" TodoStore.items.pop() TodoStore.dispatchToken = TodoDispatcher.registerCallback(TodoStore.dispatchCallback) module.exports = TodoStore
# in ItemAddComponent.coffee TodoDispatcher = require("./TodoDispatcher") ItemAddComponent = React.createClass handleAddItem: -> # note: you're NOT just pushing directly to the store! # (the restriction of moving through the dispatcher # makes everything much more modular and maintainable) TodoDispatcher.dispatch actionType: "add-item" item: "hello world" render: -> React.DOM.button { onClick: @handleAddItem }, "Add an Item!"
이렇게 하면 다음 두 가지 질문에 쉽게 답할 수 있습니다.
- Q:
MyStore
를 수정하는 모든 이벤트는 무엇입니까?- A:
MyStore.dispatchCallback
의switch
문의 경우를 확인하세요.
- A:
- Q: 해당 이벤트의 가능한 모든 소스는 무엇입니까?
- A: 해당
actionType
을 검색하기만 하면 됩니다.
- A: 해당
이것은 예를 들어 MyModel.set
및 MyModel.save
및 MyCollection.add
등을 찾는 것보다 훨씬 쉽습니다. 여기서 이러한 기본 질문에 대한 답변을 추적하는 것은 정말 빨리 어려워집니다.
또한 Dispatcher를 사용하면 waitFor
를 사용하여 단순하고 동기적인 방식으로 콜백을 순차적으로 실행할 수 있습니다. 예를 들어:
# in MessageStore.coffee MyDispatcher = require("./MyDispatcher") TodoStore = require("./TodoStore") MessageStore = {items: []} MessageStore.dispatchCallback = (payload) -> switch payload.actionType when "add-item" # synchronous event flow! MyDispatcher.waitFor [TodoStore.dispatchToken] MessageStore.items.push "You added an item! It was: " + payload.item module.exports = MessageStore
실제로 waitFor
를 사용하지 않고도 Dispatcher를 사용하여 Stores를 수정할 때 코드가 훨씬 더 깨끗해진 것을 보고 충격을 받았습니다.
상점들
따라서 데이터는 Dispatcher를 통해 Store 로 흐릅니다. 알았다. 그러나 데이터는 Store에서 View(즉, React)로 어떻게 흐릅니까? Flux 문서에 명시된 대로:
[] 보기는 종속된 상점에서 브로드캐스트하는 이벤트를 수신합니다.
좋아, 좋아. Store에 콜백을 등록한 것처럼 View(React 구성 요소)에도 콜백을 등록합니다. props
를 통해 전달된 Store에서 변경 사항이 발생할 때마다 React에 다시 render
하도록 지시합니다.
예를 들어:
# in TodoListComponent.coffee React = require("react") TodoListComponent = React.createClass componentDidMount: -> @props.TodoStore.addEventListener "change", => @forceUpdate() , @ componentWillUnmount: -> # remove the callback render: -> # show the items in a list. React.DOM.ul {}, @props.TodoStore.items.map (item) -> React.DOM.li {}, item
엄청난!
그렇다면 그 "change"
이벤트를 어떻게 내보냅니까? 글쎄요, Flux는 EventEmitter
사용을 권장합니다. 공식 예에서:
var MessageStore = merge(EventEmitter.prototype, { emitChange: function() { this.emit(CHANGE_EVENT); }, /** * @param {function} callback */ addChangeListener: function(callback) { this.on(CHANGE_EVENT, callback); }, get: function(id) { return _messages[id]; }, getAll: function() { return _messages; }, // etc...
역겨운! 간단한 스토어를 원할 때마다 모든 것을 직접 작성해야 합니까? 표시하고 싶은 정보가 있을 때마다 어느 것을 사용해야 합니까? 더 나은 방법이 있어야합니다!
사라진 조각
Backbone의 모델과 컬렉션에는 Flux의 EventEmitter 기반 Store가 하는 것처럼 보이는 모든 것이 이미 있습니다.
원시 EventEmitter를 사용하라고 Flux는 Store를 생성할 때마다 Backbone의 모델 및 컬렉션의 50-75%를 다시 생성할 것을 권장합니다. 상점에 EventEmitter를 사용하는 것은 Express.js와 같이 잘 구축된 마이크로프레임워크나 이와 동등한 것이 이미 모든 기본 및 상용구를 처리할 때 서버에 베어 Node.js를 사용하는 것과 같습니다.

Express.js가 Node.js에 구축된 것처럼 Backbone의 모델 및 컬렉션은 EventEmitter에 구축됩니다. 그리고 그것은 당신이 거의 항상 필요로 하는 모든 것을 가지고 있습니다: Backbone은 change
이벤트를 내보내고 쿼리 메소드, getter 및 setter 및 모든 것을 가지고 있습니다. 게다가 Backbone의 Jeremy Ashkenas와 230명의 기여자로 구성된 그의 군대는 내가 할 수 있는 것보다 이 모든 일에서 훨씬 더 나은 일을 했습니다.
이 백본 튜토리얼의 예로서, 위의 MessageStore 예제를 백본 버전으로 변환했습니다.
객관적으로 코드가 적고(작업을 복제할 필요가 없음) 주관적으로 더 명확하고 간결합니다(예: _messages[message.id] = message
대신 this.add(message)
).
이제 Stores에 Backbone을 사용합시다!
FluxBone 패턴: 백본에 의한 Flux Stores
이 튜토리얼은 Backbone for Stores를 사용하는 Flux 아키텍처인 FluxBone 이라는 접근 방식의 기초입니다. 다음은 FluxBone 아키텍처의 기본 패턴입니다.
- 상점은 Dispatcher에 콜백을 등록한 인스턴스화된 백본 모델 또는 컬렉션입니다. 일반적으로 이는 싱글톤임을 의미합니다.
- View 구성 요소는 Store를 직접 수정 하지 않습니다 (예:
.set()
없음). 대신 구성 요소는 작업을 Dispatcher에 전달합니다. - 보기 구성 요소 쿼리 저장소 및 해당 이벤트에 바인딩하여 업데이트를 트리거합니다.
Backbone 및 Flux 예제를 사용하여 각 부분을 차례로 살펴보겠습니다.
1. 상점은 Dispatcher에 콜백을 등록한 백본 모델 또는 컬렉션을 인스턴스화합니다.
# in TodoDispatcher.coffee Dispatcher = require("flux").Dispatcher TodoDispatcher = new Dispatcher() # That's all it takes! module.exports = TodoDispatcher
# in stores/TodoStore.coffee Backbone = require("backbone") TodoDispatcher = require("../dispatcher") TodoItem = Backbone.Model.extend({}) TodoCollection = Backbone.Collection.extend model: TodoItem url: "/todo" # we register a callback with the Dispatcher on init. initialize: -> @dispatchToken = TodoDispatcher.register(@dispatchCallback) dispatchCallback: (payload) => switch payload.actionType # remove the Model instance from the Store. when "todo-delete" @remove payload.todo when "todo-add" @add payload.todo when "todo-update" # do stuff... @add payload.todo, merge: true # ... etc # the Store is an instantiated Collection; a singleton. TodoStore = new TodoCollection() module.exports = TodoStore
2. 구성 요소는 Store를 직접 수정 하지 않습니다 (예: .set()
없음). 대신 구성 요소는 작업을 Dispatcher에 전달합니다.
# components/TodoComponent.coffee React = require("react") TodoListComponent = React.createClass handleTodoDelete: -> # instead of removing the todo from the TodoStore directly, # we use the Dispatcher TodoDispatcher.dispatch actionType: "todo-delete" todo: @props.todoItem # ... (see below) ... module.exports = TodoListComponent
3. 구성 요소는 업데이트를 트리거하기 위해 이벤트를 저장하고 바인딩합니다.
# components/TodoComponent.coffee React = require("react") TodoListComponent = React.createClass handleTodoDelete: -> # instead of removing the todo from the TodoStore directly, # we use the dispatcher. #flux TodoDispatcher.dispatch actionType: "todo-delete" todo: @props.todoItem # ... componentDidMount: -> # the Component binds to the Store's events @props.TodoStore.on "add remove reset", => @forceUpdate() , @ componentWillUnmount: -> # turn off all events and callbacks that have this context @props.TodoStore.off null, null, this render: -> React.DOM.ul {}, @props.TodoStore.items.map (todoItem) -> # TODO: TodoItemComponent, which would bind to # `this.props.todoItem.on('change')` TodoItemComponent { todoItem: todoItem } module.exports = TodoListComponent
나는 이 Flux 및 Backbone 접근 방식을 내 프로젝트에 적용했으며 이 패턴을 사용하도록 React 애플리케이션을 다시 설계하고 나면 거의 모든 추악한 부분이 사라졌습니다. 조금은 기적 같은 일이었습니다. 더 나은 방법을 찾느라 이빨을 갈게 했던 코드 조각이 하나 둘씩 합리적인 흐름으로 대체되었습니다. 그리고 Backbone이 이 패턴에 통합하는 것처럼 보이는 부드러움은 놀랍습니다. 저는 Backbone, Flux 또는 React를 단일 애플리케이션에 함께 맞추기 위해 싸우고 있다는 느낌이 들지 않습니다.
예제 믹신
구성 요소에 FluxBone Store를 추가할 때마다 this.on this.on(...)
및 this.off(...)
코드를 작성하는 것은 약간 오래될 수 있습니다.
다음은 매우 순진하지만 확실히 반복 작업을 훨씬 더 쉽게 만들 수 있는 React Mixin의 예입니다.
# in FluxBoneMixin.coffee module.exports = (propName) -> componentDidMount: -> @props[propName].on "all", => @forceUpdate() , @ componentWillUnmount: -> @props[propName].off "all", => @forceUpdate() , @
# in HelloComponent.coffee React = require("react") UserStore = require("./stores/UserStore") TodoStore = require("./stores/TodoStore") FluxBoneMixin = require("./FluxBoneMixin") MyComponent = React.createClass mixins: [ FluxBoneMixin("UserStore"), FluxBoneMixin("TodoStore"), ] render: -> React.DOM.div {}, "Hello, #{ @props.UserStore.get('name') }, you have #{ @props.TodoStore.length } things to do." React.renderComponent( MyComponent { UserStore: UserStore TodoStore: TodoStore } , document.body.querySelector(".main") )
웹 API와 동기화
원래 Flux 다이어그램에서는 ActionCreators만을 통해 Web API와 상호 작용합니다. 이 작업은 Dispatcher에 작업을 보내기 전에 서버의 응답이 필요합니다. 그것은 나와 어울리지 않았습니다. 스토어가 서버보다 먼저 변경 사항을 알아야 하지 않을까요?
저는 다이어그램의 해당 부분을 뒤집기로 선택했습니다. Stores는 Backbone의 sync()
를 통해 RESTful CRUD API와 직접 상호 작용합니다. 이것은 적어도 실제 RESTful CRUD API로 작업하는 경우 매우 편리합니다.
데이터 무결성은 문제 없이 유지됩니다. 새 속성을 .set()
하면 change
이벤트가 React를 다시 렌더링하여 새 데이터를 낙관적으로 표시합니다. 서버에 .save()
를 시도하면 request
이벤트가 로드 아이콘을 표시하도록 알려줍니다. 일이 진행되면 sync
이벤트는 로딩 아이콘을 제거하도록 알려주거나 error
이벤트는 상황을 빨간색으로 바꾸도록 알려줍니다. 여기에서 영감을 볼 수 있습니다.
또한 첫 번째 방어 계층에 대한 유효성 검사(및 해당 invalid
이벤트)와 서버에서 새 정보를 가져오는 .fetch()
메서드가 있습니다.
덜 표준적인 작업의 경우 ActionCreators를 통한 상호 작용이 더 합리적일 수 있습니다. 나는 Facebook이 "단순한 CRUD"를 많이 하지 않는다고 생각합니다. 이 경우 스토어를 우선순위에 두지 않는 것은 놀라운 일이 아닙니다.
결론
Facebook의 엔지니어링 팀은 React로 프론트 엔드 웹을 발전시키기 위해 놀라운 작업을 수행했으며 Flux의 도입으로 기술 측면에서뿐만 아니라 엔지니어링 측면에서도 진정으로 확장되는 더 넓은 아키텍처를 엿볼 수 있습니다. Backbone의 영리하고 신중한 사용(이 튜토리얼의 예에 따라)은 Flux의 격차를 메울 수 있어 1인 인디 상점에서 대기업에 이르기까지 누구나 인상적인 애플리케이션을 만들고 유지 관리하는 것을 놀라울 정도로 쉽게 만듭니다.