마우스 트래킹 이야기

이형준·2023년 8월 17일
1

트러블슈팅

목록 보기
1/7


마우스 트래킹은, 실시간 협업 툴을 구상한다면 빼놓을 수 없는 기능이다. 실시간으로 다른 유저의 마우스 움직임을 볼 수 있다는 것은, 다른 어떠한 기능보다도 유저에게 협업을 하고 있다는 느낌을 주기에 안성맞춤이기 때문!

더불이 이번에 진행한 프로젝트에서 내가 주로 개발을 맡았던 부분이기도 하다. 다양한 곳에서 문제를 어떤 문제를 마주했고, 어떻게 해결했으며, 어떻게 이 문제를 개선해왔는지 한번 적어보려 한다.

첫 시도 😀

본격적인 내용에 앞서 우리 프로젝트가 어떤 플로우로 통신이 이루어지는지 알아볼 필요가 있겠다.

보는 바와 같이, 세션마다 서버에 공유 자원을 둔 형태로 이루어져 있다. 각 클라이언트들은 자유롭게 공유 자원에 접근하여 수정한다. 공유 자원에 변동이 생기면, 서버는 이러한 변동 사항을 갈무리하여 Observe고 있는 다른 클라이언트에게 전달한다.

물론 실제로 공유하는 자원을 실시간으로 충돌 없이 관리하고, 싱크를 맞추어 브로드캐스팅 해 주는 일은 절~대로 간단한 일이 아니다. 그럼에도 불구하고 위의 그림과 같은 단순한 플로우로 구현할 수 있었던 이유는 순전히 Y.js 프레임워크의 덕이라고 할 수 있다.

Y.js는 CRDT 알고리즘을 기반으로 공유 자원 관리를 도와주는 라이브러리이다. CRDT(Conflict-Free-Replicated Data Types) 알고리즘은 공유 데이터의 동기화 문제 해결을 위한 알고리즘으로, 비슷한 기능을 하는 알고리즘으로는 OT(Operational Transformation) 알고리즘이 있다. 이에 대해 공부했던 내용은 차후 또 포스팅하는 걸로.

그렇다면 위와 같은 플로우에서 각 유저간 마우스 커서 위치를 동기화한다면 어떤 방법을 써야 할까? 누구나 가장 먼저 떠올릴 방법은 아마 마우스가 움직일 때 마다 좌표를 공유 데이터에 넣어서 브로드캐스팅 하는 것 일것이다. 나 역시 이 생각을 가장 먼저 하였고, 그 즉시 실행에 옮겼다.

반쪽짜리 성공, 첫 번째 문제 🤔

그렇게 실행에 옮긴 첫 번째 구현 시도는, 아쉽게도 반쪽짜리 성공이었다. 우선 유저 서로간에 마우스 좌표를 공유하여 화면에 띄우는 것은 성공했다! 생각보다 별거 없구나~ 하며 신나게 마우스를 흔드는 것도 잠시, 뭔가 단단히 잘못되었음을 깨닫게 되었다. 한쪽에서 루트 노드에 마우스를 올려놓아도, 다른 쪽에서 해당 마우스는 완전 엉뚱한 곳에 있던 것.

중요한 포인트를 고려하지 못했던 것이 이유였다. 바로 우리 웹사이트에서 마우스가 행동하는 주 무대는 바로 마인드맵이고, 이 마인드맵은 HTML의 줌 인/아웃, 드래그가 가능한 Canvas를 기반으로 이루어져 있다는 것이다. 당연하게도 유저가 보고 있는 캔버스의 일부가 각자 다르기에 이러한 문제가 발생한 것!

  • 만만한 녀석이 아니란 건 알고 있었지만..

첫 번째 문제 해결 🤔

문제의 원인을 찾아낸 후에는 종일 얼이 빠진 상태로 있었던 것 같다. 그도 그럴 것이 '이 때 당시에는 이게 가능하긴 한 거야?' 란 생각뿐이었다 🤣 각 유저마다 캔버스에서의 시점을 자유롭게 이동하는 데, 이걸 다 보정해서 보여줘야 한다니..

많은 시도를 해보았지만, 전부 완벽한 해답이 되지 못했다. 생각보다 의외의 곳에서 해답을 찾을 수 있었는데, 마인드맵을 캔버스에 그리기 위한 라이브러리의 내장 함수에서 답을 찾을 수 있었다. 그것도 내가 원하는 동작 그대로!!!

  • 아니 너 거기 있었어?!

이게 진짜 찾고나서 반갑기도 했지만, 상당히 억울하기도 했다. 왜냐하면 이 라이브러리 공식 문서는 정말 프로젝트 시작부터 지긋하게 봤던 거라서 🤣 주의깊게 보지 않은 내 잘못이지 싶었다.

딱 가려운 곳을 긁어 줄 수 있는 함수들을 찾은 거라서, 이후로는 일사천리로 작업할 수 있었고, 감격스럽게도 서로의 마우스 좌표가 잘 동기화되는 모습을 확인할 수 있었다.

두 번째 문제 🐢

