케이스 ce-0294-ios-dictation-duplicate-events-safari-ko · 시나리오 scenario-ios-dictation-duplicate-events

iOS 음성 인식이 텍스트를 단어로 나눠서 입력 이벤트를 재발생시킴

OS: iOS 17.0+ 기기: iPhone 또는 iPad 모든 기기 브라우저: Safari 17.0+ 키보드: 음성 인식 초안
dictationvoice-inputbeforeinputinputiossafariduplicate-eventssync-issue

현상

iOS Safari에서 contenteditable 요소에 음성 인식으로 텍스트를 입력할 때, 시스템이 처음에는 전체 음성 인식 텍스트로 beforeinputinput 이벤트를 발생시킵니다. 초기 입력이 완료된 후, 시스템이 텍스트를 개별 단어로 나눠서 beforeinputinput 이벤트를 다시 발생시켜, 동일한 입력에 대해 이벤트 핸들러가 여러 번 실행됩니다.

재현 예시

  1. iOS Safari(iPhone 또는 iPad)에서 contenteditable 요소가 있는 웹 페이지를 엽니다.
  2. contenteditable 요소에 포커스를 둡니다.
  3. 음성 인식을 활성화합니다(스페이스바 길게 누르기 또는 키보드의 마이크 아이콘 탭).
  4. 텍스트를 음성 인식으로 입력: “만나서 반갑습니다” (또는 여러 단어로 구성된 문구).
  5. 브라우저 콘솔이나 이벤트 로그에서 beforeinputinput 이벤트를 관찰합니다.

관찰된 동작

초기 음성 인식 시퀀스

  1. 사용자가 음성 인식을 활성화하고 “만나서 반갑습니다”라고 말함
  2. beforeinput 이벤트가 다음으로 발생:
    • inputType: 'insertText'
    • data: '만나서 반갑습니다'
    • isComposing: false
  3. input 이벤트가 발생하고 전체 텍스트 “만나서 반갑습니다”가 DOM에 삽입됨

중복 이벤트 시퀀스 (버그)

  1. 짧은 지연 후(일반적으로 100-500ms), beforeinput 이벤트가 다시 발생:
    • inputType: 'insertText'
    • data: '만나서'
    • isComposing: false
  2. input 이벤트가 발생하고 “만나서”가 삽입됨
  3. beforeinput 이벤트가 다시 발생:
    • inputType: 'insertText'
    • data: ' ' (공백 문자)
    • isComposing: false
  4. input 이벤트가 발생하고 공백이 삽입됨
  5. beforeinput 이벤트가 다시 발생:
    • inputType: 'insertText'
    • data: '반갑습니다'
    • isComposing: false
  6. input 이벤트가 발생하고 “반갑습니다”가 삽입됨

주요 특징

  • 음성 인식 중에 composition 이벤트(compositionstart, compositionupdate, compositionend)가 발생하지 않음
  • 모든 이벤트에서 isComposing가 항상 false
  • 초기 입력 완료 후 이벤트가 다시 발생함
  • 텍스트가 단어 경계(공백)에서 분할됨
  • 중복 이벤트 후 DOM 상태는 초기 이벤트 후와 동일함(실제 변경 없음)
  • 이벤트 시퀀스가 DOM 상태와 동기화되지 않음

예상 동작

  • 초기 beforeinputinput 이벤트가 전체 음성 인식 텍스트로 한 번만 발생해야 함
  • 완료 후 이벤트가 다시 발생하지 않아야 함
  • 이벤트가 다시 발생하는 경우, 실제 DOM 변경을 반영해야 함(중복 삽입이 아님)
  • 이벤트 시퀀스가 DOM 상태와 동기화를 유지해야 함
  • 음성 인식 중에 composition 이벤트가 발생해야 함(macOS Safari에서와 같이)

영향

다음과 같은 문제가 발생할 수 있습니다:

  • 중복 처리: 동일한 입력에 대해 이벤트 핸들러가 여러 번 실행됨
  • 상태 동기화 문제: 애플리케이션 상태가 DOM 상태와 일치하지 않을 수 있음
  • 성능 문제: 중복 이벤트의 불필요한 처리
  • 실행 취소/다시 실행 스택 손상: 실행 취소 스택에 중복되거나 잘못된 항목이 포함될 수 있음
  • 검증 문제: 검증 로직이 동일한 입력에 대해 여러 번 실행될 수 있음
  • 포맷팅 문제: 분할된 텍스트로 인해 포맷팅 로직이 잘못 적용될 수 있음
  • 이벤트 시퀀스 혼란: 단일 입력 이벤트를 기대하는 핸들러가 여러 이벤트를 받음

브라우저 비교

  • iOS Safari: 음성 인식 중 composition 이벤트가 발생하지 않음, 완료 후 텍스트를 단어로 나눠서 이벤트가 다시 발생함
  • iOS Chrome: Safari와 동일한 동작(Apple이 요구하는 WebKit 엔진 사용)
  • macOS Safari: 음성 인식 중 composition 이벤트가 발생함, 완료 후 이벤트가 다시 발생하지 않음
  • Chrome/Edge/Firefox (데스크톱): 음성 인식 동작이 다양하지만 일반적으로 더 일관적이며, 중복 재발생 없음

음성 인식 입력 구분

중요: iOS의 웹 애플리케이션에서 음성 인식 입력을 감지하는 신뢰할 수 있는 방법은 없습니다. 웹 API는 음성 인식 감지 기능을 제공하지 않으며, UITextInputContext.isDictationInputExpected와 같은 네이티브 iOS API는 웹 컨텍스트에서 사용할 수 없습니다.

