IT일상

React에서 터치 스와이프 구현하기 2 (유한 상태 머신으로 리팩토링) 본문

프론트엔드

React에서 터치 스와이프 구현하기 2 (유한 상태 머신으로 리팩토링)

solo5star 2023. 9. 30. 17:26
리뉴얼 된 블로그로 보기: https://solo5star.dev/posts/58/

 

https://solo5star.tistory.com/57

 

React에서 터치 스와이프 구현하기

배경 우테코에서 개발하고 있는 요즘카페 서비스에선 메인 화면의 카페들을 스와이프 형식으로 보여주고 있습니다. 따라서 터치 스와이프 구현이 필요합니다. 이 글에서 JavaScript로 구현한 scroll

solo5star.tistory.com

위 글에서 이어지는 내용입니다.

 

 

배경

지난 글에서 요즘카페 서비스의 터치 스와이프를 고도화하는 구현을 진행했었습니다.

 

요구사항 1, 2까지 구현했었는데요, 터치하였을 때 터치에 반응하여 요소가 따라오게 하는 구현이랑, 판정을 통해 이전 혹은 다음 요소로 snap이 일어나게 하게끔 구현해보았습니다.

 

문제는 ... 구현을 하드하게 하다 보니 상태가 너무 많아졌다는 점인데요, 상태가 많은 것은 그렇다 쳐도 논리적으로 상태 모순이 발생할 수 있다는 문제가 있었습니다.

 

"터치 중"과 "snap 애니메이션"이 동시에 일어날 수 있을까요? 실제론 그렇지 않습니다. 터치 중일 땐 snap 애니메이션이 일어날 수 없고, snap 애니메이션이 일어나는 중에는 터치 중일 수 없습니다.

 

따라서 prevPosition은 터치 중일때만 number이며, 그 외의 상황에선 null이어야 합니다. 마찬가지로 snapStartedAt도 snap 애니메이션이 일어나는 중에만 number여야 합니다. 그렇지만 코드 상으론 둘 다 null 이거나, 둘 다 number이 될 수도 있습니다. 이렇게 되면 안되겠지만 가능할 수도 있다는 것이죠.

 

이것을 상태 모순이라고 합니다.

 

분기에 따라 사용하는 상태들을 그룹핑해보았습니다. 그림에서 볼 수 있듯이, "아무것도 안하는 중"일 땐 "터치 중", "snap 애니메이션 중"의 상태는 사용하지 않습니다. (엄격히 말하자면, 사용할 수 없어야 합니다)

 

그런데 이 상황은 마치 신호등과도 같다고 할 수 있습니다. "아무것도 안하는 중", "터치 중", "snap 애니메이션 중" 3가지는 동시에 존재할 수 없으며, 무조건 하나만 활성이 되어 있어야 합니다.

 

이러한 특성은 마치 유한 상태 머신(Finite-State Machine)이라고도 할 수 있습니다.

 

유한 상태 머신(Finite-State Machine)이란 무엇일까요? "유한" 이라는 것은 가질 수 있는 상태(state)가 유한임을 의미합니다. "상태 머신"은 주어진 복수개의 상태 중 하나만을 가지며, 외부 자극에 의해 다른 상태로 전이(transition)되는 모델을 뜻합니다.

 

유한 상태 머신은 주로 게임에서 많이 언급되는데요, 주로 몬스터와 같은 AI의 행동에 유한 상태 머신 모델을 적용합니다. 몬스터가 "평소"와 같이 행동하다가 플레이어를 발견하면 "쫒아가는" 상태로 전이되고 공격합니다. 불리한 것 같으면 "도망가는" 상태로 전이할 수도 있구요.

 

 

유한 상태 머신(Finite-State Machine) 적용

본격적으로 유한 상태 머신 모델을 적용해보도록 하겠습니다.

 

터치 스와이프의 동작에 유한 상태 머신 모델을 적용하면 3가지의 상태와 함께 전이(transition) 조건을 정의할 수 있습니다. 외부의 어떤 자극에 어떻게 전이시킬지 정하는 것이죠.

 

위 그림과 같이 전이 방향을 그려보았습니다. 사용자의 터치 입력에 반응하여 전이가 결정됩니다.

 

