Skip to content

composition 이벤트 시나리오별 처리 규칙

compositionstart/update/end가 예상대로 오지 않을 때 처리하는 방법

이 문서는 이벤트 시나리오별로 에디터가 어떻게 처리해야 하는지를 정의한다. 브라우저별 구체적인 동작 차이는 브라우저·플랫폼별 IME 동작 차이를 참고한다.


1. 정상 시나리오: composition 3종이 모두 발생

Section titled “1. 정상 시나리오: composition 3종이 모두 발생”
compositionstart (data: "")
compositionupdate (data: "ㄱ")
compositionupdate (data: "가")
compositionend (data: "가")

처리:

  1. compositionstartisComposing = true
  2. compositionupdatepreedit = e.data (화면에만 표시, 문서에 반영 안 함)
  3. compositionendisComposing = false, commit(e.data) (문서에 반영)

2. compositionstart 없이 compositionend만 오는 경우

Section titled “2. compositionstart 없이 compositionend만 오는 경우”

발생 조건: 레거시 IME, 상태가 꼬인 경우

처리:

el.addEventListener('compositionend', (e) => {
// compositionstart가 없었어도 data가 있으면 commit
if (e.data) {
commit(e.data);
}
isComposing = false;
});

규칙:

  • compositionend.data가 비어있지 않으면 → 커서 위치에 삽입
  • compositionend.data가 빈 문자열이면 → 무시 (취소로 간주)

발생 조건: 일부 IME에서 조합 없이 바로 확정

compositionstart (data: "")
compositionend (data: "가")

처리:

  • preedit 구간이 비어 있었던 것으로 처리
  • compositionend.data를 그대로 commit

4. composition 없이 insertText만 오는 경우

Section titled “4. composition 없이 insertText만 오는 경우”

발생 조건:

  • 데드 키 입력 (é 등)
  • 아랍어 IME (단순 키→문자 매핑)
  • iOS Safari 딕테이션
  • iOS Safari 한글 (불규칙하게 발생)

처리:

el.addEventListener('input', (e) => {
if (!isComposing && e.inputType === 'insertText') {
commit(e.data);
}
});

규칙:

  • compositionstart가 한 번도 발생하지 않았으면 → “조합 중”이 아님
  • insertTextdata를 바로 문서에 반영

5. compositionend.data가 긴 문자열인 경우

Section titled “5. compositionend.data가 긴 문자열인 경우”

발생 조건:

  • 중국어/일본어: 후보에서 문절·문장 단위 선택
  • 일본어 再変換 (재변환)

처리:

el.addEventListener('compositionend', (e) => {
// 길이 제한 없이 그대로 commit
commit(e.data); // "東京都渋谷区" 같은 긴 문자열 가능
});

규칙:

  • compositionend.data가 한 글자라고 가정하지 않는다
  • 전체 문자열을 하나의 undo 단위로 처리

6. compositionend.data가 빈 문자열인 경우 (취소)

Section titled “6. compositionend.data가 빈 문자열인 경우 (취소)”

발생 조건:

  • 사용자가 Esc 누름
  • IME가 조합 취소

처리:

el.addEventListener('compositionend', (e) => {
if (e.data === '') {
// 취소: preedit 구간만 제거
removePreedit();
} else {
commit(e.data);
}
isComposing = false;
});

7. compositionend 직후 input이 중복 발생하는 경우

Section titled “7. compositionend 직후 input이 중복 발생하는 경우”

발생 조건: 일부 브라우저에서 compositionend 후 같은 내용의 input이 한 번 더 옴

compositionend (data: "가")
input (inputType: insertText, data: "가") // 중복

처리:

let lastCommit = null;
let lastCommitTime = 0;
el.addEventListener('compositionend', (e) => {
lastCommit = e.data;
lastCommitTime = Date.now();
commit(e.data);
isComposing = false;
});
el.addEventListener('input', (e) => {
// compositionend 직후 100ms 이내에 같은 내용이 오면 무시
if (Date.now() - lastCommitTime < 100 && e.data === lastCommit) {
return;
}
if (!isComposing && e.inputType === 'insertText') {
commit(e.data);
}
});

8. 삭제 inputType만 오는 경우 (composition 없이)

Section titled “8. 삭제 inputType만 오는 경우 (composition 없이)”

발생 조건: iOS Safari에서 한글 삭제 시

처리:

el.addEventListener('beforeinput', (e) => {
switch (e.inputType) {
case 'deleteContentBackward':
// 커서 앞 1글자 삭제
deleteBackward(1);
break;
case 'deleteWordBackward':
case 'deleteBackwardWord':
// 커서 앞 단어 삭제
const range = e.getTargetRanges?.()?.[0];
if (range) {
deleteRange(range);
} else {
deleteWordBackward();
}
break;
}
});

9. blur 시 compositionend가 오지 않는 경우

Section titled “9. blur 시 compositionend가 오지 않는 경우”

발생 조건: Safari (macOS)

처리:

el.addEventListener('blur', () => {
if (isComposing) {
// Safari: compositionend가 안 옴
// 정책 선택:
// A) preedit 버림: removePreedit();
// B) preedit commit: commit(preeditText);
isComposing = false;
preeditText = '';
}
});

우선순위조건처리
1compositionend 발생e.data가 있으면 commit, 없으면 취소
2compositionupdate 발생preedit 갱신 (문서에 반영 안 함)
3composition 없이 insertText바로 commit
4composition 없이 delete*해당 범위 삭제
5blur 발생 중 isComposing강제 정리