잠재적 지표 (신뢰할 수 없음)

  • composition 이벤트 부재(iOS의 한국어 IME에서도 발생)
  • 여러 단어의 빠른 삽입
  • 텍스트가 분할되어 다시 삽입되는 것처럼 보임
  • 완전한 단어로 빠르게 연속 발생하는 이벤트
  • isComposing가 항상 false(iOS의 한국어 IME에서도 마찬가지)

이러한 지표는 확정적이지 않으며 거짓 양성을 생성할 수 있습니다.

이벤트 발생 순서

“만나서 반갑습니다”를 음성 인식으로 입력할 때 발생하는 이벤트 순서:

1단계: 초기 음성 인식 입력

순서이벤트inputTypedataDOM 상태 (변경 전)DOM 상태 (변경 후)
1beforeinputinsertText’만나서 반갑습니다’""-
2inputinsertText’만나서 반갑습니다’"""만나서 반갑습니다” ✅

2단계: 중복 이벤트 발생 (버그)

초기 입력 완료 후 약 100-500ms 지연 후, 텍스트가 단어별로 나뉘어 이벤트가 다시 발생:

순서이벤트inputTypedataDOM 상태 (변경 전)DOM 상태 (변경 후)
3beforeinputinsertText’만나서‘“만나서 반갑습니다”-
4inputinsertText’만나서‘“만나서 반갑습니다""만나서 반갑습니다” ❌
5beforeinputinsertText’ ‘“만나서 반갑습니다”-
6inputinsertText’ ‘“만나서 반갑습니다""만나서 반갑습니다” ❌
7beforeinputinsertText’반갑습니다‘“만나서 반갑습니다”-
8inputinsertText’반갑습니다‘“만나서 반갑습니다""만나서 반갑습니다” ❌

주요 특징

  • 이벤트 1-2: 전체 텍스트가 한 번에 삽입됨 (DOM 실제 변경)
  • 이벤트 3-8: 텍스트가 단어별로 재발생하지만 DOM은 변경되지 않음
  • Composition 이벤트: 모든 단계에서 compositionstart, compositionupdate, compositionend 이벤트가 발생하지 않음
  • isComposing: 모든 이벤트에서 isComposing: false
  • 이벤트 간 지연: 이벤트 2와 이벤트 3 사이에 100-500ms 지연

전체 이벤트 모니터링

iOS 음성 인식 입력 시 발생하는 모든 이벤트를 모니터링하는 코드:

const element = document.querySelector('[contenteditable]');
const eventLog = [];

const eventsToMonitor = [
  'compositionstart', 'compositionupdate', 'compositionend',
  'beforeinput', 'input',
  'keydown', 'keyup', 'keypress'
];

eventsToMonitor.forEach(eventType => {
  element.addEventListener(eventType, (e) => {
    const eventData = {
      timestamp: Date.now(),
      type: eventType,
      inputType: e.inputType || null,
      data: e.data || null,
      isComposing: e.isComposing || false,
      textContent: element.textContent
    };
    eventLog.push(eventData);
    console.log(`[${eventType}]`, eventData);
  }, { capture: true });
});

발생하는 이벤트 vs 발생하지 않는 이벤트

이벤트 타입발생 여부초기 입력중복 이벤트
beforeinput✅ 발생1회3회
input✅ 발생1회3회
compositionstart❌ 발생 안 함--
compositionupdate❌ 발생 안 함--
compositionend❌ 발생 안 함--
keydown❌ 발생 안 함--
keyup❌ 발생 안 함--
keypress❌ 발생 안 함--

참고사항 및 가능한 해결 방향

이벤트 처리 고려사항

  • 이벤트 핸들러가 동일한 입력에 대해 여러 번 실행될 수 있음
  • 실제 DOM 변경이 없는 이벤트(이벤트 4, 6, 8)는 처리하지 않아야 함
  • textContent를 확인하여 실제 DOM 변경 여부를 판단할 수 있음

Undo/Redo 스택

  • 중복 이벤트를 undo 스택에 기록하면 undo 항목이 중복 생성됨
  • 실제 DOM 변경이 있는 경우에만 undo 스택에 기록해야 함

Voice Control 동시 사용

  • iOS 설정에서 Voice Control과 Dictation을 동시에 활성화하면 텍스트가 실제로 두 번 삽입될 수 있음
  • 이 경우는 DOM이 실제로 변경되므로 textContent 기반 중복 제거로는 감지하지 못함
  • 사용자에게 하나만 활성화하도록 안내 권장

테스트 환경

iOS 버전브라우저언어재현 여부
iOS 16.xSafari한국어✅ 확인됨
iOS 16.xSafari영어✅ 확인됨
iOS 16.xChrome iOS한국어✅ 확인됨
iOS 17.xSafari한국어✅ 확인됨
iOS 17.xSafari영어✅ 확인됨
iOS 17.xChrome iOS한국어✅ 확인됨
iOS 18.xSafari한국어⚠️ 미확인
iOS 18.xSafari영어⚠️ 미확인

참고: 모든 iOS 버전에서 동일한 문제가 발생할 가능성이 높음 (WebKit 엔진 공유). 언어에 관계없이 문제가 발생하는 것으로 보임.

이 시나리오의 변형

케이스 OS 브라우저 상태
ce-0293-ios-dictation-duplicate-events-safari-ko iOS 17.0+ Safari 17.0+ 초안
ce-0294-ios-dictation-duplicate-events-safari-ko iOS 17.0+ Safari 17.0+ 초안

Playground for this case

Use the reported environment as a reference and record what happens in your environment while interacting with the editable area.

Reported environment
OS: iOS 17.0+
Device: iPhone 또는 iPad 모든 기기
Browser: Safari 17.0+
Keyboard: 음성 인식
Your environment
Sample HTML:
Event log
Use this log together with the case description when filing or updating an issue.
0 events
Interact with the editable area to see events here.