모바일 지원

contenteditable 에디터에서 모바일 기기 지원 가이드입니다.

개요

모바일 기기는 가상 키보드, 터치 상호작용, 다른 IME 동작, 뷰포트 제약으로 인해 고유한 도전과제를 제시합니다. 이 가이드는 모바일 특수 고려사항을 다룹니다.

주요 도전과제:

  • 가상 키보드가 뷰포트 크기 조정
  • 터치 선택이 마우스보다 덜 정확함
  • iOS Safari에서 한국어 IME의 경우 조합 이벤트가 발생하지 않음
  • 텍스트 예측과 자동 수정이 입력을 방해함
  • 다른 키보드 앱이 다른 동작을 함

가상 키보드

가상 키보드 표시 및 뷰포트 변경을 처리합니다.

키보드 감지

class VirtualKeyboardDetector {
  #viewportHeight = window.visualViewport?.height || window.innerHeight;
  #isKeyboardVisible = false;

  constructor(editor: Editor) {
    this.#editor = editor;
    this.#setupDetection();
  }

  #setupDetection() {
    if (!window.visualViewport) {
      // visualViewport가 없는 브라우저를 위한 대체
      window.addEventListener('resize', () => this.#detectKeyboard());
      return;
    }

    window.visualViewport.addEventListener('resize', () => {
      const newHeight = window.visualViewport.height;
      const heightDiff = this.#viewportHeight - newHeight;

      if (heightDiff > 150) {
        // 키보드 표시됨
        this.#onKeyboardShow();
      } else if (heightDiff < -150) {
        // 키보드 숨김
        this.#onKeyboardHide();
      }

      this.#viewportHeight = newHeight;
    });
  }

  #onKeyboardShow() {
    this.#isKeyboardVisible = true;
    this.#scrollToCursor();
    this.#adjustLayout();
  }

  #onKeyboardHide() {
    this.#isKeyboardVisible = false;
    this.#restoreLayout();
  }

  #scrollToCursor() {
    const selection = window.getSelection();
    if (!selection || selection.rangeCount === 0) return;

    const range = selection.getRangeAt(0);
    const rect = range.getBoundingClientRect();
    const viewportHeight = window.visualViewport?.height || window.innerHeight;

    if (rect.bottom > viewportHeight) {
      range.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
    }
  }
}

뷰포트 조정

// 키보드가 나타날 때 에디터 높이 조정
function adjustEditorForKeyboard(editor: HTMLElement) {
  const viewport = window.visualViewport;
  if (!viewport) return;

  const keyboardHeight = window.innerHeight - viewport.height;
  
  if (keyboardHeight > 0) {
    // 키보드가 보임
    editor.style.maxHeight = `${viewport.height - 100}px`;
    editor.style.overflowY = 'auto';
  } else {
    // 키보드가 숨김
    editor.style.maxHeight = '';
    editor.style.overflowY = '';
  }
}

터치 선택

터치 기반 텍스트 선택을 처리합니다.

// 터치 선택 정확도 개선
class TouchSelectionHandler {
  constructor(editor: Editor) {
    this.#editor = editor;
    this.#setupTouchHandlers();
  }

  #setupTouchHandlers() {
    let touchStart: Touch | null = null;

    this.#editor.element.addEventListener('touchstart', (e) => {
      touchStart = e.touches[0];
    });

    this.#editor.element.addEventListener('touchend', (e) => {
      if (!touchStart) return;

      // 선택 영역 업데이트 대기
      setTimeout(() => {
        const selection = window.getSelection();
        if (selection && selection.rangeCount > 0) {
          this.#adjustSelection(selection.getRangeAt(0));
        }
      }, 100);
    });
  }

  #adjustSelection(range: Range) {
    // 필요시 선택 영역 경계 조정
    // 터치 선택은 부정확할 수 있음
  }
}

모바일 입력 처리

모바일 특수 입력 동작을 처리합니다.

텍스트 예측

// 모바일 텍스트 예측 처리
element.addEventListener('beforeinput', (e) => {
  if (e.inputType === 'insertFromPredictiveText') {
    // 모바일 키보드 텍스트 예측
    e.preventDefault();
    this.#handlePredictiveText(e.data);
  }
});

// 코드 블록에 대한 예측 비활성화
function disablePrediction(element: HTMLElement) {
  element.setAttribute('autocorrect', 'off');
  element.setAttribute('autocapitalize', 'off');
  element.setAttribute('spellcheck', 'false');
  element.setAttribute('inputmode', 'text');
}

자동 수정

// 자동 수정 감지 및 처리
class AutocorrectHandler {
  #lastInput = '';

  handleInput(e: InputEvent) {
    if (e.inputType === 'insertText') {
      // 이것이 자동 수정일 수 있는지 확인
      if (this.#isLikelyAutocorrect(e.data)) {
        // 자동 수정 처리
        this.#handleAutocorrect(e.data);
      } else {
        this.#lastInput = e.data;
      }
    }
  }

  #isLikelyAutocorrect(text: string): boolean {
    // 휴리스틱: 텍스트가 입력한 것과 매우 다르면
    // 이를 신뢰할 수 있게 감지하는 것은 어려움
    return false;
  }
}

iOS 특수 문제

iOS Safari는 고유한 동작을 가집니다.

iOS Safari 문제:

  • 한국어 IME에 대해 조합 이벤트가 발생하지 않음
  • 에디터가 포커스를 잃을 때 선택 영역이 손실될 수 있음
  • 가상 키보드 동작이 Android와 다름
// iOS Safari 조합 해결책
function isIOS() {
  return /iPad|iPhone|iPod/.test(navigator.userAgent);
}

function isSafari() {
  return /Safari/.test(navigator.userAgent) && !/Chrome/.test(navigator.userAgent);
}

// iOS Safari에서 특정 키에 대해 항상 기본 동작 허용
if (isIOS() && isSafari()) {
  element.addEventListener('keydown', (e) => {
    if (['Enter', 'Backspace', 'Delete'].includes(e.key)) {
      // 브라우저 기본 동작 허용
      return;
    }
  });
}

Android 특수 문제

Android 브라우저는 고유한 특성을 가집니다.

// Android Chrome 특수 처리
function isAndroid() {
  return /Android/.test(navigator.userAgent);
}

// 다른 키보드 앱 처리
// Gboard, SwiftKey, Samsung Keyboard는 다른 동작을 가짐
function handleAndroidKeyboard() {
  // 일부 키보드는 입력 처리를 방해할 수 있음
  // 다른 키보드 앱으로 테스트
}

모범 사례

  • 에뮬레이터뿐만 아니라 실제 기기에서 테스트
  • 키보드가 나타날 때 뷰포트 변경 처리
  • 커서를 보이게 스크롤
  • 코드 블록에 대한 자동 수정 비활성화
  • 다른 키보드 앱으로 테스트
  • 터치 선택 부정확도 처리
  • 모바일 성능 제약 고려

Related Pages

에디터 아키텍처

에디터 아키텍처 개요

입력 처리 & IME

입력 처리 가이드

디버깅 기법

디버깅 전략