마우스 트래킹에서 만난 두 번째 문제는 성능 이슈였다. 완성된 마우스 트래킹 스켈레톤 코드의 흐름은 다음과 같았다.

  1. 유저가 마우스를 움직여 onmousemove 이벤트가 발생한다.
  2. 이벤트 리스너는 변동된 마우스 좌표를 Canvas 좌표로 변환해 서버로 전송한다.
  3. 서버는 데이터를 받아 동기화 후, 다른 유저에게 브로드캐스팅한다.
  4. 다른 유저는 전송받은 좌표를 DOM 좌표로 변환해 화면에 렌더링한다.

완성된 코드는 확실하게 동작했다. 하지만 또다른 문제를 야기했다. 유저가 두명이라면 부하가 크지 않겠지만, 셋이라면? 혹은 네명이라면? 서버에서 마우스 데이터를 동기화 하는 데에 많은 자원을 사용하게 된다. 다른 객체의 동기화에도 악영향을 끼칠 것은 불보듯 뻔한 사실이다. 실제로 팀원들 모두가 접속한 후 테스트를 진행해보면, 툭툭 끊기는 것을 확인하기도 했고 🤔

두 번째 문제 해결? ❓

성능 이슈가 발생한 본질적인 이유가 뭘까? 너무 잦은 동기화 요청을 보내는 것이다. 이러한 본질적인 문제를 해결하기 위해서, 동기화 요청이 특정 텀을 두고 동작하게 하는, 일종의 스로틀링(Throttling)을 도입해 보았다.

export const throttle = (callback, delay) => {
    let previousCall = new Date().getTime();
    return function () {
        const time = new Date().getTime();

        if (time - previousCall >= delay) {
            previousCall = time;
            callback.apply(null, arguments);
        }
    };
};
  • 마우스 외에도 다양한 곳에 사용했던 throttle 함수. 원하는 함수에 원하는 만큼 딜레이를 주어 스로틀링 할 수 있도록 작성했다.

스로틀링을 걸었기에 마우스 움직임이 더이상 매끄럽지는 않았지만, 성능 개선은 탁월했다. 10ms의 텀만을 두어도, 동기화 요청 빈도가 기존의 2할 정도로 확 떨어질 정도로!

다만, 새로운 과제가 생겼다. 약간의 스로틀링만을 걸었기에 여전히 마우스 트래킹은 부드럽게 잘 이루어졌지만, 마우스를 빠르게 움직이는 경우에는 툭툭 끊겨서 동기화되는 모습을 확인할 수 있었다.

추가 개선, 인터폴레이션 😀

어떤 경우든 마우스 좌표가 툭툭 끊기며 움직이는 상황은 달갑지 않다. UX적으로 매우 좋지 않고, 무엇보다 뭔가 완성도 없어 보인다!!

이러한 부분을 어떻게 개선할 수 있을 지 서칭하다가, 보간법, 속칭 인터폴레이션(Interpolation) 이라 하는 개념에 대해 알게 되었다. 인터폴레이션은 두 좌표 사이 중간의 값을 추정하는 것을 말하는데, 이 녀석을 활용해보면 좋겠다는 생각이 들었다!

다른 유저의 마우스 좌표들을 받아 내 화면에 띄워야 하는 상황을 가정해보자. 동기화 하여 띄워야 하는 다른 유저의 마우스 좌표가 { x:1, y:1 }, { x:3, y:3 } 와 같이 연속적으로 들어오고 있다. 이 때 인터폴레이션을 이용하여 중간값을 예측, { x:1, y:1 }, { x:2, y:2 }, { x:3, y:3 } 와 같이 렌더링한다면 훨씬 부드럽게 보일 것이다.

이렇게 되면 스로틀링에서 오는 마우스 좌표 간극을 메꿀 수 있기에, 성능과 유저 경험 두 마리 토끼를 다 잡을 수 있다.

2차원의 경우 계산식도 크게 복잡하지 않다. 두 좌표 사이의 평균값을 연산하여 보간값을 구하면 그만, 그 후 현재 좌표를 렌더링하기 전에 보간값을 먼저 렌더링해주면 된다.

마지막 욕심, 애니메이션 🚲

좋다. 너무나 좋다. 처음에 비한다면 엄청난 성능 개선을 이뤄냈고, 유저 친화적인 부분도 충분히 잡아냈다. 하지만 사람의 욕심에는 끝이 없다?! 이제는 움직임이 끊기는 것 자체가 싫어졌다. 그렇다면 정말 일절 끊기는 일 없게 각 좌표간에 애니메이션을 통해 이동하게 한다면 어떨까?

불가능해 보이지는 않았다. 분명히 이런 애니메이션이 있는 웹 사이트들을 본적이 있었으니까. 하지만 리액트 생 초짜인 나에게는..

가볍게 시도해봤는데, 아무래도 지금 프로젝트 위에 얹기에는 쉽지 않을 것 같다. 마우스 애니메이션을 넣으니까 아예 다른 곳에서 터져버리더라고.. 지금까지 이런 적 없었던 게 다행이다. 하지만 포기하지 않는다! 시간이 나면 관련 라이브러리를 공부하던 해서 내 꼭 SOMETHINK에 애니메이션을 넣으리라 😀

profile
저의 미약한 재능이 세상을 바꿀 수 있을 거라 믿습니다.

0개의 댓글