MMZ 일지 19. 한글 입력할 때 2번 입력이 되는 경우 in React, Typescript

14
post-thumbnail

혹시나 잘못된 개념 전달이 있다면 댓글 부탁드립니다. 저의 성장의 도움이 됩니다.

에러 상황

  • onKeyUp 이벤트 적용

사진처럼 끝자리가 한글인 경우에 이벤트로 값을 추가하면 2번 입력되었다. ㅎ1 처럼 숫자로 끝나거나 영어로 끝나는 경우에는 이 현상이 발생하지 않는다.
(빈값이 추가된 이유는 target.value = ""; 을 했기 때문이다. 이래서 원인 찾기가 더 어려웠다.)


원인 : IME(input method editor) 문제

입력기 또는 입력 방식 편집기(input method editor, IME)는 한글, 한자처럼 컴퓨터 자판에 있는 글자보다 수가 더 많은 문자를 계산하거나 조합하여 입력해 주는 시스템 소프트웨어이다.

onKeyDown, onKeyUp 이벤트에 한글 뿐만 아니라, 일본어, 중국어 등에서 발생하는 문제라고 한다.

그러나 IME 과정에서 keydown 이벤트가 발생하면, OS와 브라우저에서 해당 이벤트를 모두 처리하기 때문에 keydown 이벤트가 중복으로 발생하게 되는 것이다.
참조한 블로그

왜 2번 동작하는지는 언급된 블로그이다.

조합 중인지는 KeyboardEvent.isComposing의 값(boolean으로 반환)으로 확인할 수 있다. 2번 동작할 때 isComposing을 console.log()로 확인하면 첫번째는 true, 두번째는 false로 찍힌다. 첫번째는 조합중일 때 발생하고, 두번째는 조합이 끝난 후에 실행된다.

즉, 이벤트가 2번 실행되는 것은 제어할 수 없지만, isComposing 의 값에 따라 실행할 동작을 1번만 수행하도록 조건을 걸어주면 해결된다.


해결방안 2가지

(1) onKeyPress로 이벤트 변경

가장 간편한 방법으로 이벤트의 종류를 바꿀 수 있다.
하지만 이 방법을 추천하지 않는다. 키가 먹히고 안먹히고 보다 onKeyPress 이벤트 사용을 자제해야한다.

차이점

  • onKeyDown, onKeyUp
    키보드를 누르거나 땔 때 발생하는 이벤트이다.

  • onKeyPress
    문자가 생성될 때 발생하는 이벤트로, 문자를 발생하지 않는 alt, shift, ctrl 등의 키는 인식하지 못한다.

The keydown and keyup events provide a code indicating which key is pressed, while keypress indicates which character was entered. For example, a lowercase "a" will be reported as 65 by keydown and keyup, but as 97 by keypress. An uppercase "A" is reported as 65 by all events.

즉, 같은 버튼을 눌러도 어떤 키 vs 어떤 값 인지에 따라 차이가 발생한다.
가장 쉬운 방법이지만 큰 문제가 있다.


추천하지 않는 이유

MDN 문서에서 해당 이벤트는 더이상 지원하지 않을 예정이란다. 식자도 이 방법으로 테스트를 해보니 해결은 되어 아직까지는 이벤트를 지원하지만 다른 방법을 추천한다.
MDN keypress 이벤트

해결한 코드

위에 설명한 이유로 해당방법은 추천하지 않는다.

const TagsMaker = () => {
  const dispatch = useAppDispatch();
  const tagsDatas = useAppSelector((state) => state.recipe.inputTexts.tags);
  const [inputValue, setInputValue] = useState("");

  const changeInputValueHandler = (event: KeyboardEvent<HTMLInputElement>) => {
    const target = event.target as HTMLInputElement;
    if (event.key === "Enter") {
      dispatch(recipeActions.addTag({ name: target.value }));
      setInputValue("");
      // target.value = ""; 
      // 주석은 수정 전 코드
      // inputValue를 선언 안해서 발생한 문제인줄 알았으나 아니었다.
      // 오히려, 비동기 작업이 진행되기 전 빈 문자열로 할당해서 똑같은 값이 2번 찍히지 않고 빈값으로 찍혔던 것이다.
    }
  };

  return (
    <STagsContainer>
      {tagsDatas.map((tagData, idx) => {
        return <TagWithBtn key={idx} tagValue={tagData.name} id={idx} />;
      })}
      <SNonOutlineInput
        name="name"
        value={inputValue}
        onChange={(event) => {
          setInputValue(event.target.value);
        }}
        onKeyPress={changeInputValueHandler}
		// 문제를 해결한 코드
		// 아래의 주석이 문제였다.
        // onKeyUp={changeInputValueHandler}
        placeholder="태그를 추가해주세요."
      />
    </STagsContainer>
  );
};
export default TagsMaker;


