Flutter Web 마우스 클릭으로 스크롤이 되는 괴이한 UX

나용수·2021년 2월 16일
1

Syntax Highlighting이 되는 Code Editor 구현하기(with TextField) 같이 web상에서 TextField를 다뤄야 할 일이 가끔 있다.

근데 Flutter라는 프레임워크는 본래 Mobile Platform을 타겟팅하고 개발되었기에, 모든 컨트롤이 'Touch'에 기반한다. 결국 Scrollable한 모든 위젯의 스크롤 제어는 터치에 의해서 되는 것이다.

그리고 Flutter web이라는 것은 정확하게 Web Support for Flutter 즉, 웹에서 돌아가게끔만 개발한 것이기에, ListView나 TextField 및 SingleChildScrollView로 감싸는 모든 위젯들의 스크롤을 마우스 드래그로 하게 되는 끔찍한 UX를 맞이하게 된다.

물론 마우스 wheel로도 스크롤이 된다. 그래서 일반적인 위젯들에서는 크게 문제가 없다. TextField를 제외하고.

TextField 위젯의 특징

TextField는 텍스트를 Edit할 수 있는 위젯이고, 그렇다면 드래그를 통한 Selection이 되어야 한다.

근데 TextField 위젯을 만들 때 multiLine 프로퍼티에 null값을 줘버리거나, 특정한 값을 주고 parent 위젯(가령 Container)의 height를 작게 줄 경우 TextField 위젯은 Vertical하게 스크롤 가능한 위젯이 된다.

휠과 드래그로 스크롤이 되는 TextField

여기서 드는 의문점은, text selection도 cursor dragging을 통한 것인데, 어떻게 스크롤과 text select가 동시에 가능하냐는 것이다.

스크롤과 select가 모두 되는 TextField

플러터 팀에서는 이를 어떻게 구현했는가하고 프레임워크 소스코드를 직접 뜯어보았다.

text_selection.dart를 flutter 패키지 안에서 잘 찾아서 보면, TextSelection을 하게 되면 거기에 파란색 배경색을 넣어주는 놈인 TextSelectionOverlay 클래스라던가, 여러가지가 있다.

그중에 딱 지금 찾고자 하는 이름의 클래스가 있다. TextSelectionGestureDetector.

일부 코드가 아래와 같다.

/// A gesture detector to respond to non-exclusive event chains for a text field.
///
/// An ordinary [GestureDetector] configured to handle events like tap and
/// double tap will only recognize one or the other. This widget detects both:
/// first the tap and then, if another tap down occurs within a time limit, the
/// double tap.
///
/// See also:
///
///  * [TextField], a Material text field which uses this gesture detector.
///  * [CupertinoTextField], a Cupertino text field which uses this gesture
///    detector.
class TextSelectionGestureDetector extends StatefulWidget {
  ...
  
  State<StatefulWidget> createState() => _TextSelectionGestureDetectorState();
}

class _TextSelectionGestureDetectorState extends State<TextSelectionGestureDetector> {
  ...
  
  Widget build(BuildContext context) {
    ...
    if (widget.onDragSelectionStart != null ||
        widget.onDragSelectionUpdate != null ||
        widget.onDragSelectionEnd != null) {
      // TODO(mdebbar): Support dragging in any direction (for multiline text).
      // https://github.com/flutter/flutter/issues/28676
      gestures[HorizontalDragGestureRecognizer] = GestureRecognizerFactoryWithHandlers<HorizontalDragGestureRecognizer>(
        () => HorizontalDragGestureRecognizer(debugOwner: this, kind: PointerDeviceKind.mouse),
        (HorizontalDragGestureRecognizer instance) {
          instance
            // Text selection should start from the position of the first pointer
            // down event.
            ..dragStartBehavior = DragStartBehavior.down
            ..onStart = _handleDragStart
            ..onUpdate = _handleDragUpdate
            ..onEnd = _handleDragEnd;
        },
      );
    }
		...
    return RawGestureDetector(
      gestures: gestures,
      excludeFromSemantics: true,
      behavior: widget.behavior,
      child: widget.child,
    );
  }
}

보다시피 Horizontal한 drag gesture에 반응한다. (text_selection.dart파일에 'vertical'이라는 키워드는 검색결과가 0개이다.)

그리고 아마 Scrollable한 위젯들의 계보를 타고 가다 보면.. 결국 Scroll Axis를 정하게 되어 있기에, 아마 TextField의 스크롤은 Axis.Vertical으로 설정되어 있을 것이고, Horizontal drag는 text selection으로 인식하는 것이다.

