현상
IME 조합 중 beforeinput은 inputType: 'insertCompositionText'로 발생하는 반면 해당 input 이벤트는 inputType: 'deleteContentBackward'로 발생할 수 있습니다. 이 불일치는 iOS Safari의 한글 IME뿐만 아니라 다양한 브라우저/IME 조합에서 발생할 수 있으며, Firefox의 특정 IME, 다른 모바일 브라우저 등에서도 관찰됩니다. 이 불일치는 input 이벤트의 inputType만 사용하여 DOM 변경을 올바르게 이해하는 것을 불가능하게 만듭니다.
재현 예시
contenteditable요소에 포커스를 둡니다.- IME를 활성화합니다 (한글, 일본어, 중국어 또는 기타 언어).
- 텍스트 조합을 시작합니다 (예: 한글의 경우 “ㅎ” 그 다음 “ㅏ” 그 다음 “ㄴ”을 입력하여 “한” 조합).
- 조합을 업데이트하기 위해 계속 입력합니다 (예: 한글의 경우 “ㄱ” 그 다음 “ㅡ” 그 다음 “ㄹ”을 입력하여 “한글”로 업데이트).
- 브라우저 콘솔이나 이벤트 로그에서
beforeinput과input이벤트를 관찰합니다. beforeinput.inputType이input.inputType과 일치하는지 확인합니다 - 다를 수 있습니다.
관찰된 동작
조합 텍스트를 업데이트할 때:
-
beforeinput 이벤트:
inputType: 'insertCompositionText'isComposing: truedata: '한글'(새 조합 텍스트)getTargetRanges()는 조합 텍스트가 삽입될 위치를 나타내는 범위를 반환합니다- 범위는 일반적으로 교체될 이전 조합 텍스트를 포함합니다
-
input 이벤트:
inputType: 'deleteContentBackward'(불일치!)data: null또는 비어 있음- 실제 DOM 변경은
beforeinput에서 나타난 삽입이 아닌 삭제일 수 있습니다 - 조합 텍스트가 업데이트되는 대신 삭제될 수 있습니다
-
결과:
inputType에 의존하여 무슨 일이 일어났는지 결정하는 핸들러가 변경을 잘못 해석합니다beforeinput의targetRanges가 손실되고input에서 사용할 수 없습니다- 애플리케이션 상태가 DOM 상태와 일관되지 않을 수 있습니다
예상 동작
input이벤트의inputType이beforeinput이벤트의inputType과 일치해야 합니다beforeinput이insertCompositionText로 발생하면input도insertCompositionText를 가져야 합니다input.data가beforeinput.data와 일치해야 합니다 (또는 실제 커밋된 텍스트를 반영해야 함)- DOM 변경이
beforeinput에서 나타난 것과 일치해야 합니다
영향
이것은 다음을 일으킬 수 있습니다:
- 잘못된 DOM 변경 감지: 핸들러가 삽입이 발생했다고 생각하지만 실제로는 삭제가 발생했습니다
- 손실된 targetRanges 컨텍스트:
beforeinput의targetRanges는 중요하지만input에서 사용할 수 없습니다 - 잘못된 실행 취소/다시 실행: 실행 취소/다시 실행 스택이 잘못된 작업 유형을 기록합니다
- 상태 동기화 문제: 애플리케이션 상태가 일관되지 않게 됩니다
- 이벤트 핸들러 실패: 일치하는
inputType값을 기대하는 핸들러가 실패합니다
브라우저 비교
- iOS Safari:
beforeinput에서insertCompositionText를 발생시키지만input에서deleteContentBackward를 발생시킬 수 있음, 특히 한글 및 일본어 IME에서 자주 발생 - macOS Safari: 특정 IME 조합에서 유사한 불일치를 보일 수 있음
- Firefox: 특정 IME 시나리오에서 불일치를 가질 수 있음, 특히 모바일 기기에서
- Chrome/Edge: 일반적으로 이벤트 간 일관된
inputType이지만 엣지 케이스가 있을 수 있음 - Android Chrome: 텍스트 예측 및 IME 변형으로 인해 불일치 가능성이 더 높음
- 모바일 브라우저: 다양한 IME에서 일반적으로 불일치 가능성이 더 높음
참고 및 해결 방법 가능한 방향
-
beforeinput에서 targetRanges 저장:
input핸들러에서 사용하기 위해targetRanges를 저장합니다:let lastBeforeInputTargetRanges = null; let lastBeforeInputType = null; element.addEventListener('beforeinput', (e) => { lastBeforeInputTargetRanges = e.getTargetRanges?.() || []; lastBeforeInputType = e.inputType; }); element.addEventListener('input', (e) => { if (lastBeforeInputType && e.inputType !== lastBeforeInputType) { // 불일치 감지 - targetRanges를 사용하여 실제 변경 이해 if (lastBeforeInputTargetRanges && lastBeforeInputTargetRanges.length > 0) { // inputType이 아닌 targetRanges를 기반으로 처리 handleActualChange(lastBeforeInputTargetRanges, e); } } lastBeforeInputTargetRanges = null; lastBeforeInputType = null; }); -
DOM 상태 비교: 불일치가 발생하면 실제 변경을 이해하기 위해 이전과 이후 DOM을 비교합니다:
let domBefore = null; element.addEventListener('beforeinput', (e) => { domBefore = element.innerHTML; }); element.addEventListener('input', (e) => { const domAfter = element.innerHTML; if (lastBeforeInputType && e.inputType !== lastBeforeInputType) { // domBefore와 domAfter를 비교하여 실제 변경 이해 const actualChange = compareDOM(domBefore, domAfter); handleChange(actualChange); } domBefore = null; }); -
inputType에만 의존하지 않기: 조합 이벤트를 처리할 때 항상 DOM 검사로 확인합니다
-
우아하게 처리:
inputType일치에 의존하지 않는 폴백 로직을 가집니다