HTML canvas, 마인드맵 이야기

이형준·2023년 8월 31일
1

트러블슈팅

목록 보기
2/7

HTMLcanvas 요소에 대해 잘 알고 계신가? 우선 난 이번 프로젝트 진행 전에는 canvas 에 대해 정~말 아무것도 몰랐다. 하지만 마인드맵 기반 협업 툴이라는주제를 다루는 프로젝트 특성상, 프로젝트 기간 내내 canvas 와 동고동락하게 되었고, 이제는 그래도 이 녀석이 무엇이고, 무엇을 할 수 있는 지 정도는 말할 수 있게 되었다. 그런 김에, canvas 와 함께하며 어떤 문제를 만났었고, 어떻게 해결했는지 적어보려 한다.

프로젝트 배경 🗺️

FE측에서 가장 급했던 안건은 다름아닌 '마인드맵'을 어떻게 그릴 것이냐 였다. 이때 canvas의 존재를 처음 알게 되었던 것 같다. 캔버스? 좋다! 그럼 이걸 이용해서 마인드맵을 화면에 그려보자! 라며 야심차게 작업을 하려 했으나,

이거.. 생각보다 엄청 난해하다. canvas 는 생각보다 다양한 기능을 제공하는 요소였고, 따라서 공식 문서 양도 방대했다. 또한 API들이 상당히 로우 레벨이라, 단순한 동그라미, 선 하나 그리는 데에도 많은 품이 들어갔다.

이런 다양한 기능들을 지원하는 canvas 를 처음부터 꾸려나간다면, 정말 원하는 것을 모두 canvas에 띄울 수 있을 것이다. 하지만 개발 기간은 사실상 2주,canvas 위에 쌩 코딩을 하는 것은 시간이 너무 부족할 것 같다는 판단 하에 canvas 렌더링을 도와주는 라이브러리를 찾아보는 데에 쓰기 시작했다.

팀원들과 같이 서칭해 본 후에, 후보를 세 가지까지 좁혔다. MindMup, D3.js, vis.js ! 세 라이브러리 모두 완성도 높은 라이브러리지만, 우리 팀의 최종 선택은 vis.js 였다. 이유를 하나하나 따져보자면,

MindMup마인드맵을 구성한다 라는 목적 하나로서는 훌륭했지만, 확장성이 발목을 잡았다. 우리 팀은 차후 마인드맵에 더해 여러 가지 기능을 얹을 심산이었기에, 아쉽지만 패스!

D3.jsvis.js 는 둘 다 다양한 데이터들의 가시화를 지원했지만, 개중에서도 vis.jsNetwork 형태가 마인드맵을 구성하기에 알맞고, 뼈대가 갖추어져 있어 원하는 기능을 구현하기 좋겠다고 판단하였다. 그렇게, 본격적인 작업에 들어갔다.

일단 마인드맵을 그려보자 ✒

  • 그림과 같이 NodeEdge 의 정보를 담은 배열을 객체로 묶어 vis.js 에게 전달하면, 데이터를 기반으로 canvas를 렌더링해준다.

vis.js 의 공식문서를 읽어가며 라이브러리 사용 방법과 내장 메서드들을 익혔다. 단순히 데이터 시각화만들 도와주는 라이브러리이니만큼 꽤나 심플한 동작 흐름을 가지고 있는데, 위의 그림과 같다.

다만, 위의 그림에서 그친다면 단순히 혼자 볼 수 있는 그래프를 그린 것일 뿐이다. 구현하려는 것이 여러 명이 동시에 편집 가능한 협업 툴이니 만큼, 여기서 끝나서는 아무 의미가 없다. 세션에 접속한 모든 유저가 같은 데이터를 받아 와 렌더링 해야 하고, 같은 데이터에 접근하여 수정이 가능해야 한다. 그럼 이건 어떻게 하지...?

여러 명이서 같이 보자 👨‍👩‍👧‍👦

해당 문제에 대한 나의 해답은 CRDT 알고리즘을 이용한 라이브러리인 Y.js 였다.

Y.js는 CRDT 알고리즘을 기반으로 공유 자원 관리를 도와주는 라이브러리이다. CRDT(Conflict-Free-Replicated Data Types) 알고리즘은 공유 데이터의 동기화 문제 해결을 위한 알고리즘으로, 비슷한 기능을 하는 알고리즘으로는 OT(Operational Transformation) 알고리즘이 있다. 저번에 쓴 글 -> 마우스 트래킹 이야기

설명은 이전 블로그 글로 대체하고, vis.js 를 통한 canvas 렌더링을 여기에 적용해보자.

서버에서 세션의 마인드맵 데이터를 관리하고, 변동이 생길 때 마다 이를 반영하여 각 클라이언트들에게 갱신된 마인드맵 데이터를 송신한다. 또한, 클라이언트 단에서는 변동 사항을 Listening 하고 있다가, 새로운 데이터를 수신한다. 이 때 canvas 를 리렌더링 해주어야 하는데, 따라서 마인드맵 데이터를 React.jsstate 로 관리한다. 그렇게 하면...

