@wordpress/data 및 TypeScript를 사용하는 유한 상태 기계
게시 됨: 2021-09-16애플리케이션에서 로그인 화면을 디자인한다고 가정합니다. 사용자가 처음 볼 때 빈 양식이 있습니다. 사용자는 필드를 채우고 모든 설정이 완료되면 로그인 버튼을 눌러 자격 증명을 확인하고 로그인합니다. 확인이 성공하면 다음 화면으로 이동합니다. 그러나 그렇지 않은 경우 오류 메시지가 표시되고 다시 시도하도록 요청됩니다.
이러한 화면의 상태를 구현하고 TypeScript 및 @wordpress/data
를 사용하여 응용 프로그램의 상태를 정의하는 방법에 대한 내 게시물을 읽은 경우 다음과 같이 했을 것입니다.
type State = { readonly username: string; readonly password: string; readonly isLoggingIn: boolean; readonly errorMessage: string; };
상태를 관리하는 데 필요한 모든 필드가 있지만... 이것이 우리가 할 수 있는 최선입니까?
유한 상태 기계
FSM( Finite State Machine )은 광범위하게 말하면 유한한 상태 집합과 상태 사이의 가능한 전환을 정의할 수 있는 수학적 모델입니다. (위키피디아에 대한 자세한 정보). 나는 이 추상화가 우리 애플리케이션의 상태를 모델링할 때 우리의 요구에 완벽하게 들어맞기 때문에 좋아합니다. 예를 들어 로그인 화면의 상태 다이어그램은 다음과 같을 수 있습니다.

