[html] Composition Event (IME 문제)

alirz-pixel·2025년 7월 10일
0

문제 배경

가끔 사이트 이용 중에 한국어 입력 시, 마지막 글자가 2번 입력이 되는 모습을 본 적이 있을 것이다.
(아래 짤은 한국어 입력 후 Enter를 눌렀을 때, 마지막 글자가 한번 더 입력되는 모습)

해결법

해당 문제는 조합 중인 글자에서 발생하는 IME 문제이므로 compositionstart (조합시작), compositionend (조합 완성) 이벤트를 이용하여 '글자 조합 확정 이벤트'를 예외처리 해주면 된다.

IME에 대해 좀 더 알아보고 싶다면, 아래의 코드를 실행하여 영어로도 입력해보길 추천
(영어는 조합형 글자가 아니기 때문에 composition evnet가 발생하지 않음)

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <title>IME 이벤트 디버깅</title>
    <style>
        body {
            font-family: sans-serif;
            padding: 20px;
        }

        #input {
            font-size: 1.2em;
            padding: 8px 16px;
            width: 300px;
        }

        #stack {
            margin-top: 20px;
            border: 1px solid #ccc;
            padding: 10px;
            overflow-y: auto;
            background-color: #f9f9f9;
            font-family: monospace;
        }

        .event-line {
            padding: 4px;
            border-bottom: 1px dotted #ddd;
        }

        #list {
            margin-top: 30px;
            padding-left: 20px;
        }

        #list li {
            font-size: 1.1em;
            margin-bottom: 4px;
        }
    </style>
</head>
<body>

<h2>IME 이벤트 디버깅</h2>

<input type="text" id="input"/>
<div id="stack"></div>

<h3>📋 Todo List</h3>
<ul id="list"></ul>

<script>
    const input = document.getElementById("input");
    const stack = document.getElementById("stack");
    const list = document.getElementById("list");

    function pushToStack(message) {
        const time = new Date().toLocaleTimeString();
        const div = document.createElement("div");
        div.className = "event-line";
        div.textContent = `[${time}] ${message}`;
        stack.prepend(div); // 최신 이벤트를 위로 쌓기
        console.log(`[${time}] ${message}`);
    }

    function addTodo() {
        const value = input.value;
        const li = document.createElement("li");
        li.textContent = value;
        list.appendChild(li);
        input.value = "";
        pushToStack(`[addTodo] '${value}' 추가됨`);
    }

    let isComposing = false;
    input.addEventListener("compositionstart", () => {
        isComposing = true;
        pushToStack("compositionstart");
    });

    input.addEventListener("compositionend", (e) => {
        isComposing = false;
        pushToStack(`compositionend → data: '${e.data}'`);
    });

    input.addEventListener("compositionupdate", (e) => {
        pushToStack(`compositionupdate → data: '${e.data}'`);
    })


    input.addEventListener("keydown", (e) => {
        if (e.key === "Enter") {
            if (isComposing === true) {
                pushToStack(`OS가 조합 중인 글자를 확정 짓는 이벤트는 예외처리: isComposing = ${isComposing}`);
                return;
            }
            pushToStack(`조합 완료된 글자 (${input.value})만 Todo에 반영: isComposing = ${isComposing}`);
            addTodo();
        }
    });
</script>
</body>
</html>

문제 원인 자세히 살펴보기

IME 문제

한글 (정확히는 조합형 글자)을 입력하고 Enter 키를 누르면 마지막 글자가 한 번 더 입력되는 문제가 발생한다.

Enter 키로 입력을 완료하는 기능 구현 시, 글자 조합 과정과 맞물려 2가지의 이벤트가 수행된다.
1. Keydown 이벤트: js로 등록해둔 keydown 이벤트가 수행된다.
2. 글자 조합 확정 이벤트: IME는 확정된 문자열을 커밋(commit)한다.

"사용자가 입력한 문자열"과 "확정된 문자열 커밋"이라는 두 이벤트로 인해 위 예시처럼 (한국어) + (어)가 발생하게 된 것이다.

조합 중인 글자?

한국어는 자모 (字母)를 조합해서 하나의 글자로 만드는 "조합형 글자"이다.

단계입력자모 구성조합 중 글자 상태IME 이벤트
1ㅎ 입력초성: ㅇㅎ (조합 시작)compositionstart + update
2ㅏ 입력중성: ㅏ하 (ㅎ + ㅏ)compositionupdate
3ㄴ 입력종성: ㄴ한 (하 + ㄴ)compositionupdate
4-1ㄱ 입력초성: ㄱ한 (조합 완성)compositionend
4-2-----초성: ㄱㄱ (조합 시작)compositionstart + update

조합 중인 글자는 사용자의 키 입력 순서대로 compositionstart -> compositionupdate -> compositionend 순으로 이벤트가 발생한다.

그리고 조합된 글자는 일반적으로 텍스트 커서(Text Cursor)가 다음 글자로 넘어가는 순간이라고 이해하면 된다 (위 표에서 4-1,4-2). 위 상황 외에도 Enter키를 누르면 OS가 내부적으로 해당 문자를 확정 짓는다 (이로인해 IME 문제가 발생하게 된다).

실제 웹에서 확인해보기

reference

  • MDN Web Docs: CompositionEvent 설명 (Link)
  • 중국어 IME 조합: 조합 과정에서 onChange가 발생하는 문제 (Link)

0개의 댓글