React

[React] onKeyDown이 두 번 적용되는 현상을 분석해보자

tony1724 2025. 7. 29. 20:00

팀원이 개발한 채팅 부분에 문제가 보고되었다.

 

한글의 마지막 글자가 두 번 전송되고 있다

 

윈도우 환경에서 테스트를 할 때는 분명 문제가 없었는데..

코드도 큰 문제가 없어보였다

조금 더 조사를 해 본 결과 문제를 제기한 사용자들이 모두 MacOS를 사용했고 한글에서만 문제가 발생한다는 공통적인 특성이 있었다.

 

MacOs에서도 사파리 환경에서는 문제가 없는 걸로 보아 MacOS와 크롬 환경에서 발생하는 문제로 예상된다.

 

윈도우에서는 잘 되는 것으로 보아 아마 두 운영체제 간의 입력 방식에 차이가 있는 것 같은데, 맥북이 없어서 테스트를 할 방법이 없다..

 

하.. 정말 어쩔 수 없네..

그래서.. 정말 어쩔 수 없이 테스트를 위해 맥북 Air M4 16인치 16GB를 구매했다..

 

 

 

keydown에 대해서 먼저 알아보자

먼저, keydown이 어떻게 동작하는지 알기 위해 mdn 사이트에서 해당 함수에 대해 알아보았다.

https://developer.mozilla.org/ko/docs/Web/API/KeyboardEvent

 

KeyboardEvent - Web API | MDN

KeyboardEvent 객체는 키보드와 사용자의 상호 작용을 나타냅니다. 각 이벤트는 사용자와 키보드의 키(또는 보조 키를 같이 눌렀을 때의 결합)를 나타냅니다. 이벤트 타입 (keydown, keypress 또는 keyup)은

developer.mozilla.org

 

입력을 처리하는 이벤트 함수로는 크게 keydown, keypress, keyup으로 세 가지가 있었다.

keypress는 현재 사용을 권장하고 있지 않는다고 한다.

 

keydown과 keyup의 차이?

keydown은 키를 눌렀을 때, 이벤트가 작동한다.

keyup은 키를 뗏을 때, 이벤트가 작동한다.

 

조금 더 자세히 설명을 하자면,

keydown은 키보드를 꾹 누르고 있으면 지속적으로 이벤트가 작동을 하고,

keyup은 반대로 아무리 오래 꾹 누르고 있어도 손가락을 떼는 순간 이벤트가 한번 작동한다.

 

이 차이점을 활용해서 해당 문서에는 재밌는 예제를 설명하고 있다. (급하면 넘어가도 좋다)

    <input id="input1" type="text" placeholder="input1" />
    <br /><br />
    <input id="input2" type="text" placeholder="input2" />

    <script>
      document.addEventListener("keydown", (e) => {
        console.log("[keydown] key:", e.key, "target:", e.target.id);
      });

      document.addEventListener("keyup", (e) => {
        console.log("[keyup] key:", e.key, "target:", e.target.id);
      });
    </script>

이렇게 간단한 input 두 개가 있고,

input1에서 tab을 눌렀을 때 과연 어떤 콘솔 로그가 출력될까?

 

그렇다!

tab을 누른 시점엔 즉각적으로 keydown 이벤트가 작동하여 현재 target.id인 input1이 출력이 된다.

그리고 tab의 변경사항이 적용되어 input2로 포커스가 이동한 순간 keyup이 작동하여 현재 target.id인 input2가 출력된다!

 

 

IME란?

본격적으로 두 환경에서의 입력방식을 비교해 보기에 앞서 IME에 대해서 짚고 넘어가야 한다.

IME란 Input Method Editor의 약자로, 직역하면 입력 방식 편집기이고 쉽게 말해서 입력을 조합해 주는 프로그램이라고 생각하면 쉽다.

한글은 영어와 달리 초성, 중성, 종성을 조합하여 글자를 만들기 때문에 IME를 통해 한글을 조합해주는 절차가 필요하다.

 

다행히도, 이 IME 과정을 알 수 있는 아주 좋은 함수가 존재했다.

https://developer.mozilla.org/ko/docs/Web/API/KeyboardEvent/isComposing

 

KeyboardEvent: isComposing property - Web API | MDN

KeyboardEvent.isComposing 는 읽기 전용 속성으로 compositionstart 이후나 compositionend 이전과 같은 합성 세션 내에서 이벤트가 발생하는지를 불리언 값으로 나타냅니다.

developer.mozilla.org

 

바로 이 isComposing 함수를 통해 현재 입력 이벤트가 한글을 조합 중인지, 아닌지를 알 수 있다.

 