보시다시피 구현하려는 동작을 명확하고 간결한 방식으로 캡처합니다.
@wordpress/data
로 상태 정의하기
React 소개 시리즈에서 @wordpress/data
패키지를 사용하여 앱 상태를 만들고 관리하는 방법을 보았습니다. 데이터 저장소의 주요 구성 요소를 정의하여 그렇게 했습니다.
- 상태를 쿼리하는 선택기 세트
- 업데이트 요청을 트리거하는 일련의 작업
- 현재 상태와 업데이트 작업이 주어지면 상태를 업데이트하는 감속기 기능
본질적으로 @wordpress/data
의 저장소는 이미 유한 상태 기계처럼 작동합니다. 감속기 기능이 작업을 사용하여 한 상태에서 다른 상태로 "전환"되기 때문입니다.
( state: State, action: Action ) => State
@wordpress/data
에서 TypeScript로 유한 상태 기계를 정의하는 방법
@wordpress/data
는 위에서 몇 줄을 소개한 FSM 모델에 매우 가까운 것 같습니다. 우리는 우리가 있을 수 있는 특정 상태와 그 사이의 전환이 누락되었습니다. 따라서 어떻게 할 수 있는지 자세히 살펴보겠습니다. 한 번에 한 단계씩 이러한 문제를 해결하십시오.
명시적 상태
우리는 앱 상태의 가능한 구현을 보여줌으로써 이 게시물을 시작했습니다. 우리의 첫 번째 제안은 작업을 완료하는 많은 속성이었지만 유한 상태 기계처럼 보이지 않았습니다. 상태가 모델에 명시적으로 정의되어 있는지 확인하는 것으로 시작하겠습니다.
type State = | Form | LoggingIn | Success | Error; type Form = { readonly status: 'form'; readonly username: string; readonly password: string; }; type LoggingIn = { readonly status: 'logging-in'; readonly username: string; readonly password: string; }; type Success = { readonly status: 'success'; // ... }; type Error = { readonly status: 'error'; readonly message: string; };
꽤 뻔하죠? 상태당 하나의 유형이 있으며 앱의 전체 상태는 유형의 구별된 합집합으로 정의됩니다. (a) 상태가 "서로 다른 유형의 합집합"(즉, Form
이거나 LoggingIn
이거나 무엇이든 상관 없음)이고 (b) 속성(이 경우에는 , status
) 매 순간에 어떤 특정 상태를 가지고 있는지 구별할 수 있습니다.
이 솔루션이 처음에 사용했던 솔루션보다 훨씬 낫다고 생각합니다. 원래 솔루션에서 우리는 "무효 상태"를 정의할 수 있었습니다. 왜냐하면 로그인 중일 수 있고( isLoggingIn
을 true
로 설정함) 동시에 오류 상태에 있을 수 있기 때문입니다( errorMessage
를 비어 있는 값이 아닌 다른 값으로 설정하기만 하면 됩니다. 끈). 그러나 이것은 분명히 말이 되지 않습니다! 어떤거야? 로그인 중입니까 아니면 오류가 표시되어야 합니까?
반면에 새로운 솔루션은 상태를 나타낼 때 훨씬 더 정확합니다. "유효하지 않은" 상태를 "불가능한" 상태로 만듭니다. LoggingIn
에 있다면 오류 메시지를 설정할 방법이 없습니다(속성이 없습니다!). Error
가 표시되면 로그인할 수 없습니다. 훨씬 낫지 않나요?
행위
@wordpress/data
에서 작업은 우리 상태의 저장소를 업데이트합니다. 우리는 상태를 FSM으로 모델링하고 있으므로 우리의 행동은 원래 다이어그램의 화살표가 될 것입니다. 당신이 해야 할 일은 그것들을 구별된 유형의 합집합으로 모델링하는 것뿐입니다(관례에 따라 구별자는 일반적으로 type
으로 명명됨). 이제 계속할 수 있습니다.
type SetCredentials = { readonly type: 'SET_CREDENTIALS'; readonly username: string; readonly password: string; }; type Login = { readonly type: 'LOGIN'; }; type ShowApp = { readonly type: 'SHOW_APP'; // ... }; type ShowError = { readonly type: 'SHOW_ERROR'; readonly message: string; }; type BackToLoginForm = { readonly type: 'BACK_TO_LOGIN_FORM'; };
우리의 행동은 원래 다이어그램에 있던 화살표를 나타내야 합니다. 맞죠? 글쎄, 나는 당신에 대해 모르지만 나는 이러한 행동 유형이 전혀 화살표처럼 보이지 않는다고 생각합니다! 화살표에는 방향이 있습니다(한 상태에서 다른 상태로 이동). 행동은 하지 않습니다.

특정 작업은 한 상태에서 다른 상태로 이동할 때만 유용하다는 것을 코드에서 명확히 할 방법이 필요합니다. 그리고 지금까지 내가 찾은 최고의 솔루션은 감속기 자체를 사용하는 것입니다.
function reducer( state: State, action: Action ): State { switch ( state.status ) { case 'form': switch ( action.type ) { case 'SET_CREDENTIALS': return { status: 'ready', username: action.username, password: action.password, }; case 'LOGIN': return { ...state, status: 'logging-in' }; } case ...: ... } return state; }
리듀서가 state
의 status
를 필터링하여 시작하면 화살표의 "소스"를 알 수 있습니다. 그런 다음 현재 작업을 보고 "외향 화살표"인 경우 새 "대상" 상태를 생성합니다.
위의 스니펫에서 이것이 어떻게 작동하는지 명확하게 볼 수 있습니다. 현재 status
가 form
인 경우 SET_CREDENTIALS
작업은 우리가 있던 것과 동일한 상태(자격 증명 업데이트, duh)로 이동하고 LOGIN
작업은 상태를 logging-in
변경합니다. 이 상태의 다른 모든 작업은 무시되므로 상태가 변경되지 않습니다.
다 좋고 완벽하지만... 나도 몰라, 난 그 코드를 별로 좋아하지 않아, 그렇지? 내 코드가 명확하고 간결하며 설명이 필요 없고 형식이 안전한지 확인하고 싶습니다. 그렇게 할 수 있습니까?
강력한 형식의 감속기
방금 발생한 혼란을 해결하려면 FSM의 가능한 각 상태에 자체 감속기가 있도록 reducer
를 리팩토링하면 됩니다. 이 작업을 올바르게 수행하면 강력한 형식이 지정되고 허용되는 항목과 허용되지 않는 항목이 명확해집니다.
애플리케이션의 각 상태에 대한 유형을 만드는 것으로 시작하겠습니다. 각 유형은 주어진 상태에서 다른 곳으로 이동할 수 있는 작업을 그룹화합니다. 예를 들어, Form
상태에는 두 개의 바깥쪽 화살표( SetCredentials
및 Login
)가 있으므로 두 작업을 FormAction
유형을 생성해 보겠습니다. 각 상태에 대해 프로세스를 반복하면 다음을 얻을 수 있습니다.
type FormAction = SetCredentials | Login; type LoggingInAction = ShowError | ShowApp; type SuccessAction = ...; type ErrorAction = BackToLoginForm; type Action = | FormAction | LoggingInAction | SuccessAction | ErrorAction;
다음으로 필요한 모든 감속기를 정의합니다. 다시 말하지만, 각 상태에 대해 하나씩:
function reduceForm( state: Form, action: FormAction ): Form | LoggingIn { switch ( action.type ) { switch ( action.type ) { case 'SET_CREDENTIALS': return { status: 'ready', username: action.username, password: action.password, }; case 'LOGIN': return { ...state, status: 'logging-in' }; } } reduceLoggingIn: ( state: LoggingIn, action: LoggingInAction ) => Success | Error reduceError: ( state: Error, action: ErrorAction ) => Form reduceSuccess: ( state: Success, action: SuccessAction ) => ...
TypeScript와 방금 정의한 유형 덕분에 이제 소스 상태와 각 리듀서가 기대하는 일련의 작업 및 결과적으로 얻을 수 있는 대상 상태에 대해 매우 정확하게 알 수 있습니다.
마지막으로 고유한 reducer
기능으로 모든 것을 연결하기만 하면 됩니다.
function reducer( state: State, action: Action ): State { switch ( state.status ) { case 'form': return reduceForm( state, action as FormActions ) ?? state; case 'logging-in': return reduceLoggingIn( state, action as LoggingInActions ) ?? state; case 'error': return reduceError( state, action as ErrorActions ) ?? state; case 'success': return reduceSuccess( state, action as SuccessActions ) ?? state; } }
제 생각에는 결과 코드가 더 명확하고 유지 관리하기 쉽습니다. 그리고 무엇보다도 TypeScript는 이제 우리가 하고 있는 일을 확인하고 누락된 시나리오를 포착할 수 있습니다. 엄청난!
결론
이 게시물에서 우리는 유한 상태 머신이 무엇인지, 그리고 TypeScript를 사용하여 @wordpress/data
저장소에서 유한 상태 머신을 구현하는 방법을 보았습니다. 우리의 최종 솔루션은 매우 간단하지만 많은 이점을 제공합니다. 앱의 상태와 의도를 더 잘 포착하고 TypeScript의 기능을 활용합니다.
이 게시물이 마음에 드셨으면 좋겠습니다! 그렇다면 친구 및 동료와 공유하십시오. 그리고 아래 코멘트 섹션에서 이러한 문제를 해결하는 방법을 알려주십시오.
Unsplash에서 Patrick Hendry의 추천 이미지.