짠! 감격스럽게도 진짜로 동시에 마인드맵이 그려지는 모습을 확인할 수 있다. 그나저나 이 시절 페이지 진짜 못생겼다 🤣

배경도 넣어보자 🎨

노드 추가, 삭제, 수정.. 이런 기본적인 부분은, Y.js 에서 관리되는 마인드맵 데이터를 만지는 선에서 대부분 구현이 가능하다. 마인드맵 데이터가 담겨 있는 해시 테이블 자료구조에서 원하는 데이터를 찾고, 수정하고, 삭제하고 등등.. 구현할 기능의 양이 많았던 것 뿐, 순조롭게 대부분의 기능을 구현해 나갈 수 있었다. 하지만 모든 게 순조롭지는 않았으니.. 폴리싱 단계에서 생각 외로 나를 힘들게 했던 부분은 다름아닌 canvas 배경이었다.

  • 위 사진의 배경에 있는 점들이 작업 결과물이다. 이거 띄우려고 얼마나 힘들었는지 😂

canvas API를 다뤄본 적이 있다면, 엥? 저게 왜 어려웠다고 하는거지? 라고 할 수도 있겠다. 실제로 canvas 의 배경을 채우는 일은 크게 어렵지 않게 할 수 있으니까.

canvasfillRect() 메서드를 사용하거나, CSS 속성을 통해 간단히 배경을 채울 수 있..을 줄 알았다. 내가 작업하고 있는 캔버스가 조금 특이하다는 것을 몰랐었거든.

간과한 점은, vis.js 에서 렌더링해주는 canvas가 줌 인/아웃, 시점 드래그가 자유로운, 사실상 끝이 없는 무한한 캔버스라는 것이다. 따라서 단순히 캔버스 전체를 한 이미지 or 패턴으로 덮어씌우게 되면, 캔버스의 시점 이동에 따라 배경이 동적으로 변하지 않는다는 것이다. 이게 뭔 소리인가 싶을텐데, 백문이 불여일견. 이 때로 버전을 돌려서 한번 보여드리면,

  • 시점은 자유롭지만, 배경은 고정이라 뭔가 둥둥 떠다니는 느낌.. 이건 내가 원하던 게 아니야!!

조금 더 canvas 에 대해 공부한 후에, 두 번째 시도를 했다.

CanvasRenderingContext2D.createPattern() 메서드를 통해 배경을 채울 패턴을 만들고, fillStyle 속성을 생성한 패턴으로 설정하고, fill 메서드에 채울 공간을 인자로 넣는다. 이렇게 정해진 패턴을 만들어서 캔버스 상에 띄우면, 마치 도화지에 큰 이미지를 올려놓은 것 처럼 동작하여 시점에 따라 배경이 움직일 수 있다.

두 번째로 간과한 점은, canvas 의 원점 좌표가 일반적인 canvas 와 달랐다는 것. canvas 요소의 원점은 좌측 상단에서 (0,0) 으로 시작하여 양수로 증가하는 좌표계를 가지지만, vis.js 를 통해 렌더링 된 canvas 는 캔버스의 좌표계는 정중앙이 (0,0)으로, 4개의 사분면으로 이루어져 있었다.

이러니 원하는 위치에 배경이 잘 그려질 리 만무했고, 그냥 좌표를 때려박아 fill() 을 해버리니 마인드맵의 원점 오른쪽 아래로만 배경이 생겼다. 설상가상으로, 무한한 캔버스에 유한한 배경을 띄우니, 줌 아웃을 크게 해버리면 빈 공간이 노출되는 문제까지.. 허탈 그 자체 😂

결국 찾아낸 답 👍

오랜 시간 고민하고, 여러 가지를 시도해본 뒤에야 비로소 문제를 해결할 수 있었다. 물론 정답이라고는 할 수 없겠지만, 오래 고민한 문제를 풀어내는 경험은 언제나 짜릿하다 😀

우선, 좌표계에서 오는 오차는 vis.jsDOMtoCanvas API를 이용하여 해결했다. 웹 브라우저 상의 좌표를 canvas 의 좌표로 변환해주는 기능을 하는 데, 이를 통해 왼쪽 구석 좌표를 canvas 좌표로 변환한 곳에 렌더링함으로써, 기존에 요상한 곳에 렌더링되던 문제를 해결할 수 있었다.

둘째로, 배경이 채워지지 않은 빈 공간이 생기던 문제는, 캔버스 시점을 이동 할 때 캔버스의 유저 시점을 감싸는 큰 배경을 리렌더링함으로써 해결할 수 있었다.

  • 마침내 구현된 동적 배경! 너무 이쁘고~
profile
저의 미약한 재능이 세상을 바꿀 수 있을 거라 믿습니다.

0개의 댓글