또한, 아래의 세 가지 이벤트리스너를 통해 현재 조합 상태를 확인할 수도 있다.

compositionstart : 조합 시작

compositionupdate : 조합 중

compositionend : 조합 완료

 

input태그와 keydown함수를 활용해 각각의 한글, 엔터 입력에 대해 value, key, code, composing이 어떻게 돌아가는지 테스트 코드를 작성해 보았다.

<!DOCTYPE html>
<html lang="ko">
  <head>
    <meta charset="UTF-8" />
    <title>한글 입력 이벤트 테스트</title>
  </head>
  <body>
    <h2>아래 input에 한글을 입력해보세요</h2>
    <input id="input" type="text" placeholder="여기에 한글 입력" />

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

      input.addEventListener("compositionstart", () => {
        console.log("🟡 compositionstart: 조합 입력 시작");
      });

      input.addEventListener("compositionupdate", (e) => {
        console.log(`🟠 compositionupdate: 현재 조합 → ${e.data}`);
      });

      input.addEventListener("compositionend", (e) => {
        console.log(`🟢 compositionend: 조합 완료 → ${e.data}`);
      });

      input.addEventListener("keydown", (e) => {
        console.log(
          `🔵 value = ${input.value} keydown: key = ${e.key}, code = ${e.code} isComposing = ${e.isComposing}`,
        );
      });
    </script>
  </body>
</html>

 

 

맥북 환경에서의 입력

첫 입력은 keydown의 특성상 아무 문자가 없기 때문에 isComposing = false가 나오고 있다. (keyup이면 true겠죠? ㅎㅎ)

"맥" -> "ㅂ" 이 입력되는 순간 "맥"이라는 글자가 조합되고 composing이 완료되는 것을 확인할 수 있다.

 

하지만, "맥북"을 모두 입력하고 "Enter"를 입력한 순간 이상한 점을 발견할 수 있다.

분명 엔터키를 한번 눌렀으나, 이벤트는 두 번 발생하고 있었다!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

 

key=Enter 이벤트가 두 번 발생했기 때문에 당연하게도 아래의 내 코드는 두 번 작동할 수밖에 없었던 것이다.

 

그럼 윈도우 환경에서는 keydown 이벤트가 한 번만 발생할까?

윈도우 환경에서의 입력

 

어라? 윈도우 환경에서도 Enter를 눌렀을 때 keydown 이벤트가 두 번 발생하고 있었다

 

대신 차이점이 있었는데, 

맥북에서 key는 입력한 문자가 출력되었지만, 윈도우에서 key는 IME 조합 중에는 Process로 출력이 된다.

 

똑같이 keydown 이벤트는 두 번 발생하지만, 윈도우에서는 첫 번째 이벤트의 key가 Enter가 아닌 Process로 왔기 때문에 결과적으로 한 번만 작동한 것이다!

 

 

해결방법

원인을 알아냈기 때문에 해결 방법은 두 가지 정도 있는 것 같다.

 

방법 1) isComposing이 false일 경우에만 전송 처리를 하자!

맥북와 윈도우 환경 모두 isComposing의 타이밍은 동일하였다.

 

엔터를 입력했을 때, keydown 이벤트를 기준으로 IME를 조합 중이기 때문에 isComposing은 true이다.

두 번째 isComposing은 false이다!

따라서, 아래와 같이 isComposing ==== false일 경우에 원하는 코드를 실행하게 두면 된다

react에서 isComposing을 사용하기 위해선 nativeEvent 프로퍼티 내부 메서드를 사용해야 한다.

 

참고로, isComposing === true일 경우는 다음과 같은 일이 발생한다.

한번만 전송되지만 "이"가 value에 남는다.

이건 맥북에서 마지막 글자만 한번 더 전송되는 이유와 관련 있다.

 

먼저 왜 맥북에서만 마지막 글자가 한번 더 전송되었는지 알아보자.

아래는 채팅입력창 코드이다.

 

코드를 보면, onChange에서 입력을 감지하고 e.target.value를 input state에 최신화한다.

"맥북"을 입력하고 엔터를 입력하였을 때,

onKeyDown의 handleKeyDown이 실행되며 첫 번째 keydown 이벤트를 처리한다.

 

handleKeyDown 함수(원래 코드)

기존의 handleKeyDown 함수는 e.key가 Enter일 경우 바로 handleSendMessage 함수를 실행한다.

handleSendMessage 함수

handleSendMessage 함수 내부에선 input을 전송하고, 빈 문자열로 변경한다.

 

