케이스 ce-0585-chrome-web-speech-api-contenteditable-insertion-ko · 시나리오 scenario-voice-input-contenteditable

Web Speech API 최종 결과가 인식 중 선택이 바뀌면 잘못된 Range에 삽입될 수 있음

OS: Windows 10+ 기기: Desktop Any 브라우저: Chrome 120+ 키보드: US QWERTY (음성 입력용 마이크) 초안
chromeweb-speech-apivoiceselectionasynccontenteditable

현상

Web Speech API는 SpeechRecognitionResult를 비동기로 넘긴다. 에디터는 흔히 인식 시작 시점에 window.getSelection().getRangeAt(0)을 복제해 두고, result.isFinal일 때 insertNode / insertText를 호출한다. 그 사이 사용자가 탭 이동, 툴바 클릭, 다른 필드 포커스를 할 수 있어 저장한 Range가 무효화되거나, 사용자가 말하던 위치를 더 이상 나타내지 않을 수 있다. Chrome은 “이 transcript를 이 contenteditable에 묶는다”는 내장 프리미티브가 없어 정확성은 전부 앱 책임이다. 게다가 스크립트가 DOM을 직접 바꾸면 beforeinput / input이 나지 않을 수 있어, 프레임워크가 평소 파이프라인으로 동기화하지 못한다.

재현 단계

  1. contenteditable 영역과 바깥의 포커스 가능한 컨트롤(예: <button>)을 만든다.
  2. 음성 인식 스크립트: start 시 선택이 에디터 안에 있으면 Range를 복제한다.
  3. 최종 결과가 올 때까지 말하거나 onresult를 시뮬레이션한다.
  4. 최종 결과가 오기 전에 바깥 컨트롤을 클릭해 document.activeElement가 에디터가 아니게 한다.
  5. onresult에서 editor.contains(range.startContainer)나 document.activeElement 확인 없이 복제한 Range로 transcript를 적용한다.
  6. 콘솔의 DOM 오류, 예기치 않은 위치 삽입, 폴백에 따른 무반응 등을 관찰한다.

관찰된 동작

  • 오래된 Range: 경계 노드가 리렌더로 제거되면 Range 조작이 InvalidNodeTypeError를 내거나 무반응일 수 있다.
  • 잘못된 activeElement: getSelection()을 다시 읽는 폴백은 포커스가 옮겨진 위치 기준으로 삽입한다.
  • input 이벤트 부재: 스크립트 돌 변이는 키보드 beforeinput/input과 같지 않아 input에서만 모델을 맞추는 리스너가 돌지 않는다.
  • composition 없음: IME와 달리 Speech API는 composition*을 내지 않는다. compositionend에만 동기화하는 에디터는 갱신을 놓친다.

예상 동작

에디터에 포커스가 없고, 선택이 인식 시작 시점과 같은 편집 루트에 있다고 확신할 수 없으면 애플리케이션은 오래된 Range에 transcript를 적용하면 안 된다. 취소하거나 사용자가 에디터를 다시 포커스할 때까지 큐에 두거나, 안내해야 한다. 의도적인 프로그래매틱 삽입은 키보드 입력과 동일한 상태 갱신 경로(커스텀 이벤트나 명시적 모델 패치)를 타야 한다.

영향

  • 문서 손상: 텍스트가 잘못된 블록이나 래퍼 밖에 생긴다.
  • 프레임워크 불일치: 가상 DOM은 발생하지 않은 이벤트를 가정한다.
  • 접근성: 발화 중 툴바·다이얼로그로 포커스가 옮겨지면 음성 사용자 데이터가 조용히 잘못 들어갈 수 있다.

브라우저 비교

  • Chrome / Edge: Speech Recognition API 사용 가능. 비동기 타이밍 이슈는 통합 수준에서 매우 흔하다.
  • Safari / Firefox: API 가용성·동작이 다르다. Chrome과 동일하다고 가정하면 안 된다.
  • OS 디테이션: Speech API를 완전히 우회해 네이티브 편집 경로를 쓸 수 있어 버그 표면이 다르다.

해결 방법

  1. 삽입마다 가드 (영문 케이스 본문과 동일한 safeInsert 패턴).
  2. 세션 버전: 인식 시작 시 sessionId를 올리고, 에디터 blur에서 다시 올리면 이전 결과는 무시한다.
  3. 합성 파이프라인: 프로그래매틱 삽입 후 InputEvent를 디스패치하거나 모델 업데이터를 직접 호출해 단일 진실 경로를 유지한다.

참고

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: Windows 10+
Device: Desktop Any
Browser: Chrome 120+
Keyboard: US QWERTY (음성 입력용 마이크)
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.