implement multi-click with Angular, RxJS

Jurung Park
Pagecall Engineering
7 min readJan 29, 2019

YouTube 앱에서 n10초 앞으로 가기 또는 뒤로 가기를 하기 위해 사용자가 취해야하는 행동은 무엇일까? 바로 화면의 오른쪽 또는 왼쪽부분을 n+1번 빠르게 탭하는 것이다. 동영상의 오른쪽 부분을 빠르게 두 번 탭하면 앞으로 10초, 세 번 탭하면 앞으로 20초 이동하게 된다.

영상의 오른쪽을 3번 탭하여 20초 앞으로 가기

이 글에서 설명하고자 하는 기능이 이러한 다중클릭 기능이다. 사실 글에서 설명하고자 하는 기능은 위에서 예시로든 YouTube 앱의 사례와는 약간 다르다. YouTube앱에서 앞뒤로 이동할 때에는 다중클릭이 발생되고 있는 중간 상태에서도 중간 액션을 하지만 우리가 구현한 기능은 다중클릭이 모두 발생했을때 즉, n번 클릭이 모두 완료되었을 때 비로소 액션을 취한다. 우리는 이러한 기능을 Angular Directive, RxJS의 유용한 Operator를 활용하여 쉽게 구현했다.

RxJS를 쓰지 않으면 얼마나 귀찮을지 상상해보자

물론 RxJS의 Observable 패턴을 쓰지않더라도 충분히 구현할 수 있다. 내가 부지런하여 setTimeout, clearTimeout 그리고 수많은 if 문과 timestamp들로 떡칠된 안타까운 코드를 선보여줄 수 있었으면 좋았겠지만 솔직히 귀찮아서 구현해보지 않았다. 상상해보면 알겠지만 고민하기조차 싫다. ‘빠르게 3번 클릭하는 상황에서 2번째 클릭 직후 더블클릭으로 인식되지 않으려면 어떻게 해야되지?’, ‘setTimeout을 걸어서 일정 간격만큼 기다리다가 일정 간격안에 새로 클릭 이벤트가 발생하면 clearTimeout을 하고 다시 setTimeout을 걸어야 될까?’, ‘클릭이 몇 번 중첩됐는지 저장하는 변수는 어디에 두고 관리할까?’ 이런 질문들에 대한 적절한 대답을 찾더라도 깔끔하고 우아한 코드를 짜는 것은 또 다른 문제이다.

Angular Directive의 유용함!