그럼 첫번째 Enter keydown 이벤트가 발생한 이 시점에는 "맥북"은 채팅에 전송되고 input이 빈 문자열이 된다.

 

하지만, 이 시점에서는 아직 "북"이 조합이 완료되지 않은 상태이기 때문에

IME에서는 이 첫번째 keydown 이벤트 직후 Enter를 통해 조합을 완성시킨다.

 

쉽게 말해서 keydown 시점 onChange로 input은 "맥북"이 되어 채팅으로 전송되지만,

이 시점에서는 아직 "북"이라는 글자의 조합이 완성되지 않은 것이다!

 

따라서 채팅 전송 직후, "북"이 조합완성되고, onChange가 다시 트리거 된다.

 

그럼 setInput("북")이 들어가게 되고,

다음 두 번째 Enter keydown 이벤트에는 input에 새롭게 저장된 "북"을 전송하게 되는 것이다!

 

이 프로세스가 정확히 이해가 안 간다면 아래 예시를 참고하면 좋을 것 같다.

첫번째 keydown 이벤트 때는 아직 "조합 중"이다!

 

 

자. 이제 거의 다 왔다.

다시 아래의 예제를 봐보자

 

이제는 isComposing === true 일 때 왜 input에 "이"가 남았는지 설명할 수 있다.

 

그렇다.

isComposing === true

첫 번째 Enter 이벤트에는 onChange로 완성된 "하이"은 정상적으로 전송되나,

두 번째 input에 남게 된 "이"는 input에 유효하게 남아있고, 전송되지 않았기 때문이다

 

 

방법 2) keyup을 사용하자

지금까지의 흐름을 모두 이해했다면 제목을 보고 바로 이해했을 것이다.

 

이전엔, keydown의 특성상 조합이 완료되기 전 전송 후 초기화를 했기 때문에 문제가 발생하였다.

 

그렇다면, 조합이 완료된 후에 깔끔하게 전송을 하면 되지 않을까?

textarea에서는 개행처리를 따로 해줘야한다.

이렇게 한다면, 아래와 같이 조합이 완료된 후 "맥북"을 전송하게 되고, 빈 문자열로 정상적으로 초기화된다.

물론, 이벤트는 동일하게 두 번 발생하기 때문에 빈 문자열일 경우 전송을 막아주기만 하면 해결될 것 같다!

 

keyup + macOS 환경

 

 

 

 

왜 Enter는 keydown 이벤트가 두 번 발생할까? (번외)

구글링을 아무리 해봐도 해결방법만 있을 뿐, keydown 이벤트가 두 번 발생하는 근본적인 이유는 명확하게 기재되어 있지 않았다.

MacOS + Safari 환경

그리고 MacOS + Safari 환경에서는 keydown 이벤트가 한 번만 발생하는 것으로 보아, 아마 크롬 브라우저에서 의도한 사항으로 보인다.

 

이처럼 첫 번째 keydown은 IME에서 조합을 진행한다는 의미의 이벤트 발생,

조합이 끝나고 실제 브라우저 단에서 개행 등, Enter key 처리를 위한 두 번째 keydown 이벤트를 의도적으로 발생시키는 듯하다.

 

이유는 코드를 뜯어보거나 크롬 개발자들만이 알겠지만 아마 조합 처리 이전과 이후의 조금 더 세밀한 이벤트 처리를 위함이거나, 다양한 환경에서의 호환성을 위해 개발된 사항이지 않을까 생각된다.

 

 

 

마무리하며..

간단하게 원인만 찾고 해결하려 했으나 꼬리에 꼬리를 밟아 결국 크롬 소스코드까지 뒤져보게 되었다.

생각보다 복잡한 원인과 이벤트 출력방식에 고생을 많이 했는데, 몰랐던 것들을 하나씩 이해해 나가며 문제에 다가가는 과정이 즐겁고 뿌듯하게 다가왔다.

 

 

참고자료

https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent

 

KeyboardEvent - Web APIs | MDN

KeyboardEvent objects describe a user interaction with the keyboard; each event describes a single interaction between the user and a key (or combination of a key with modifier keys) on the keyboard. The event type (keydown, keypress, or keyup) identifies

developer.mozilla.org

https://github.com/vuejs/vue/issues/10277

 

Korean input trigger keydown event twice · Issue #10277 · vuejs/vue

Version 2.6.10 Reproduction link https://jsfiddle.net/0zj6narq/42/ Steps to reproduce type Korean input keydown event like Arrow Up, Down, Left, Right, Enter, Tab ... What is expected? keydown even...

github.com