vue

vue v-model 한글 입력 문제: IME 조합 문자 처리 매커니즘

소이소 2025. 5. 30. 11:38

서론

input이나 textarea와 같이 텍스트 입력을 처리하는 HTML 요소로 한글을 입력할 때 원하는 대로 동작하지 않는 상황이 있습니다.
 
아래 vue로 작성된 예시를 보면 한글 입력시 문제가 발생합니다.
"ㅇ", "아", "안" 입력 중에는 `length`가 0이고, "안"이 완성되어야 `length`가 1이 됩니다.

 
영어를 입력하면 문제없이 입력된 글자수에 따라 length가 늘어나는 것을 볼 수 있습니다.

왜 한글 입력시 'ㅇ' 만 입력해도 length가 1이 안되는 걸까요?
 
이번 글에서는 vue에서 IME(Input Method Editor) 조합 문자를 처리 방식을 분석하고, 한글 입력문제를 해결하기 위한 방안을 알아보겠습니다.

본론

먼저 이 문제가 vue의 문제인지, 아니면 브라우저에서 한글 입력 시 발생하는 기본적인 현상인지 확인해보겠습니다.
 
바닐라 JavaScript로 동일한 기능을 구현해보면 JavaScript의 input 이벤트 실시간으로 값을 갱신합니다.
'ㅇ'를 입력하자마자 length가 1로 증가합니다.

const input = document.querySelector('input');

input.addEventListener('input', (e) => {
  console.log('input 이벤트:', e.target.value, 'length:', e.target.value.length);
});

input.addEventListener('compositionstart', () => {
  console.log('--조합 시작');
});

input.addEventListener('compositionupdate', (e) => {
  console.log('조합 중:', e.data);
});

input.addEventListener('compositionend', (e) => {
  console.log('조합 완료:', e.data);
});

출력 예시:

--조합 시작
input 이벤트: ㅇ length: 1
조합 중: ㅇ
input 이벤트: ㅏ length: 1
조합 중: ㅏ
input 이벤트: ㄴ length: 1
조합 중: ㄴ
input 이벤트: 안 length: 1
조합 완료: 안

문제 : vue v-model 사용시 글자 조합 완료시에만 length가 증가

위 예시는 다음과 같은 vue 코드로 작성되었습니다. 
 
vue의 v-model은 폼 입력 요소와 애플리케이션 상태 간의 양방향 데이터 바인딩을 제공하는 디렉티브입니다.
v-model="value"를 통해 input의 값을 실시간으로 추적하여 사용자 입력값을 자동으로 컴포넌트 데이터와 동기화 합니다. 

<div class="dialog-content">
  <input
    v-model="value
    class="input"
    placeholder="텍스트를 작성해주세요"
    autofocus />
</div>

 

원인 : vue v-model의 IME 조합 문자 처리 메커니즘

그렇다면 vue에서는 왜 이런 현상이 발생할까요?
 
핵심은 vue의 v-model이 IME(Input Method Editor) 조합 문자를 처리하는 방식에 있습니다.
먼저 조합형 문자가 무엇인지 알아보고, vue가 어떻게 이를 처리하는지 살펴보겠습니다.

조합형 문자

조합형 문자는 여러 키 ( ㅇ, ㅏ, ㄴ ) 입력을 조합하여 하나의 글자( 안 )를 만드는 방식으로 한글, 일본어, 중국어와 같은 문자가 있습니다.

// 한국어 입력
// ㅇ → ㅏ → ㄴ → 안

// 일본어 입력 "こんにちは" (konnichiha → こんにちは)
// k → o → n → n → i → c → h → i → h → a → 変換 → こんにちは

// 중국어 입력 "你好" (nihao → 你好)  
// n → i → h → a → o → 선택 → 你好

 
vue의 v-model은 내부적으로 composition 이벤트들을 자동으로 처리합니다. 사용자가 명시적으로 @compositionstart 등을 추가하지 않아도 Vue가 알아서 IME 처리를 해줍니다. 
 
v-model 코드

core/packages/runtime-dom/src/directives/vModel.ts at main · vuejs/core

🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web. - vuejs/core

github.com

 
vue github에 공개되어있는 v-model 코드를 간략하게 요약해보았습니다.
해당 동작 방식을 서술해보자면 다음과 같습니다.

export const vModelText = {
  created(el, { modifiers }, vnode) {
    addEventListener(el, 'input', (e) => {
      if ((e.target as any).composing) return; 
      el[assignKey](el.value);
    });
    
    // Vue가 자체적으로 composing 상태 관리
    addEventListener(el, 'compositionstart', (e) => {
      (e.target as any).composing = true;
    });
    
    addEventListener(el, 'compositionend', (e) => {
      (e.target as any).composing = false;
      e.target.dispatchEvent(new Event('input'));
    });
  }
};

 
element에 Input Method Editor (IME) 입력이 발생하면 컴포지션(composition) 이벤트가 발생합니다.