유한 상태 모델의 상태를 적절히 표현할 수 있는 문법이 때마침 TypeScript에 있습니다. 바로 tagged union인데요, union은 취할 수 있는 타입들의 집합입니다. 런타임에서는 union의 타입들 중 하나만 존재할 수 있기 때문에 유한 상태 머신을 표현하기에 가장 찰떡같은 문법이라고 할 수 있습니다.

 

tagged는 union의 타입을 구분하는 방법입니다. MachineState 타입을 보시면 공통적으로 label 프로퍼티를 가진 것을 볼 수 있는데요, 이 프로퍼티를 기준으로 Type Narrowing이 동작하게 됩니다.

 

전이는 React의 useState에서 제공해주는 dispatch 함수를 사용하면 됩니다.

 

 

상태와 전이 추가하기

위의 요구사항 3번을 추가한다면 어떻게 추가할 수 있을까요? 구현 방법에 대한 자세한 언급은 하지 않지만 유한 상태 머신 모델에서 상태와 전이를 어떻게 추가할 수 있는지 보여드리도록 하겠습니다.

 

 

"터치 중" 으로 넘어가기 전에, "스와이프 방향 체크" 를 거치도록 해주었습니다. 세로 스와이프인데 가로로 스와이프를 하려고 하면 "아무것도 안하는 중" 으로 전이되구요.

 

새로운 상태와 전이를 어떻게 추가하는지 잘 보셨나요? 외부의 자극(터치)에 따라 어떻게 동작해야 할지를 잘 생각해본다면 쉬울 겁니다.

 

TypeScript에서도 마찬가지로 tagged union에 상태를 추가해주기만 하면 됩니다. 간단하죠? 그리고 앞서 정의한 대로 상태가 전이될 수 있도록 구현을 수정해주면 될 것 같습니다.

 

 

소스코드와 시연

본문에서 소스코드와 함께 설명하면 좋았겠지만...! 구현이 상당히 길고 복잡한 관계로 gist 링크를 첨부하겠습니다. 실제 요즘카페에서 사용되는 소스코드를 그대로 들고 왔습니다.

https://gist.github.com/solo5star/5eaafa198a9fd794509cdce0a07a687a

 

CSS scroll-snap implementation from scratch using JavaScript(TypeScript)

CSS scroll-snap implementation from scratch using JavaScript(TypeScript) - ScrollSnapContainer.tsx

gist.github.com

 

스와이프하려는 방향이 일치할 때
스와이프하려는 방향이 일치하지 않을 때

상태 전이가 발생하는 모습을 시각화해보았습니다. 각 상태에서 외부 자극이 어떻게 들어오냐에 따라 판단하고 전이가 되는 모습을 볼 수 있습니다.

 

300ms vs 1000ms

CSS scroll-snap에서는 불가능했던 snap animation의 속도 조절도 위와 같이 가능합니다.

 

easing 함수를 변경하면 위와 같이 통통 튀는 애니메이션도 적용할 수 있구요,

 

위, 아래 요소만 로드하여 DOM 생성을 최소화할 수도 있습니다.

 

커스텀 애니메이션도 가능합니다! 🥳

 

요즘카페의 길고 긴 터치 스와이프 고도화 구현기가 이렇게 마무리되었습니다 🎉 JavaScript로 하나하나 구현해서 그런지 아직까진 잔버그들이 있지만 하나씩 해결해나갈 예정입니다.

 

 

맺으며

프론트엔드 개발을 하며 디자인 패턴을 자주 사용하진 않지만, 본문의 터치 스와이프처럼 복잡한 로직에서 특효약이 될 수 있는 경우가 종종 있습니다. 부적절한 모델을 선택한다면 오히려 더 어려운 문제로 발전시킬 수도 있지만... 적합한 모델을 잘 선택한다면 문제 해결에 적지 않은 도움이 될 것입니다.

 

터치 스와이프의 상태 흐름이 많이 복잡했는데, 다이어그램을 그려보는 것이 많은 도움이 되었습니다. 때로는 시각적으로 표현해보는 것도 문제 해결에 많은 도움이 되지 않을까 싶습니다.

Comments