(2) onKeyUp, onKeyDown 이벤트는 유지하고 isComposing 반환값을 이용

목표

isComposing 의 값에 따라 실행할 동작을 1번만 수행하도록 조건을 걸어주면 해결

isComposing의 위치

공식문서에처럼 이벤트에서 isComposing 프로퍼티를 조회할 수 없다.

  • event.nativeEvent.isComposing
    event객체의 isComposing 프로퍼티는 event.nativeEvent 내부에서 조회할 수 있다.

isComposing의 반환값

  • boolean

true일 때

조건문을 사용하면 되는데 true인 경우 실행할 동작에 접근하지 못하도록 함수를 종료한다.

// 이벤트 핸들러 함수를 종료
if (event.nativeEvent.isComposing) {
  return ;
}

false일 때

원하는 동작이 수행되도록 조건을 추가한다.


해결한 코드

isComposing가 false일 때 동작하도록 했다.

const TagsMaker = () => {
  const dispatch = useAppDispatch();
  const tagsDatas = useAppSelector((state) => state.recipe.inputTexts.tags);
  const [inputValue, setInputValue] = useState("");

  const changeInputValueHandler = (event: KeyboardEvent<HTMLInputElement>) => {
    const target = event.target as HTMLInputElement;
	
    // event.nativeEvent.isComposing === false 인 경우만 실행되도록 변경
    // 2번 실행되지만 첫번째 이벤트에서는 해당 코드가 실행되지 않는다.
    if (event.key === "Enter" && event.nativeEvent.isComposing === false) {
      dispatch(recipeActions.addTag({ name: target.value }));
      setInputValue("");
    }
  };

  return (
    <STagsContainer>
      {tagsDatas.map((tagData, idx) => {
        return <TagWithBtn key={idx} tagValue={tagData.name} id={idx} />;
      })}
      <SNonOutlineInput
        name="name"
        value={inputValue}
        onChange={(event) => {
          setInputValue(event.target.value);
        }}
        onKeyDown={changeInputValueHandler}
        placeholder="태그를 추가해주세요."
      />
    </STagsContainer>
  );
};
export default TagsMaker;

TIL

어느 경우에 빈값이 추가되는지 파악하기도 어려웠다. value를 빈 문자열로 할당하는 것 때문에, 더 혼란스러웠고 state 때문에 발생한 오류인가 했다. 태그를 추가하면서 어떤 경우에 문제가 발생하는지 찾아보니 한글에서 발생한 오류라 신기했다.

구글링하면서 onkeypress로 변환하는 방식이 가장 많이 나왔고, isComposing 방법은 IDE에서 자동완성으로 안떠서 조회하는 위치도, 이해하기도 어려웠었다. 특히, 왜 2번 동작하는지와 구분을 isComposing으로 가능한 이유에 대해서 알아보는게 어려웠다. 하지만 이해를 해야 글을 쓸 수 있으니 자세하게 알아보았다.

최근 빠르게에 집착하다보니 블로깅하는 것에 의문을 가졌지만 배우는 점이 많은 것 같다. 특히 비교하고 어떤 방식으로 해야할지 알아보는 것에 대해 생각이 많았는데, 이번 에러 핸들링에서 좋은 방법이겠다 싶었다. 화이팅!🔥

2개의 댓글

comment-user-thumbnail
2023년 11월 20일

많은 도움 됬습니다. 감사합니다!

답글 달기
comment-user-thumbnail
2024년 8월 3일

덕분에 onKeyPress 대신 다른것을 사용할 수 있었습니다. 감사합니다!

답글 달기