IME(Input Method Editor) 조합 규칙

키보드 입력 방식과 언어 문자를 매핑하는 규칙입니다. 
한글의 경우, 주로 자음과 모음을 조합하여 한 글자를 만드는 과정을 말합니다. 

 
composition 이벤트의 발생 순서는 다음과 같습니다:

  1. compositionstart: 글자 조합 세션이 시작될 때 (IME 입력 시작)
  2. compositionupdate: 글자 조합 중 값이 변경될 때
  3. compositionend: 글자 조합이 완료되거나 취소될 때 (IME 입력 완료)

Vue의 v-model은 각 composition 이벤트 발생 시 실행되는 콜백 함수를 등록해둡니다.
동작 방식은 다음과 같습니다:

  • compositionstart 이벤트 발생 시, onCompositionStart() 함수가 호출되어 target.composing 값을 true로 설정합니다.
  • compositionend 이벤트 발생 시, onCompositionEnd() 함수가 실행되어 target.composing 값을 false로 변경하고, dispatchEvent(new Event('input')) 로 input 이벤트를 강제로 발생시킵니다.

즉, v-model은 IME 조합 중인 상태(composing이 true인 상태)에서는 input 이벤트 처리를 중단하며, 조합이 완료된 후(compositionend)에만 input 이벤트를 발생시켜 데이터 상태를 업데이트합니다.
따라서, 조합 문자를 입력하는 경우, 조합이 완료되기 전까지는 if ((e.target as any).composing) return 조건문이 실행되므로, length 값이 갱신되지 않습니다.
v-model은 IME 입력 중인 데이터를 임시 입력 상태로 간주하며, 완성된 입력(compositionend)만을 최종 데이터로 반영하는 구조입니다.

해결 방법

vue 코드

vue의 v-model은 IME 처리를 완벽하게 수행하지만, 이로 인해 실시간 입력 감지가 필요한 경우에는 추가적인 처리가 필요합니다.
 
이를 해결하려면 v-model 대신 :value와 @input 이벤트를 분리합니다.
즉, v-model은 완성된 입력값(조합 완료 후)만 반영되므로, e.target에서 가져오는 실시간 입력값을 직접 :value에 바인딩해 관리합니다.
이렇게 하면 input에 입력되는 값이 바로 value로 바인딩되고, 이를 활용해 실시간 입력값에 따라 버튼 비활성화, 정규식 검사 등의 처리를 즉시 수행할 수 있습니다.

<div class="dialog-content">
  <input
    :value="value"
    class="input"
    placeholder="텍스트를 작성해주세요"
    autofocus
    @input="
      (e: Event) => {
        value = (e.target as HTMLInputElement).value;
      }
    " />
</div>

 
그러면 다음과 같이 length가 실시간으로 업데이트 됩니다!

( + ) react로 구현하기

react에서는 브라우저의 기본 동작을 그대로 따르기 때문에, 별도의 처리 없이도 IME 입력(조합 중인 값 포함)을 실시간으로 감지할 수 있습니다.
다만, onKeyDown 이벤트가 발생시 글자가 두 번 입력되는 문제가 발생할 수 있으므로 onChange시 isComposing 여부를 체크하는 것을 권장합니다.

function Input() {
  const [text, setText] = useState('');

  const handleKeyDown = (e) => {
    if (!e.nativeEvent.isComposing) {
      sendMessage(text);
      setText('');
    }
  };

  return (
    <input
      value={text}
      onChange={(e) => setText(e.target.value)} // 실시간 업데이트
      onKeyDown={handleKeyDown}
    />
  );
}

 

react와 vue의 차이점

  • vue: v-model이 IME 조합을 자동으로 처리하여 완성된 문자만 인식
  • react: onChange 이벤트가 기본적으로 모든 입력을 실시간으로 감지

 
 

결론


vue의 v-model은 IME(Input Method Editor)를 통한 조합 문자 입력을 자동으로 처리하여 완성된 문자만 인식합니다. 이는 일반적으로는 올바른 동작이지만, 실시간 글자수 체크가 필요한 경우 / 검색 자동완성이 필요한 경우 / 입력 유효성 검사를 실시간으로 해야 하는 경우에는 문제가 될 수 있습니다. 

따라서 한글, 일본어, 중국어 등 조합 문자를 사용하는 환경에서 vue 애플리케이션을 개발할 때는 이러한 IME 처리 방식을 이해하고 적절한 해결책을 적용하는 것이 중요합니다!
 
🙇‍♂️ 참고문헌
아�니 이 글자 왜 들어간 거예요? (1)
JavaScript Input 요소 이벤트에 대하여