Scrollable 위젯의 제어

짧게 결론만 말하겠다. 스크롤 위젯에 NeverScrollableScrollPhysics를 넣으면 마우스 클릭으로 제어가 안된다. 당연이 휠로도 안된다.

그걸 Listener위젯 같은 걸 써서 ScrollController.jumpTo를 통해 휠로 제어하는 게 가능하다.

Listener(
  onPointerSignal: (PointerSignalEvent event) {
    if (event is PointerScrollEvent) {
      double current = _scrollController.position.pixels;
      double delta = event.scrollDelta.dy;
      _scrollController.jumpTo(current + delta);
      }
    }
  },
  child: myScrollableWidget,
)

위 코드는 단순한 거고, 저대로 하면 스크롤이 좀 맛이 간 것 처럼 극단적으로 된다. atEdge같은 조건으로 제어를 해줘야 한다.

아무튼 일반적인 Scroll위젯은 이제 됐다. 이러면 휠로 스크롤할 수 있고 드래그로는 안된다.

문제는 역시 TextField다. TextField가 multiLine속성에 의해 scrollable해지면, 개행이 될 때(엔터를 칠 때) 자동으로 한 줄 만큼 스크롤이 된다. 그게 자연스러운 UX의 text field일 것이다.

근데 NeverScrollabeScrollPhysics를 쓰면 개행했을 때 스크롤이 되지 않는다. 그래서 위 코드를 적용하면 텍스트는 계속 개행되어 아래로 내려가는데 화면은 전혀 스크롤되고 있지 않아 내가 뭘 쓰고 있는지 알 수가 없어진다. 내가 개행할 때마다 마우스 휠로 내려줘야 한다.

그래서 결론은,,,

좋은 방법은 없을까? 오랜 구글링과 삽질을 해봤는데 이건 그냥 답이 없다. 어쩌다가 한 번 휠로만 스크롤이 되면서 개행하면 자동으로 스크롤이 되도록 만들었는데 아마 되게 불온전하고 안정적이지 않은 방식이었던 걸로 기억한다. 실제로 구동하는 모습을 보면 스크롤이 맛탱이가 나가버려서 버그인가 싶을 정도였다.

위 위 코드를 보면 주석으로 TODO가 있는데, 그 아래 깃허브 링크 이슈를 타고 가보라. 그 안에는 또 #71322번 이슈로 가는 링크가 있다. 내가 몇 시간 동안 구글링하면서 이 두 이슈 페이지를 정말 많이 방문했고, 외에도 많은 이슈 페이지를 돌아다녔니면서 얻은 결론은 하나다. 아직 구현이 안됐다...

이슈 페이지를 한참 돌아다니다가 text_selection.dart의 "// TODO"를 보고 오열했다. 그냥 안되는 거다...

뭐... 프레임워크 아주 밑단에서부터 상속받아 개조하면 가능은 하겠지만.. 아주 피곤할 터이다.

Web Support for Flutter는 아직까지 안정적으로 포팅하는 데에도 급급하다. UX까지 신경 쓸 겨를이 없는 것 같다. 웹 및 데스크탑에서 일반적인 키보드/마우스 UX가 기본적으로 안돼있다.

많은 사람들이 최근에 Flutter를 보고 예찬을 하지만, 그리고 나도 Flutter를 잘 쓰고 있긴 하지만, 글쎄...
솔직히 만들기 쉬운 건 참 좋은데, 스크린샷 같은 모바일 네이티브 기능이라던지, 몇몇 기능들은 네이티브에 비해 조금 어려운 수준이 아니라 그냥 불가능하거나 심각하게 귀찮은 경우가 많다. Web이야 말할 것도 없고... dart 언어 자체도 Flutter 없으면 사지절단 당하는 수준으로 비전이 있어 보이지도 않고.

지금 하는 프로젝트가 3개쯤 되고, 다음 프로젝트를 기획은 어느정도 했고 디자인 시작하고 있는데, 지금 이 포스팅을 쓰기까지의 과정을 거치면서 Flutter로 그 프로젝트를 하려던 계획을 js로 하기로 바꿨다. 이참에 React.js를 배워서 계속 그거 써먹어야지 싶다.

그래도 모바일 앱은 계속 Flutter로 만들지 않을까 싶다. 뭐.. 모바일은 상당히 준수하다.

profile
컴퓨터에 관심 많은 산업공학과 학부생. 슬프게도 지금은 대한민국 육군에서 복무 중입니다.

0개의 댓글