Angular Directive를 쓰면 마치 multiClick이라는 이벤트가 태초부터 존재했던 것 처럼 쓸 수 있다. dblclick(https://developer.mozilla.org/en-US/docs/Web/Events/dblclick) 처럼..

목표

우리는 다음과 같이 Angular Component 탬플릿에서 쓸 수 있는 MultiClickDirective를 만들것이다.

너무 간단하지 않은가? 마치 원래부터 multiClick 이라는 이벤트가 DOM에 있었던것 처럼 코딩할 수 있다. 물론 click 횟수를 넘겨주기 위해서 이벤트 값이 조금 복잡해지긴 했지만 Component를 직접 코딩하는 개발자는 복잡한 타이밍 이슈를 생각하지 않아도 된다.
만약 빠르게 5번 클릭을 하면 5번 클릭이 끝나고 나서 onMultiClick 함수가 실행되고 times에는 5라는 값이 전달될 것이다. 5번의 연속된 클릭이 발생하는 중간에 onMultiClick이 실행되는 일은 없을 것이다. 딱 우리가 바라는 동작방식이다. 자, 그렇다면 저 마법같은 appMultiClick은 어떻게 만들면 좋을까? 일단 정답부터 공개하고 설명하겠다.

정답공개

전체코드이다. 이거 그대로 가져다 쓰면 된다.

전체코드를 갑자기 들여다보면 혼란스러울 수 있다. 이제 논리적인 시간 순서대로 한 덩어리씩 살펴보도록 하자.

Raw click event를 받아오는 부분

click stream 의 뿌리

쌩 클릭 이벤트를 받아오는 부분이 당연히 필요할 것이다. Directive에 click 이벤트가 발생하면 clickStream$ 이라는 Subject에 이벤트를 흘려준다. clickStream$은 코드 어딘가에서 filtering되고 transformating되어 다중클릭 이벤트의 흐름으로 변모할 것이다. 아래 코드를 보자!

Raw click → multi-click

단일 클릭 흐름이 다중 클릭 흐름으로 변하는 과정!

중요한 두 operator가 등장한다. buffer, debounceTime! buffer의 성질부터 살펴보자.

buffer는 Observable을 인자로 받는다. buffer가 받는 Observable은 buffer가 ‘토할' 타이밍을 잡아주는 것인데 이런식으로 타이밍을 전달해주기 위한 Observable을 흔히 notifier라고 칭한다. notifier을 통해 notify 신호가 오면 buffer는 그 순간까지 쌓아왔던 모든 값들을 한꺼번에 ‘토해내고' 그 다음부터 들어오는 값들을 다시 차곡차곡 쌓는다.
백문이 불여일견! 구슬을 가지고 놀아보자. https://rxmarbles.com/#buffer

debounceTime은 number를 인자로 받는다. 값들이 연속적으로 들어오는 상황에서 특정한 시간간격 이하로 너무 빠르게 들어오는 값은 무시되고 정해진 시간간격만큼 기다리는 동안 다음 값이 들어오지 않으면 마지막 값을 넘긴다.
백문이 불여일견! 구슬을 가지고 놀아보자. https://rxmarbles.com/#debounceTime
debountTime은 특히 ‘검색창 자동완성' 이나 ‘비밀번호 실시간 검증' 기능에서 자주 쓰인다.

구글 검색창에 검색어를 입력하는 경우를 상상해보자 키보드를 한번 두드릴때마다 검색어 추천을 해댄다면 사용자가 성가실 뿐만 아니라 구글 서버도 무척 귀찮아할 것이다. 검색어를 빠르게 타이핑하는 과정중이 아니라 검색어를 치고 일정 시간이 흐르고 난 뒤에 추천 검색어를 질의하는것이 바람직할 것이다.

회원가입을 위해 비밀번호를 새로 만드는 경우를 상상해보자. 이때도 마찬가지로 키보드를 한번 두드릴때마다 ‘안전하지 않은 비밀번호입니다’ 따위의 경고메세지를 보여준다면 사용자가 굉장히 언짢아 할 것이다. 어느 정도 입력이 완료되고 나서 비밀번호를 검증하는 것이 바람직할 것이다.

그렇다면 우리의 pipe안에 있는 operator 조합은 어떻게 단일 클릭 흐름에서 다중 클릭 흐름을 만들 수 있었을까? 우선 debouceTime을 통해서 연속 클릭이 마무리 되는 타이밍을 잡는다. 예시에서는 250ms를 시간 간격으로 정했는데 마지막 클릭 입력 후 250ms동안 아무런 입력이 없으면 연속 클릭이 완료된 것으로 간주하고 신호를 보내준다. 그러면 buffer 에서 그 신호에 맞춰 쌓여있던 이벤트들을 토해낸다.

만약 아래와 같은 순서로 click event가 발생한다면 multiClick event는 어떤 형태로 생겨나게 될까?

(click)-(100ms)-(click)-(150ms)-(click)-(300ms)-(click)-(100ms)-(click)

조금만 생각해보고 아래 정답을 확인해보자.

(100ms + 150ms + 250ms)-(multiClick-3)-(50ms+100ms+250ms)-(multiClick-2)

총 5번의 클릭이 삼중클릭 한번과 더블클릭 한번으로 나눠졌다. 진하게 표시한 250ms는 임의로 정해준 기다리는 시간으로 이 수치가 짧을 수록 다중클릭의 반응성은 좋아지지만 그 만큼 빠르게 클릭을 해야만 다중 클릭으로 인정받을 수 있기 때문에 피지컬이 딸리는 사용자는 안타깝게도 다중클릭을 이용할 수 없을 것이다.

(급) 마무리

공개된 코드 중에서 설명하지 않은 코드 부분도 있다. Angular와 RxJS가 익숙한 사람은 마치 공기처럼 당연하게 생각할 수 있겠지만 그렇지 않은 독자도 있을 것이다. 왜 필요한지는 각자가 천천히 생각해보면 도움이 될 것이다.
setTimeout과 setInterval을 써야만 하는 상황인가? latestTimestamp 따위의 변수를 만들어서 Date.now()를 매번 저장하고 if (currentTimestamp-latestTimestamp) 와 같은 조건 검사를 해야되는 상황인가? 타이밍과 관련된 이슈가 있을 땐 항상 RxJS 사용을 검토해보라. 장담컨대 반드시 필요한 operator가 이미 구현되어있을 것이다.

--

--