상태 기계의 부상
게시 됨: 2022-03-10벌써 2018년이고 수많은 프론트엔드 개발자들이 여전히 복잡성과 부동성과의 싸움을 주도하고 있습니다. 그들은 매달 높은 품질로 신속하게 제공하는 데 도움이 되는 버그 없는 애플리케이션 아키텍처인 성배를 찾았습니다. 저는 그 개발자 중 한 명이며 도움이 될 만한 흥미로운 것을 찾았습니다.
우리는 React 및 Redux와 같은 도구로 한 걸음 더 나아갔습니다. 그러나 대규모 응용 프로그램에서는 그 자체로는 충분하지 않습니다. 이 기사에서는 프론트 엔드 개발의 맥락에서 상태 머신의 개념을 소개합니다. 당신은 아마 깨닫지 못한 채 이미 그것들 중 몇 개를 구축했을 것입니다.
상태 머신 소개
상태 머신은 계산의 수학적 모델입니다. 그것은 기계가 다른 상태를 가질 수 있지만 주어진 시간에 그 중 하나만 수행한다는 추상적 개념입니다. 상태 머신에는 여러 유형이 있습니다. 가장 유명한 것은 튜링 기계라고 생각합니다. 그것은 무한 상태 기계이며, 이는 셀 수 없이 많은 상태를 가질 수 있음을 의미합니다. Turing 기계는 대부분의 경우 유한한 수의 상태를 가지고 있기 때문에 오늘날의 UI 개발에는 적합하지 않습니다. 이것이 Mealy 및 Moore와 같은 유한 상태 기계가 더 합리적인 이유입니다.
그들 사이의 차이점은 무어 기계가 이전 상태만을 기반으로 상태를 변경한다는 것입니다. 불행히도 우리는 사용자 상호 작용 및 네트워크 프로세스와 같은 많은 외부 요인을 가지고 있습니다. 이는 Moore 기계가 우리에게도 충분하지 않다는 것을 의미합니다. 우리가 찾고 있는 것은 Mely 기계입니다. 초기 상태가 있고 입력 및 현재 상태를 기반으로 새 상태로 전환됩니다.
상태 기계가 어떻게 작동하는지 설명하는 가장 쉬운 방법 중 하나는 개찰구를 보는 것입니다. 제한된 수의 상태(잠김 및 잠금 해제)가 있습니다. 다음은 가능한 입력 및 전환과 함께 이러한 상태를 보여주는 간단한 그래픽입니다.
개찰구의 초기 상태는 잠겨 있습니다. 몇 번을 눌러도 잠긴 상태로 유지됩니다. 그러나 코인을 전달하면 잠금 해제 상태로 전환됩니다. 이 시점에서 다른 동전은 아무 것도 하지 않을 것입니다. 여전히 잠금 해제 상태일 것입니다. 다른 쪽에서 밀면 효과가 있고 우리는 통과할 수 있습니다. 이 작업은 또한 시스템을 초기 잠금 상태로 전환합니다.
개찰구를 제어하는 단일 기능을 구현하려는 경우 현재 상태와 작업이라는 두 가지 인수로 끝날 것입니다. 그리고 Redux를 사용한다면 이것은 아마도 당신에게 친숙하게 들릴 것입니다. 우리가 현재 상태를 수신하고 액션의 페이로드를 기반으로 다음 상태가 무엇인지 결정하는 잘 알려진 감속기 기능과 유사합니다. 리듀서는 상태 머신의 컨텍스트에서 전환입니다. 사실, 우리가 어떻게든 변경할 수 있는 상태를 가진 모든 애플리케이션을 상태 머신이라고 부를 수 있습니다. 모든 것을 수동으로 계속해서 구현하고 있다는 것뿐입니다.
상태 머신이 더 나은 방법은 무엇입니까?
직장에서 우리는 Redux를 사용하는데 매우 만족합니다. 하지만 마음에 들지 않는 패턴이 보이기 시작했습니다. "싫어요"라고 해서 그들이 작동하지 않는다는 뜻은 아닙니다. 더 많은 복잡성을 추가하고 더 많은 코드를 작성하도록 강요합니다. 실험할 여지가 있는 사이드 프로젝트를 수행해야 했고 React 및 Redux 개발 방식을 재고하기로 결정했습니다. 나는 나를 염려하는 것들에 대해 기록하기 시작했고 상태 머신 추상화가 이러한 문제 중 일부를 실제로 해결할 수 있다는 것을 깨달았습니다. 자바스크립트에서 상태 머신을 구현하는 방법을 살펴보겠습니다.
우리는 간단한 문제를 공격할 것입니다. 백엔드 API에서 데이터를 가져와 사용자에게 표시하려고 합니다. 가장 첫 번째 단계는 전환이 아닌 상태에서 생각하는 방법을 배우는 것입니다. 상태 머신에 들어가기 전에 이러한 기능을 구축하기 위한 워크플로는 다음과 같았습니다.
- 데이터 가져오기 버튼을 표시합니다.
- 사용자가 데이터 가져오기 버튼을 클릭합니다.
- 요청을 백엔드로 실행합니다.
- 데이터를 검색하고 구문 분석합니다.
- 사용자에게 보여줍니다.
- 또는 오류가 있는 경우 오류 메시지를 표시하고 데이터 가져오기 버튼을 표시하여 프로세스를 다시 트리거할 수 있습니다.
우리는 선형적으로 생각하고 기본적으로 최종 결과에 대한 모든 가능한 방향을 다루려고 노력합니다. 한 단계는 다른 단계로 이어지며 빠르게 코드 분기를 시작할 것입니다. 사용자가 버튼을 두 번 클릭하거나 백엔드의 응답을 기다리는 동안 사용자가 버튼을 클릭하거나 요청은 성공했지만 데이터가 손상되는 것과 같은 문제는 어떻습니까? 이러한 경우에 발생한 상황을 보여주는 다양한 플래그가 있을 수 있습니다. 플래그가 있다는 것은 더 많은 if
절을 의미하고 더 복잡한 앱에서는 더 많은 충돌을 의미합니다.
이것은 우리가 전환을 생각하고 있기 때문입니다. 우리는 이러한 전환이 어떻게 그리고 어떤 순서로 발생하는지에 초점을 맞추고 있습니다. 대신 애플리케이션의 다양한 상태에 초점을 맞추는 것이 훨씬 간단할 것입니다. 우리는 몇 개의 상태를 가지고 있으며 가능한 입력은 무엇입니까? 같은 예를 사용하여:
- 게으른
이 상태에서 데이터 가져오기 버튼을 표시하고 앉아서 기다립니다. 가능한 조치는 다음과 같습니다.- 딸깍 하는 소리
사용자가 버튼을 클릭하면 백엔드로 요청을 실행한 다음 머신을 "가져오기" 상태로 전환합니다.
- 딸깍 하는 소리
- 가져오기
요청이 진행 중이며 우리는 앉아서 기다립니다. 조치는 다음과 같습니다.- 성공
데이터가 성공적으로 도착했으며 손상되지 않았습니다. 어떤 식으로든 데이터를 사용하고 "유휴" 상태로 다시 전환합니다. - 실패
요청을 하거나 데이터를 파싱하는 동안 오류가 발생하면 "오류" 상태로 전환됩니다.
- 성공
- 오류
오류 메시지를 표시하고 데이터 가져오기 버튼을 표시합니다. 이 상태는 하나의 작업을 수락합니다.- 다시 해 보다
사용자가 재시도 버튼을 클릭하면 요청을 다시 시작하고 시스템을 "가져오기" 상태로 전환합니다.
- 다시 해 보다
우리는 대략적으로 동일한 프로세스를 설명했지만 상태와 입력이 있습니다.
이것은 논리를 단순화하고 예측 가능성을 높입니다. 또한 위에서 언급한 몇 가지 문제를 해결합니다. "가져오기" 상태에 있는 동안에는 클릭을 허용하지 않습니다. 따라서 사용자가 버튼을 클릭하더라도 시스템이 해당 상태에 있는 동안 해당 작업에 응답하도록 구성되지 않았기 때문에 아무 일도 일어나지 않습니다. 이 접근 방식은 코드 로직의 예측할 수 없는 분기를 자동으로 제거합니다. 이것은 테스트하는 동안 다룰 코드가 적다 는 것을 의미합니다. 또한 통합 테스트와 같은 일부 유형의 테스트를 자동화할 수 있습니다. 애플리케이션이 하는 일에 대해 정말 명확한 아이디어를 갖고 정의된 상태와 전환을 살펴보고 주장을 생성하는 스크립트를 만들 수 있는 방법을 생각해 보십시오. 이러한 주장은 우리가 가능한 모든 상태에 도달했거나 특정 여정을 다루었음을 증명할 수 있습니다.
사실, 어떤 상태가 필요하거나 어떤 상태가 필요한지 알기 때문에 가능한 모든 상태를 기록하는 것이 가능한 모든 전환을 기록하는 것보다 쉽습니다. 그건 그렇고, 대부분의 경우 상태는 애플리케이션의 비즈니스 논리를 설명하는 반면 전환은 처음에는 매우 자주 알 수 없습니다. 당사 소프트웨어의 버그는 잘못된 상태 및/또는 잘못된 시간에 처리된 작업의 결과입니다. 그들은 우리 앱을 우리가 알지 못하는 상태로 남겨두고 이것은 우리 프로그램을 망가뜨리거나 잘못 작동하게 만듭니다. 물론 우리는 그런 상황에 처하고 싶지 않습니다. 상태 머신은 좋은 방화벽 입니다. 그들은 우리가 방법을 명시적으로 말하지 않고 일어날 수 있는 것과 언제 일어날 수 있는지에 대한 경계를 설정하기 때문에 알 수 없는 상태에 도달하는 것을 방지합니다. 상태 머신의 개념은 단방향 데이터 흐름과 정말 잘 어울립니다. 함께 사용하면 코드 복잡성을 줄이고 상태가 시작된 위치에 대한 미스터리를 풀 수 있습니다.
JavaScript로 상태 머신 생성하기
충분한 이야기 - 몇 가지 코드를 봅시다. 우리는 같은 예를 사용할 것입니다. 위의 목록을 기반으로 다음부터 시작합니다.
const machine = { 'idle': { click: function () { ... } }, 'fetching': { success: function () { ... }, failure: function () { ... } }, 'error': { 'retry': function () { ... } } }
우리는 상태를 객체로, 가능한 입력을 함수로 가지고 있습니다. 그러나 초기 상태가 누락되었습니다. 위의 코드를 다음과 같이 변경해 보겠습니다.
const machine = { state: 'idle', transitions: { 'idle': { click: function() { ... } }, 'fetching': { success: function() { ... }, failure: function() { ... } }, 'error': { 'retry': function() { ... } } } }
우리가 이해할 수 있는 모든 상태를 정의하고 나면 입력을 보내고 상태를 변경할 준비가 된 것입니다. 아래의 두 가지 도우미 메서드를 사용하여 이를 수행합니다.
const machine = { dispatch(actionName, ...payload) { const actions = this.transitions[this.state]; const action = this.transitions[this.state][actionName]; if (action) { action.apply(machine, ...payload); } }, changeStateTo(newState) { this.state = newState; }, ... }
dispatch
함수는 현재 상태의 전환에서 주어진 이름을 가진 작업이 있는지 확인합니다. 그렇다면 지정된 페이로드로 실행합니다. 또한 this.dispatch(<action>)
를 사용하여 다른 작업을 디스패치하거나 this.changeStateTo(<new state>)
로 상태를 변경할 수 있도록 machine
을 컨텍스트로 사용하여 action
핸들러를 호출합니다.
예제의 사용자 여정에 따라 우리가 전달해야 하는 첫 번째 작업은 click
입니다. 해당 작업의 핸들러는 다음과 같습니다.
transitions: { 'idle': { click: function () { this.changeStateTo('fetching'); service.getData().then( data => { try { this.dispatch('success', JSON.parse(data)); } catch (error) { this.dispatch('failure', error) } }, error => this.dispatch('failure', error) ); } }, ... } machine.dispatch('click');
먼저 머신의 상태를 fetching
으로 변경합니다. 그런 다음 백엔드에 대한 요청을 트리거합니다. Promise를 반환하는 getData
메서드가 있는 서비스가 있다고 가정해 보겠습니다. 해결되고 데이터 구문 분석이 정상이면 failure
가 아닌 경우 success
을 전달합니다.
여태까지는 그런대로 잘됐다. 다음으로 fetching
상태에서 success
및 failure
작업과 입력을 구현해야 합니다.
transitions: { 'idle': { ... }, 'fetching': { success: function (data) { // render the data this.changeStateTo('idle'); }, failure: function (error) { this.changeStateTo('error'); } }, ... }
이전 프로세스에 대해 생각할 필요가 없도록 뇌를 어떻게 해방시켰는지 주목하십시오. 우리는 사용자 클릭이나 HTTP 요청에 무슨 일이 일어나는지 신경 쓰지 않습니다. 우리는 응용 프로그램이 fetching
상태에 있다는 것을 알고 있으며 이 두 가지 작업만 예상합니다. 새로운 논리를 따로따로 작성하는 것과 약간 비슷합니다.
마지막 비트는 error
상태입니다. 응용 프로그램이 실패에서 복구할 수 있도록 해당 재시도 논리를 제공하면 좋을 것입니다.
transitions: { 'error': { retry: function () { this.changeStateTo('idle'); this.dispatch('click'); } } }
여기에서 click
핸들러에서 작성한 로직을 복제해야 합니다. 이를 방지하려면 핸들러를 두 작업 모두에 액세스할 수 있는 함수로 정의하거나 먼저 idle
상태로 전환한 다음 수동으로 click
작업을 전달해야 합니다.
작업 상태 머신의 전체 예는 내 Codepen에서 찾을 수 있습니다.
라이브러리로 상태 머신 관리
유한 상태 기계 패턴은 React, Vue 또는 Angular를 사용하는지 여부에 관계없이 작동합니다. 이전 섹션에서 보았듯이 우리는 큰 어려움 없이 상태 머신을 쉽게 구현할 수 있습니다. 그러나 때로는 라이브러리가 더 많은 유연성을 제공합니다. 좋은 것들 중 일부는 Machina.js와 XState입니다. 그러나 이 기사에서는 유한 상태 머신의 개념을 기반으로 하는 Redux와 유사한 라이브러리인 Stent에 대해 이야기할 것입니다.
Stent는 상태 머신 컨테이너의 구현입니다. Redux 및 Redux-Saga 프로젝트의 일부 아이디어를 따르지만 제 생각에는 더 간단하고 상용구 없는 프로세스를 제공합니다. readme 기반 개발을 사용하여 개발되었으며 말 그대로 API 디자인에만 몇 주를 보냈습니다. 라이브러리를 작성하고 있었기 때문에 Redux 및 Flux 아키텍처를 사용하는 동안 발생한 문제를 해결할 기회가 있었습니다.
머신 생성
대부분의 경우 당사 애플리케이션은 여러 도메인을 다룹니다. 우리는 하나의 기계로 갈 수 없습니다. 따라서 Stent를 사용하면 다음과 같은 많은 기계를 만들 수 있습니다.
import { Machine } from 'stent'; const machineA = Machine.create('A', { state: ..., transitions: ... }); const machineB = Machine.create('B', { state: ..., transitions: ... });
나중에 Machine.get
메서드를 사용하여 이러한 머신에 액세스할 수 있습니다.
const machineA = Machine.get('A'); const machineB = Machine.get('B');
렌더링 로직에 머신 연결하기
제 경우 렌더링은 React를 통해 수행되지만 다른 라이브러리를 사용할 수 있습니다. 렌더링을 트리거하는 콜백을 실행하는 것으로 요약됩니다. 내가 작업한 첫 번째 기능 중 하나는 connect
기능이었습니다.
import { connect } from 'stent/lib/helpers'; Machine.create('MachineA', ...); Machine.create('MachineB', ...); connect() .with('MachineA', 'MachineB') .map((MachineA, MachineB) => { ... rendering here });
우리는 어떤 기계가 우리에게 중요한지 말하고 이름을 부여합니다. map
에 전달하는 콜백은 처음에 한 번 발생하고 나중에 일부 시스템의 상태가 변경될 때마다 발생합니다. 여기에서 렌더링을 트리거합니다. 이 시점에서 연결된 머신에 직접 액세스할 수 있으므로 현재 상태와 메서드를 검색할 수 있습니다. 콜백을 한 번만 실행하기 위한 mapSilent
와 초기 실행을 건너뛰기 위한 mapOnce
도 있습니다.
편의를 위해 React 통합을 위해 특별히 도우미를 내보냅니다. Redux의 connect(mapStateToProps)
와 정말 비슷합니다.
import React from 'react'; import { connect } from 'stent/lib/react'; class TodoList extends React.Component { render() { const { isIdle, todos } = this.props; ... } } // MachineA and MachineB are machines defined // using Machine.create function export default connect(TodoList) .with('MachineA', 'MachineB') .map((MachineA, MachineB) => { isIdle: MachineA.isIdle, todos: MachineB.state.todos });
Stent는 매핑 콜백을 실행하고 React 구성 요소에 props
로 전송되는 객체를 수신할 것으로 예상합니다.
스텐트의 맥락에서 상태란 무엇입니까?
지금까지 우리 상태는 단순한 문자열이었습니다. 불행히도 현실 세계에서는 상태에 문자열 이상을 유지해야 합니다. 이것이 Stent의 상태가 실제로 내부에 속성이 있는 객체인 이유입니다. 유일하게 예약된 속성은 name
입니다. 다른 모든 것은 앱별 데이터입니다. 예를 들어:
{ name: 'idle' } { name: 'fetching', todos: [] } { name: 'forward', speed: 120, gear: 4 }
지금까지 Stent에 대한 내 경험에 따르면 상태 개체가 더 커지면 이러한 추가 속성을 처리하는 다른 기계가 필요할 것입니다. 다양한 상태를 식별하는 데는 시간이 걸리지만 보다 관리하기 쉬운 응용 프로그램을 작성하는 데 있어 큰 진전이라고 생각합니다. 그것은 미래를 예측하고 가능한 행동의 틀을 그리는 것과 조금 비슷합니다.
상태 머신으로 작업하기
처음의 예와 유사하게 기계의 가능한(유한) 상태를 정의하고 가능한 입력을 설명해야 합니다.
import { Machine } from 'stent'; const machine = Machine.create('sprinter', { state: { name: 'idle' }, // initial state transitions: { 'idle': { 'run please': function () { return { name: 'running' }; } }, 'running': { 'stop now': function () { return { name: 'idle' }; } } } });
우리의 초기 상태는 idle
이며, 이는 run
의 작업을 허용합니다. 머신이 running
상태에 있으면 stop
작업을 실행할 수 있으며, 이는 우리를 idle
상태로 되돌립니다.
이전에 구현한 dispatch
및 changeStateTo
도우미를 기억할 것입니다. 이 라이브러리는 동일한 논리를 제공하지만 내부적으로 숨겨져 있으므로 생각할 필요가 없습니다. 편의를 위해 Stent는 transitions
속성을 기반으로 다음을 생성합니다.
- 머신이 특정 상태에 있는지 확인하기 위한 도우미 메서드 —
idle
상태에서는isIdle()
메서드를 생성하는 반면running
경우에는isRunning()
; - 작업 전달을 위한 도우미 메서드:
runPlease()
및stopNow()
.
따라서 위의 예에서 다음을 사용할 수 있습니다.
machine.isIdle(); // boolean machine.isRunning(); // boolean machine.runPlease(); // fires action machine.stopNow(); // fires action
자동으로 생성된 메소드를 connect
유틸리티 기능과 결합하여 원을 닫을 수 있습니다. 사용자 상호 작용은 상태를 업데이트하는 시스템 입력 및 작업을 트리거합니다. 해당 업데이트로 인해 connect
에 전달된 매핑 함수가 시작되고 상태 변경에 대한 정보가 제공됩니다. 그런 다음 다시 렌더링합니다.
입력 및 작업 처리기
아마도 가장 중요한 부분은 액션 핸들러일 것입니다. 이것은 입력 및 변경된 상태에 응답하기 때문에 대부분의 애플리케이션 로직을 작성하는 곳입니다. Redux에서 내가 정말 좋아하는 부분도 여기에 통합되어 있습니다. 바로 리듀서 기능의 불변성과 단순성입니다. Stent의 액션 핸들러의 본질은 동일합니다. 현재 상태 및 작업 페이로드를 수신하고 새 상태를 반환해야 합니다. 핸들러가 아무 것도 반환하지 않으면( undefined
) 시스템 상태는 동일하게 유지됩니다.
transitions: { 'fetching': { 'success': function (state, payload) { const todos = [ ...state.todos, payload ]; return { name: 'idle', todos }; } } }
원격 서버에서 데이터를 가져와야 한다고 가정해 봅시다. 요청을 실행하고 머신을 fetching
상태로 전환합니다. 데이터가 백엔드에서 들어오면 다음과 같이 success
작업을 시작합니다.
machine.success({ label: '...' });
그런 다음 idle
상태로 돌아가 일부 데이터를 todos
배열 형식으로 유지합니다. 액션 핸들러로 설정할 수 있는 몇 가지 다른 값이 있습니다. 첫 번째이자 가장 간단한 경우는 새로운 상태가 되는 문자열만 전달할 때입니다.
transitions: { 'idle': { 'run': 'running' } }
이것은 run()
작업을 사용하여 { name: 'idle' }
에서 { name: 'running' }
으로 전환하는 것입니다. 이 접근 방식은 동기 상태 전환이 있고 메타 데이터가 없을 때 유용합니다. 따라서 다른 것을 상태로 유지하면 해당 유형의 전환이 이를 플러시합니다. 마찬가지로 상태 개체를 직접 전달할 수 있습니다.
transitions: { 'editing': { 'delete all todos': { name: 'idle', todos: [] } } }
deleteAllTodos
작업을 사용하여 editing
에서 idle
로 전환하고 있습니다.
우리는 이미 함수 핸들러를 보았고 액션 핸들러의 마지막 변형은 생성기 함수입니다. Redux-Saga 프로젝트에서 영감을 얻었으며 다음과 같습니다.
import { call } from 'stent/lib/helpers'; Machine.create('app', { 'idle': { 'fetch data': function * (state, payload) { yield { name: 'fetching' } try { const data = yield call(requestToBackend, '/api/todos/', 'POST'); return { name: 'idle', data }; } catch (error) { return { name: 'error', error }; } } } });
제너레이터에 대한 경험이 없는 경우 다소 모호하게 보일 수 있습니다. 그러나 JavaScript의 생성기는 강력한 도구입니다. 작업 처리기를 일시 중지하고 상태를 여러 번 변경하고 비동기 논리를 처리할 수 있습니다.
제너레이터의 재미
Redux-Saga를 처음 소개했을 때 비동기 작업을 처리하는 방법이 지나치게 복잡하다고 생각했습니다. 사실, 이것은 커맨드 디자인 패턴을 꽤 똑똑하게 구현한 것입니다. 이 패턴의 주요 이점은 논리 호출과 실제 구현을 분리한다는 것입니다.
다시 말해서, 우리는 우리가 원하는 것을 말하지만 그것이 어떻게 일어나야 하는지는 말하지 않습니다. Matt Hink의 블로그 시리즈는 무용담이 구현되는 방식을 이해하는 데 도움이 되었으며 읽어볼 것을 강력히 권장합니다. 나는 같은 아이디어를 Stent에 가져왔고, 이 기사의 목적을 위해, 우리는 물건을 양보함으로써 실제로 그것을 하지 않고 우리가 원하는 것에 대한 지침을 제공한다고 말할 것입니다. 작업이 수행되면 제어 권한을 다시 받습니다.
현재 다음과 같은 몇 가지가 발송될 수 있습니다(양보됨).
- 기계의 상태를 변경하기 위한 상태 객체(또는 문자열);
-
call
도우미 호출(프로미스 또는 다른 생성기 함수를 반환하는 함수인 동기 함수를 허용함) — 기본적으로 "이것을 실행하고 비동기식이면 기다리십시오. 작업이 끝나면 결과를 알려주세요.”; -
wait
도우미 호출(다른 작업을 나타내는 문자열 수락) 이 유틸리티 함수를 사용하면 핸들러를 일시 중지하고 다른 작업이 전달될 때까지 기다립니다.
다음은 변형을 보여주는 함수입니다.
const fireHTTPRequest = function () { return new Promise((resolve, reject) => { // ... }); } ... transitions: { 'idle': { 'fetch data': function * () { yield 'fetching'; // sets the state to { name: 'fetching' } yield { name: 'fetching' }; // same as above // wait for getTheData and checkForErrors actions // to be dispatched const [ data, isError ] = yield wait('get the data', 'check for errors'); // wait for the promise returned by fireHTTPRequest // to be resolved const result = yield call(fireHTTPRequest, '/api/data/users'); return { name: 'finish', users: result }; } } }
보시다시피 코드는 동기적으로 보이지만 실제로는 그렇지 않습니다. 해결된 약속을 기다리거나 다른 생성기를 반복하는 지루한 부분을 수행하는 것은 Stent일 뿐입니다.
Stent가 Redux 문제를 해결하는 방법
너무 많은 상용구 코드
Redux(및 Flux) 아키텍처는 시스템에서 순환하는 작업에 의존합니다. 응용 프로그램이 성장하면 일반적으로 많은 상수와 작업 생성자가 있게 됩니다. 이 두 가지는 서로 다른 폴더에 있는 경우가 매우 많으며 코드 실행을 추적하는 데 시간이 걸리는 경우가 있습니다. 또한 새 기능을 추가할 때 항상 전체 작업 집합을 처리해야 하므로 더 많은 작업 이름과 작업 작성자를 정의해야 합니다.
Stent에는 액션 이름이 없으며 라이브러리는 자동으로 액션 생성자를 생성합니다.
const machine = Machine.create('todo-app', { state: { name: 'idle', todos: [] }, transitions: { 'idle': { 'add todo': function (state, todo) { ... } } } }); machine.addTodo({ title: 'Fix that bug' });
우리는 machine.addTodo
액션 생성자를 머신의 메소드로 직접 정의했습니다. 이 접근 방식은 또한 내가 직면한 또 다른 문제, 즉 특정 작업에 응답하는 감속기를 찾는 문제를 해결했습니다. 일반적으로 React 구성 요소에서 addTodo
와 같은 작업 생성자 이름을 봅니다. 그러나 감속기에서는 일정한 유형의 작업으로 작업합니다. 때로는 정확한 유형을 볼 수 있도록 작업 작성자 코드로 이동해야 합니다. 여기서는 유형이 전혀 없습니다.
예측할 수 없는 상태 변화
일반적으로 Redux는 변경할 수 없는 방식으로 상태를 잘 관리합니다. 문제는 Redux 자체에 있는 것이 아니라 개발자가 언제든지 작업을 전달할 수 있다는 점입니다. 조명을 켜는 액션이 있다고 하면 해당 액션을 연속으로 두 번 실행해도 됩니까? 그렇지 않다면 Redux로 이 문제를 어떻게 해결해야 할까요? 글쎄, 우리는 로직을 보호하고 조명이 이미 켜져 있는지 확인하는 약간의 코드를 리듀서에 넣을 것입니다. 아마도 현재 상태를 확인하는 if
절일 것입니다. 이제 문제는 이것이 감속기의 범위를 벗어나는 것 아닙니까? 감속기는 그러한 예외적인 경우에 대해 알아야 합니까?
Redux에서 내가 놓치고 있는 것은 조건부 논리로 감속기를 오염시키지 않고 애플리케이션의 현재 상태를 기반으로 하는 작업의 디스패치를 중지하는 방법입니다. 그리고 이 결정을 액션 생성자가 해고되는 뷰 레이어에도 적용하고 싶지 않습니다. Stent를 사용하면 머신이 현재 상태에서 선언되지 않은 작업에 응답하지 않기 때문에 자동으로 발생합니다. 예를 들어:
const machine = Machine.create('app', { state: { name: 'idle' }, transitions: { 'idle': { 'run': 'running', 'jump': 'jumping' }, 'running': { 'stop': 'idle' } } }); // this is fine machine.run(); // This will do nothing because at this point // the machine is in a 'running' state and there is // only 'stop' action there. machine.jump();
기계가 주어진 시간에 특정 입력만 받아들인다는 사실은 이상한 버그로부터 우리를 보호하고 애플리케이션을 더 예측 가능하게 만듭니다.
전환이 아닌 상태
Redux는 Flux와 마찬가지로 전환 측면에서 생각하게 합니다. Redux로 개발하는 멘탈 모델은 액션과 이러한 액션이 리듀서의 상태를 어떻게 변환하는지에 의해 주도됩니다. 나쁘지는 않지만 대신 상태 측면에서 생각하는 것이 더 합리적이라는 것을 알았습니다.
결론
프로그래밍, 특히 UI 개발에서 상태 머신의 개념은 저에게 눈을 뜨게 했습니다. 나는 모든 곳에서 상태 머신을 보기 시작했고 항상 그 패러다임으로 전환하고 싶은 욕망이 있습니다. 나는 더 엄격하게 정의된 상태와 그들 사이의 전환의 이점을 확실히 알고 있습니다. 저는 항상 앱을 간단하고 읽기 쉽게 만드는 방법을 찾고 있습니다. 저는 상태 머신이 이러한 방향으로 나아가는 단계라고 믿습니다. 개념은 단순하면서도 동시에 강력합니다. 많은 버그를 제거할 가능성이 있습니다.