React Application에서 drag&drop과 resize

QueryPie 개발기#5: 쿼리 작성 패널 탭 기능과 쿼리 실행 결과 패널 구현

Thomas Jang
QueryPie
12 min readFeb 7, 2019

--

두 번째 스프린트가 끝났다. 이번 스프린트 기간 중에는 크리스마스와 해가 바뀌는 것 때문에 휴일이 많았다.

스프린트 기간 2주 동안 업무 일수는 6일. 업무 일수가 적기 때문에 처리해야 할 이슈도 상대적으로 적었다. 이슈의 개수가 적다보니 부담이 덜 했고, 혹시 시간이 부족하다면 휴일을 활용하여 문제를 해결하면 된다고 생각하니 마음이 편했다.

하지만 이런 기대가 사라지기까지 오랜 시간이 필요하지는 않았다.

이번 스프린트의 핵심목표

  1. 다중 탭으로 QueryPanel (쿼리 구문 작성 패널)관리
  2. QueryPanel 에디터에서 쿼리를 실행하면 결과를 보여주기

QueryPanel 탭 DOM 구성

핵심 목표를 정하고 작업을 착수하기 시작했다. 해결해야 할 이슈는 다중 탭으로 QueryPanel을 관리하는 것이었는데 개발에 앞서 모델을 구상할 필요가 있었다.

1안. Switch 방식 탭 DOM 구조

우선 위의 그림과 같은 구조는 상단(또는 하단)에 탭 바를 두어 탭의 라벨을 표시해주고 그 아래 (또는 위)에 미리 준비된 탭의 컨텐츠들 중에서 선택한 탭의 상태를 active로 변경하여 보여주는 방식이다.

1안은 개별 탭을 선택해서 보여줄 수 있기 때문에 각 탭의 컨텐츠 구성을 예상할 수 없을 때 효율적이다. 반면 큰 단점이 있다면, 탭의 개수가 늘어남에 따라 WebView가 점점 늘어나는 DOM Element 수를 감당해야 하는 문제가 생긴다는 것이다. 이런 부분들은 막상 개발할 때에는 문제라고 생각하지 못했다가 이후에 이슈가 될 수 있어 모델을 구상할 때부터 고려할 필요가 있다.

2안. Injection 방식 탭 DOM 구조

다음으로 생각할 수 있는 방법은 위의 2안으로 Tab content Panel을 구성하고 액티브 탭이 변경되면 탭의 상태를 주입(inject)하는 방법이다. 탭의 내용 구성이 크게 변경 되지 않고 미리 예상할 수 있는 구조일 때 적합하다. 정해진 구조 안에 주입하는 방식이므로 첫 번째 방법에 비해 DOM Element의 크기가 많이 커지지 않는다는 장점이 있다.

두 가지 방식 중에서 결국 Injection 방식의 2안을 선택하였다. QueryPie는 Tab content의 구성이 어느 정도 정해져 있고 작업하면서 탭의 개수가 많이 늘어날 수 있다는 점에서 injection 방식이 더 적절하다고 판단했다.

탭 라벨 추가/삭제/정렬

DOM 구성 방식을 정하고 나서는 구체적으로 탭 라벨의 추가, 삭제 및 정렬을 구현하기 시작했다. 여기에서는 탭을 추가 하면 QueryPanelStore에 QueryPanel모델을 생성하고 QueryPanelStore.panels에 추가하게 해주었다.

QueryPanel 생성 시 이름 Untitled-{n} 부여 코드

하지만 말처럼 간단한 문제가 아니었다. VSCode에서처럼 저장되지 않는 QueryPanel을 추가하면 Untitled-{n}이란 이름을 부여하고 싶었다. 이를 위해서 아래와 같은 과정으로 코드를 작성 하였다.

  1. 새로 Panel을 추가하며 동시에 ‘UntitleSeq’ 1을 부여한다.
  2. ‘UntitleSeq’의 저장값과 같은 n 값을 가진 Panel이 삭제 될 때, ‘UntitleSeq’ 값을 1만큼 줄인다.
  3. 새로운 Panel을 추가될 때, UntitleSeq와 같은 n 값을 가진 Panel이 삭제되지 않았으면 UntitleSeq값에 1을 더해준다.

그리고 Panel의 내용이 변경되어 저장이 필요한 상태를 체크해야 했고, 또 Panel의 내용을 사용자 컴퓨터에 파일로 저장하거나 사용자 컴퓨터에 있는 .sql, .txt파일을 열 수 있어야 했다.

이렇듯 예상보다 너무 많은 이슈가 있어서 정리할 필요가 있었다.

📌Tab Panel 관리할 때 고려사항

  • Panel을 추가할 때 저장되지 않은 Panel 숫자 세기
  • Panel의 내용이 변경이 되었는지 알 수 있는 상태 관리
  • Panel 내용 자동 저장
  • 파일 불러오기 (단순 불러오기가 아니라, 파일을 불러올 때 파일 내용 인코딩 타입 체크)
  • Panel이 추가되고 Panel의 개수가 많아 스크롤이 필요해지면 활성화된 탭의 위치로 스크롤
  • Panel을 드래그하여 순서 변경하기
  • Panel을 닫을 때 Panel 내용이 변경되었다면 파일을 저장할 지 사용자에게 물어보기
