현상
Web Speech API는 SpeechRecognitionResult를 비동기로 넘긴다. 에디터는 흔히 인식 시작 시점에 window.getSelection().getRangeAt(0)을 복제해 두고, result.isFinal일 때 insertNode / insertText를 호출한다. 그 사이 사용자가 탭 이동, 툴바 클릭, 다른 필드 포커스를 할 수 있어 저장한 Range가 무효화되거나, 사용자가 말하던 위치를 더 이상 나타내지 않을 수 있다. Chrome은 “이 transcript를 이 contenteditable에 묶는다”는 내장 프리미티브가 없어 정확성은 전부 앱 책임이다. 게다가 스크립트가 DOM을 직접 바꾸면 beforeinput / input이 나지 않을 수 있어, 프레임워크가 평소 파이프라인으로 동기화하지 못한다.
재현 단계
contenteditable영역과 바깥의 포커스 가능한 컨트롤(예:<button>)을 만든다.- 음성 인식 스크립트:
start시 선택이 에디터 안에 있으면 Range를 복제한다. - 최종 결과가 올 때까지 말하거나
onresult를 시뮬레이션한다. - 최종 결과가 오기 전에 바깥 컨트롤을 클릭해
document.activeElement가 에디터가 아니게 한다. onresult에서editor.contains(range.startContainer)나document.activeElement확인 없이 복제한 Range로 transcript를 적용한다.- 콘솔의 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를 완전히 우회해 네이티브 편집 경로를 쓸 수 있어 버그 표면이 다르다.
해결 방법
- 삽입마다 가드 (영문 케이스 본문과 동일한
safeInsert패턴). - 세션 버전: 인식 시작 시
sessionId를 올리고, 에디터blur에서 다시 올리면 이전 결과는 무시한다. - 합성 파이프라인: 프로그래매틱 삽입 후
InputEvent를 디스패치하거나 모델 업데이터를 직접 호출해 단일 진실 경로를 유지한다.