Query Panel 구현 모습

결국 위에서 언급한 모든 이슈는 해결되었고, 위의 그림은 개발이 완료된 Tab Panel을 캡쳐한 것이다. 이렇게 다 만든 뒤에 스크린 캡쳐를 하면 참 쉽게 느껴진다. 하지만 처음 개발에 착수하고 한땀 한땀 코딩을 할 때를 생각하면 어떻게 이런 결과물을 만들어 냈는지 참 뿌듯하고 행복하다. 이 맛에 개발을 하는 것 같다.

다른 이슈들의 처리 방법은 고수들이 넘쳐나는 인터넷 세상에 수 많은 해결방법들을 찾을 수 있기에 생략하고, 내가 좋아하는 UI 관련 이슈에 대해서 간단히 다루고 넘어가면 좋겠다.

Drag & Drop Tab Panel

React로 웹애플리케이션 만든 경험이 아주 많은 편이 아니기 때문에 Drag & Drop에 대해 막연한 두려움을 가지고 있었다. 기존에 jQuery로 개발을 할 때에는 정말 이벤트를 한땀한땀 입혀야 한다고 생각해서 싫었고, 또 파편화된 브라우저가 정말 싫었다. 그래서 가급적이면 Drag & Drop을 사용하기 보다 Mousedown & Mousemove & Mouseup을 이용했었다.

하지만 이번에는 생각을 처음부터 다시 할 수 있었다. WebView는 Chrome만 지원하면 되는 상황이라 비교적 간단했고, 무엇보다 나의 개발 스킬이 최근 몇 년간 상승되었기 때문이다.

Tab JSX

그래서 막상 코드를 짜고 보니 너무 간단했다. JSX에 draggable 속성을 주고 onDragStart, end, over를 추가하면 된다. over와 start는 드래깅이 시작된 인덱스와 드래깅이 진행중인 인덱스를 파악하기 위해 사용될 뿐이고 실제 가장 복잡한 코드는 dragEnd였다. 가장 복잡했다고는 하지만 이 역시 코드를 보면간단하다.

queryPanelStore에 sortPanel액션을 만들고 드래그 종료 시점에 dragTabIndex와 dragOverTabIndex를 보내면 dragTabIndex를 위치로 삽입시켜주는 코드를 액션 안에 구현하였다.

fromIndex의 아이템을 toIndex로 보내기 위해서는 splice API 이용하는 방법을 사용할 수 있다. slice를 2번해서 합치는 방법도 있지만 splice가 좀 더 깔끔하다고 생각한다.

기존에 array의 순서를 변경하는 코드에서는 movePanel이라는 변수를 만들지 않고 splice가 삭제한 아이템을 return하는 것을 이용하면 한 줄로 코드를 짤 수 있었지만 우리 프로젝트는 mobx & MST를 사용하고 있었기 때문에 2줄의 코드가 더 필요했다.

개체 패널 내에서 Drag & Drop

쿼리 실행 결과 출력하기

QueryPanel 관리에 대한 이슈를 해결하자 더욱 중요한 실행 결과 출력이 남아있었다. 쿼리 실행 결과가 레코드 데이터라면 데이터 그리드로 데이터를 출력해야했다.

아래는 SQLGate에서 멀티 쿼리를 실행했을 때의 모습인데 대부분의 다른 쿼리 실행 툴이 한 번에 한 개의 데이터를 출력하는데 비해 SQLGate는 쿼리 실행 결과를 한 번에 Panel로 쌓아서 보여주는 형태를 강점으로 가지고 있었다.

SQLGate의 쿼리 실행 결과 (가로/세로 보기)

사실 한국인들은 무엇이든 한 눈에 보는걸 좋아한다. 무엇이든… 그래서 QueryPie 역시 개발 전부터 쿼리 실행결과를 여러 개 출력해야 할 때 실행 결과를 위 아래로 쌓거나 좌우로 펼쳐서 볼 수 있게 만들겠다고 마음 먹었다.

하지만 개발자들이 이런 요구를 수용하기 위해 얼마나 많은 고민과 시간을 써야하는지 사용자들은 알지 못할 것 같다. 여기서도 역시 생각지 못한 이슈들이 발생하기 시작했다.

Issue 1. 예상과 다르게 복잡한 데이터 구조 ( 💀💀💀)

쿼리에디터에서 작성한 쿼리문을 API에 전달하는 것까지는 간단했다.

쿼리에디터의 상태가 변경 될 때마다 QueryPanel 모델을 변경해주도록 만들었기 때문에 쿼리를 실행할 때엔 그저 아래의 명령을 실행하는 것으로 모든 일이 끝났다.

queryPanelStore.activePanel.execute();

하지만 돌아오는 결과는 예상을 뛰어넘었다.

QueryPie 백엔드와 미들웨어 개발팀은 쿼리 실행결과를 가져올 때 발생할 수 있는 최악의 상황들을 모두 고려하여 매우 복잡한 형태의 데이터 구조를 나에게 알려주었다.

그래서 쿼리 실행 전에 파싱을 통해 구문을 분석하여 여러 개의 문장으로 만들어 쿼리를 실행하고, 각각의 요청 결과 데이터셋은 병렬화되어 여러 번에 나누어 처리 될 수 있게 만들어주었다.

물론 개발자의 한 사람인 나로서도 잘 짜여진 데이터 구조를 싫어하지 않는다. 하지만 순박한 프론트엔드 개발자로 돌아와 생각하면 왜 나에게 이런 시련을 주는 것인지 이해 할 수 없었다.

Issue 2. Result Panel(쿼리 실행 결과 패널) 크기조정 ( 💀💀💀💀💀)

쿼리 실행 결과는 잘 말아서 표현하면 된다고 생각했지만 각 Result Panel들의 크기를 조정하는 일은 고민이 많았다.

  1. 사용자가 Panel의 크기를 조정하지 않은 상태에서 화면 전체를 리사이즈하거나, 주변 Panel의 크기를 조정하여 결과셋의 크기가 변경되면 크기가 1/n의 크기로 자동조정 되어야 함
  2. 사용자가 여러 개의 Result Panel 중에 한 개의 크기를 조정하면, Result Set의 크기가 변경되어도 사용자가 변경한 Result Panel의 크기는 유지 되어야 함
  3. Result Panel 내부의 컴포넌트들에서 Result Panel의 크기를 알 수 있어야 함 (컴포넌트 안쪽에 데이터 그리드 컴포넌트를 붙여야하기 때문)
  4. Result Set의 마지막에 있는 Result Panel의 크기를 조정하면 스크롤 포지션이 그에 맞도록 따라가야 함

QueryPie를 개발하면서 다른 부분들에 대한 리사이징은 리사이징 할 개체의 너비값이나 높이값을 이용하여 Resizer의 위치를 결정하는 방식으로 개발되었다.

에디터와 결과패널 코드

그렇기 때문에 resizer의 style.top 속성으로 resizerTop의 값을 전달해주면 된다. 이런 방법으로 개발할 때엔 resizer에 마우스 다운 이벤트가 발생 될 때를 감지하여 mouseMove와 mouseDown이벤트를 window나 document에 binding 했다가 resizer moving이 종료되는 시점에 unbinding해주는 방식으로 개발하면 된다.

이 때, 이벤트 binding, unbinding 관리와 마우스의 포지션을 구하는 코드가 지저분해지기 때문에 최근에는 mouseEventSubscribe라는 함수를 만들어 사용하고 있다.

mouseEventSubscribe는 마우스 이동이 일어나면 callBack 함수를 실행하는 간단한 함수인데, mouseEventSubscribe를 사용하는 쪽의 코드가 간결해지는데다 EventSubscribe 시작 전의 위치와 변경 후 위치 비교를 손쉽게 할 수 있는 장점이 있다.

앞서 이야기했듯이 리사이징을 너비나 높이 값으로 처리하는 일은 쉽게 해결이 가능하다. (개발기가 작성된 후에 UI성능을 높이기 위해 mouseEventSubscribe함수 안에서 callback함수 실행을 throttle처리 하였다. 내용 참고)

Result Panel (쿼리 실행 결과 패널) 리사이징

다시 Result Panel의 리사이즈로 돌아와서 보면 Result Panel의 리사이즈는 가로 또는 세로로 쌓여있는 Panel을 리사이즈 해주어야 하기 때문에 Resizer가 절대값으로 표현되면 각각의 Resizer의 위치를 재계산 해주어 하는 문제가 생긴다.

그런 불필요한 코드를 작성하지 않고 변경될 Panel의 크기로 Resizer가 위치를 잡는 것이 편리하다. 그래서 css flex를 이용하여 Panel과 Resizer를 출력하여 위치를 따로 정하지 않도록 하고, Resizer를 이동할 때 Resizer의 위치로부터 상대적 변화를 측정하여 Panel의 크기를 결정하도록 만들었다.

Result Panel (쿼리 실행 결과 패널) 코드

끝으로 Resizer가 이동할 때 Scroll Container 밖으로 나갈 경우 Scroll Container의 스크롤 위치를 조정해주도록 하였다.

Result Panel (쿼리 실행 결과 패널) 리사이징 & 스크롤

마치면서

우여곡절 끝에 두 번째 스프린트가 끝나고 이제 세 번째 스프린트가 진행중이다. QueryPie 팀원 모두가 하나되어 매일 열심히 개발하며 디자인, 기획 등 모든 CHEQUER 크루들 역시 QueryPie를 위해 함께 애쓰고 있다. 그렇기에 나 역시 한 눈 팔 새도 없이 하루하루를 보낸다.

이번에는 리사이즈와 드래그를 주제로 개발기를 썼지만 개발과 동시에 회고를 하느라 좀 더 많은 예제와 참고 자료를 글에 포함하지 못해 아쉽다. 오늘 다루었던 내용들은 QueryPie 개발이 완료되면 좀 더 정리해보고 싶다는 생각을 남기며 이번 개발기를 마친다.

영어- https://medium.com/p/49f62ccbf3c

--

--